<?php

namespace Limb_Chatbot\Includes\Migrations;


use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Data_Objects\Chatbot_Meta;
use Limb_Chatbot\Includes\Data_Objects\Setting;
use Limb_Chatbot\Includes\Data_Schemas\Setting as Setting_Schema;
use Limb_Chatbot\Includes\Services\Ai_Model_Catalog_Refresher;
use Limb_Chatbot\Includes\Services\Channel_Events\Channel_Event_Repository;
use Limb_Chatbot\Includes\Services\Notifications\Notification_Service;
use Limb_Chatbot\Includes\Services\Seeder_Service;

/**
 * Migration for version 2.0.0.
 *
 * Adds access roles and type defaults for vectors and actions.
 *
 * @since 2.0.0
 */
class Migration_2_0_0 extends Abstract_Migration {

	/**
	 * Special role allowing universal access.
	 */
	private const ROLE_EVERYONE = 'everyone';
	/**
	 * Default action type.
	 */
	private const ACTION_TYPE_DEFAULT = 1;
	/**
	 * Default action config for migrated rows.
	 */
	private const ACTION_DEFAULT_CONFIG = [
		'show_fields'   => 1,
		'callback_text' => null,
	];

	/**
	 * Models to delete during this migration.
	 *
	 * @since 2.0.0
	 */
	private const DEPRECATED_MODELS = [
		'claude-3-haiku-20240307',
		'gemini-2.5-flash',
		'gemini-2.5-flash-lite',
		'gemini-2.5-pro',
		'imagen-3.0-generate-002',
	];

	/**
	 * @inheritDoc
	 */
	public function get_version(): string {
		return '2.0.0';
	}

	/**
	 * @inheritDoc
	 */
	public function get_description(): string {
		return 'Add ip blocks table, migrate widgets to table, and add type/access_roles/config updates for vectors and actions.';
	}

	/**
	 * @inheritDoc
	 */
	public function up(): bool {
		// Seed missing unified notification setting for older installs.
		$this->seed_unified_notification_settings();

		if ( ! $this->migrate_ip_blocks_table() ) {
			return false;
		}
		if ( ! $this->migrate_widgets_table() ) {
			return false;
		}
		if ( ! $this->seed_default_widgets() ) {
			return false;
		}
		if ( ! $this->migrate_vectors() ) {
			return false;
		}
		if ( ! $this->migrate_actions() ) {
			return false;
		}

		if ( ! $this->create_events_table() ) {
			return false;
		}

		if ( ! $this->create_ratings_table() ) {
			return false;
		}

		if ( ! $this->migrate_chatbot_limits() ) {
			return false;
		}

		$refresher = new Ai_Model_Catalog_Refresher(
			fn( string $message, string $level = 'info' ) => $this->log( $message, $level ),
			fn( string $key, $value ) => $this->maybe_add_setting( $key, $value )
		);

		if ( ! $refresher->run( self::DEPRECATED_MODELS ) ) {
			return false;
		}

		if ( ! $this->migrate_chatbot_settings() ) {
			return false;
		}



		$this->log( 'Migration 2.0.0 completed successfully.' );

		return true;
	}

	/**
	 * Seed missing `lbaic.plugin.notifications` setting for older installs.
	 *
	 * Also migrates legacy internal notification role slugs (e.g. `admin`)
	 * into native WP role slugs (e.g. `administrator`) and normalizes the
	 * shape to `visibility.roles/custom_emails`.
	 *
	 * @return void
	 * @since 2.0.1
	 */
	private function seed_unified_notification_settings(): void {
		$key = 'lbaic.plugin.notifications';

		if ( ! Setting::exists( $key ) ) {
			Setting::create(
				[
					'key'   => $key,
					'value' => Notification_Service::get_defaults(),
				]
			);
		}
	}

	/**
	 * Create IP blocks table if needed.
	 *
	 * @return bool
	 * @since 2.0.0
	 */
	private function migrate_ip_blocks_table(): bool {
		$ip_blocks_table = $this->wpdb->prefix . 'lbaic_ip_blocks';
		if ( ! $this->table_exists( $ip_blocks_table ) ) {
			$collate = $this->wpdb->get_charset_collate();
			$schema  = "CREATE TABLE {$ip_blocks_table} (
				id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
				ip varchar(45) NOT NULL,
				location varchar(255) NULL,
				reason text NULL,
				type varchar(20) DEFAULT 'manual' NOT NULL,
				created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
				blocked_until datetime NULL,
				chatbot_user_id bigint(20) unsigned NULL,
				PRIMARY KEY (id),
				KEY ip (ip),
				KEY chatbot_user_id (chatbot_user_id),
				KEY blocked_until (blocked_until),
				KEY type (type),
				KEY created_at (created_at)
			) {$collate};";
			$this->db_delta( $schema );
		}

		if ( ! $this->table_exists( $ip_blocks_table ) ) {
			$this->log( "Failed creating ip blocks table {$ip_blocks_table}", 'error' );

			return false;
		}

		return true;
	}

	/**
	 * Create widgets table if needed and migrate existing legacy widgets.
	 *
	 * @return bool
	 */
	private function migrate_widgets_table(): bool {
		$widgets_table = $this->wpdb->prefix . 'lbaic_widgets';
		if ( ! $this->table_exists( $widgets_table ) ) {
			$collate = $this->wpdb->get_charset_collate();
			$schema  = "CREATE TABLE {$widgets_table} (
				id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
				uuid char(36) NOT NULL,
				type varchar(64) NOT NULL,
				published tinyint(1) DEFAULT 1 NOT NULL,
				config longtext NULL,
				locations longtext NULL,
				visibility longtext NULL,
				`order` int(11) DEFAULT 0 NOT NULL,
				created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
				updated_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
				PRIMARY KEY (id),
				UNIQUE KEY uuid (uuid),
				KEY type (type),
				KEY published (published),
				KEY `order` (`order`),
				KEY created_at (created_at),
				KEY updated_at (updated_at)
			) {$collate};";
			$this->db_delta( $schema );
		}

		if ( ! $this->table_exists( $widgets_table ) ) {
			$this->log( "Failed creating widgets table {$widgets_table}", 'error' );

			return false;
		}

		return true;
	}

	/**
	 * Seed default widgets for installs that do not have any widgets yet.
	 *
	 * @return bool
	 * @since 2.0.0
	 */
	private function seed_default_widgets(): bool {
		try {
			( new Seeder_Service( false, true ) )->seed_default_widgets_once();
		} catch ( \Exception $e ) {
			$this->log( 'Failed seeding default widgets: ' . $e->getMessage(), 'error' );
			return false;
		}

		return true;
	}

	/**
	 * Add/access_roles for vectors and backfill existing rows.
	 *
	 * @return bool
	 */
	private function migrate_vectors(): bool {
		$vectors_table = $this->wpdb->prefix . 'lbaic_vectors';
		if ( ! $this->table_exists( $vectors_table ) ) {
			$this->log( "Table {$vectors_table} does not exist, skipping." );

			return true;
		}

		if ( ! $this->column_exists( $vectors_table, 'access_roles' ) ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$result = $this->wpdb->query( "ALTER TABLE `{$vectors_table}` ADD COLUMN access_roles longtext NULL AFTER config_id" );
			if ( false === $result ) {
				$this->log( 'Failed adding access_roles column: ' . $this->wpdb->last_error, 'error' );

				return false;
			}
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$updated = $this->wpdb->query(
			$this->wpdb->prepare(
				"UPDATE `{$vectors_table}` SET access_roles = %s",
				wp_json_encode( [ self::ROLE_EVERYONE ] )
			)
		);
		if ( false === $updated ) {
			$this->log( 'Failed setting default access_roles for existing vectors: ' . $this->wpdb->last_error,
				'error' );

			return false;
		}

		return true;
	}

	/**
	 * Add type/access_roles/config for actions and backfill existing rows.
	 *
	 * @return bool
	 */
	private function migrate_actions(): bool {
		$actions_table = $this->wpdb->prefix . 'lbaic_actions';
		if ( ! $this->table_exists( $actions_table ) ) {
			$this->log( "Table {$actions_table} does not exist, skipping." );

			return true;
		}

		if ( ! $this->column_exists( $actions_table, 'type' ) ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$result = $this->wpdb->query( "ALTER TABLE `{$actions_table}` ADD COLUMN `type` tinyint(1) DEFAULT 1 NOT NULL AFTER name" );
			if ( false === $result ) {
				$this->log( 'Failed adding type column to actions: ' . $this->wpdb->last_error, 'error' );

				return false;
			}
		}

		if ( ! $this->column_exists( $actions_table, 'access_roles' ) ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$result = $this->wpdb->query( "ALTER TABLE `{$actions_table}` ADD COLUMN access_roles longtext NULL AFTER `type`" );
			if ( false === $result ) {
				$this->log( 'Failed adding access_roles column to actions: ' . $this->wpdb->last_error, 'error' );

				return false;
			}
		}
		if ( ! $this->column_exists( $actions_table, 'config' ) ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$result = $this->wpdb->query( "ALTER TABLE `{$actions_table}` ADD COLUMN config longtext NULL AFTER access_roles" );
			if ( false === $result ) {
				$this->log( 'Failed adding config column to actions: ' . $this->wpdb->last_error, 'error' );

				return false;
			}
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$updated = $this->wpdb->query(
			$this->wpdb->prepare(
				"UPDATE `{$actions_table}` SET access_roles = %s, `type` = %d, config = IF(config IS NULL OR config = '', %s, config)",
				wp_json_encode( [ self::ROLE_EVERYONE ] ),
				self::ACTION_TYPE_DEFAULT,
				wp_json_encode( self::ACTION_DEFAULT_CONFIG )
			)
		);
		if ( false === $updated ) {
			$this->log( 'Failed setting default access_roles/type/config for existing actions: ' . $this->wpdb->last_error,
				'error' );

			return false;
		}

		return true;
	}

	private function create_events_table() {
		$table = $this->wpdb->prefix . Channel_Event_Repository::TABLE_NAME;

		if ( $this->table_exists( $table ) ) {
			$this->log( "Table {$table} already exists — skipping." );

			return true;
		}

		$collate = $this->wpdb->get_charset_collate();
		$schema  = "CREATE TABLE {$table} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			event_type varchar(64) NOT NULL,
			chat_uuid char(36) NULL,
			chatbot_user_uuid char(36) NULL,
			recipient_wp_user_id bigint(20) unsigned NOT NULL,
			payload longtext NOT NULL,
			is_read tinyint(1) DEFAULT 0 NOT NULL,
			created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
			PRIMARY KEY (id),
			KEY event_type (event_type),
			KEY chat_uuid (chat_uuid),
			KEY chatbot_user_uuid (chatbot_user_uuid),
			KEY recipient_wp_user_id (recipient_wp_user_id),
			KEY is_read (is_read),
			KEY created_at (created_at)
		) {$collate};";

		$this->db_delta( $schema );

		if ( ! $this->table_exists( $table ) ) {
			$this->log( "Failed to create table {$table}.", 'error' );

			return false;
		}

		$this->log( "Table {$table} created successfully." );

		return true;
	}

	/**
	 * Create ratings table if needed.
	 *
	 * @return bool
	 * @since 2.0.0
	 */
	private function create_ratings_table(): bool {
		$table = $this->wpdb->prefix . 'lbaic_ratings';

		if ( $this->table_exists( $table ) ) {
			$this->log( "Table {$table} already exists — skipping." );

			return true;
		}

		$collate = $this->wpdb->get_charset_collate();
		$schema  = "CREATE TABLE {$table} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			from_type varchar(100) NULL,
			from_id bigint(20) unsigned NULL,
			target_type varchar(100) NULL,
			target_id bigint(20) unsigned NULL,
			rate int(11) NOT NULL,
			notes text NULL,
			metadata longtext NULL,
			chatbot_user_id bigint(20) unsigned NULL,
			created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
			source_id varchar(191) NULL,
			PRIMARY KEY (id),
			KEY from_type (from_type),
			KEY from_id (from_id),
			KEY target_type (target_type),
			KEY target_id (target_id),
			KEY chatbot_user_id (chatbot_user_id),
			KEY created_at (created_at),
			KEY source_id (source_id)
		) {$collate};";

		$this->db_delta( $schema );

		if ( ! $this->table_exists( $table ) ) {
			$this->log( "Failed to create table {$table}.", 'error' );

			return false;
		}

		$this->log( "Table {$table} created successfully." );

		return true;
	}

	/**
	 * Replace old chatbot usage limits with the new tiered defaults.
	 *
	 * Applies to:
	 *  1. The default (non-post) chatbot stored in wp_options (`lbaic.utilities.chatbot.limits`).
	 *  2. Every published `lbaic_chatbot` CPT post via its `_limits` postmeta.
	 *
	 * The new defaults are sourced from the schema so that a single source of
	 * truth is maintained in Data_Schemas\Setting::chatbot_rules().
	 *
	 * @return bool
	 * @since 2.0.0
	 */
	private function migrate_chatbot_limits(): bool {
		$limits_key    = Setting::SETTING_PREFIX . Chatbot::SETTING_PREFIX . 'limits';
		$chatbot_rules = Setting_Schema::chatbot_rules();

		if ( ! isset( $chatbot_rules[ $limits_key ]['default'] ) ) {
			$this->log( 'Could not resolve default limits from schema — skipping limits migration.', 'error' );

			return false;
		}

		$new_defaults = $chatbot_rules[ $limits_key ]['default'];

		// 1. Update the default chatbot setting in wp_options.
		if ( Setting::exists( $limits_key ) ) {
			Setting::update( [ 'key' => $limits_key ], [ 'value' => $new_defaults ] );
			$this->log( 'Default chatbot limits updated in wp_options.' );
		}

		// Also update preview variant if it exists.
		$preview_key = $limits_key . Setting::PREVIEW_POSTFIX;
		if ( Setting::exists( $preview_key ) ) {
			Setting::update( [ 'key' => $preview_key ], [ 'value' => $new_defaults ] );
			$this->log( 'Default chatbot limits (preview) updated in wp_options.' );
		}

		// 2. Update every chatbot CPT post's _limits meta via Data Objects.
		$chatbots = Chatbot::where( [] );

		if ( $chatbots->is_empty() ) {
			$this->log( 'No chatbot posts found — nothing to migrate for postmeta limits.' );

			return true;
		}

		$updated_count    = 0;
		$preview_meta_key = '_limits' . Setting::PREVIEW_POSTFIX;

		foreach ( $chatbots->get() as $chatbot ) {
			if ( ! $chatbot instanceof Chatbot || empty( $chatbot->get_id() ) ) {
				continue;
			}

			// Update or create the _limits meta.
			$chatbot->update_parameter( 'limits', $new_defaults );
			++$updated_count;

			// Also update _limits.preview meta only if it already exists.
			if ( $chatbot->has_meta( $preview_meta_key ) ) {
				Chatbot_Meta::update(
					[ 'meta_key' => $preview_meta_key, 'chatbot_id' => $chatbot->get_id() ],
					[ 'meta_value' => $new_defaults ]
				);
			}
		}

		$this->log( "Chatbot limits migrated for {$updated_count} chatbot post(s)." );

		return true;
	}

	/**
	 * Placeholder for any necessary chatbot settings migrations.
	 *
	 * This method can be expanded in the future to handle any specific
	 * transformations needed for chatbot-related settings during this migration.
	 *
	 * @return bool True on success, false on failure.
	 * @throws \Exception
	 * @since 2.0.0
	 */
	private function migrate_chatbot_settings() {
		$chatbot = Chatbot::make();
		$avatar  = $chatbot->get_parameter( 'avatar' );
		if ( $avatar == 2 ) {
			// do nothing
		} else {
			$chatbot->update_parameter( 'avatar', 0 );
		}
		$shadow_dom = $chatbot->get_parameter( 'shadow_dom' );
		if ( $shadow_dom == 1 ) {
			// do nothing
		} else {
			$chatbot->update_parameter( 'shadow_dom', 1 );
		}

		return true;
	}
}

