<?php

declare(strict_types=1);

namespace SecurWP\Core\Database;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

final class MigrationManager {

	public const CRON_HOOK = 'securwp_db_migration_runner';

	private const SCHEMA_MAP_OPTION = 'securwp_db_schema_map';
	private const LAST_ERROR_OPTION = 'securwp_db_migration_last_error';
	private const LAST_RUN_OPTION = 'securwp_db_migration_last_run';

	private static bool $bootstrapped = false;
	private static bool $ran_this_request = false;

	public static function bootstrap(): void {
		if ( self::$bootstrapped ) {
			return;
		}

		self::$bootstrapped = true;

		add_action( self::CRON_HOOK, [ self::class, 'run_scheduled' ] );
		add_action( 'admin_notices', [ self::class, 'render_admin_notice' ] );

		if ( ! self::has_pending_version_delta() && ! self::has_last_error() ) {
			return;
		}

		add_action( 'init', [ self::class, 'maybe_schedule' ], 1 );
		add_action( 'admin_init', [ self::class, 'run_admin' ], 1 );

		if ( self::should_run_inline() ) {
			self::run_migrations( self::detect_runner() );
		}
	}

	public static function run_admin(): void {
		self::run_migrations( 'admin' );
	}

	public static function run_scheduled(): void {
		self::run_migrations( 'cron' );
	}

	public static function maybe_schedule(): void {
		if ( ! self::has_pending_version_delta() && ! self::has_last_error() ) {
			return;
		}

		if ( wp_next_scheduled( self::CRON_HOOK ) ) {
			return;
		}

		wp_schedule_single_event( time() + MINUTE_IN_SECONDS, self::CRON_HOOK );
	}

	public static function run_migrations( string $runner ): bool {
		if ( self::$ran_this_request ) {
			return true;
		}

		$token = MigrationLock::acquire( $runner );
		if ( ! is_string( $token ) || $token === '' ) {
			return false;
		}

		self::$ran_this_request = true;

		try {
			$schema_map = self::get_schema_map();

			foreach ( MigrationRegistry::all() as $migration ) {
				$module = $migration->module_key();
				$target_version = $migration->target_version();
				$stored_version = (int) ( $schema_map[ $module ] ?? 0 );
				$is_applied = $migration->is_applied();

				if ( $stored_version >= $target_version && $is_applied ) {
					continue;
				}

				if ( ! $is_applied ) {
					$result = $migration->apply();
					SchemaInspector::flush_cache();
					if ( ! $result || ! $migration->is_applied() ) {
						throw new \RuntimeException( sprintf( 'Migration %s did not complete successfully.', get_class( $migration ) ) );
					}
				}

				$schema_map[ $module ] = $target_version;
				self::persist_schema_map( $schema_map );
			}

			delete_option( self::LAST_ERROR_OPTION );
			update_option(
				self::LAST_RUN_OPTION,
				[
					'status'     => 'success',
					'runner'     => sanitize_key( $runner ),
					'completed'  => time(),
					'schema_map' => self::get_schema_map(),
				],
				false
			);
			wp_clear_scheduled_hook( self::CRON_HOOK );

			return true;
		} catch ( \Throwable $throwable ) {
			$error = [
				'message'   => sanitize_text_field( $throwable->getMessage() ),
				'runner'    => sanitize_key( $runner ),
				'failed_at' => time(),
			];
			update_option( self::LAST_ERROR_OPTION, $error, false );
			update_option(
				self::LAST_RUN_OPTION,
				[
					'status'    => 'failed',
					'runner'    => sanitize_key( $runner ),
					'completed' => time(),
				],
				false
			);
			securwp_log( '[SecurWP] DB migration failed: ' . $error['message'] );

			return false;
		} finally {
			MigrationLock::release( $token );
		}
	}

	/**
	 * Rebuild the stored schema map from the migrations that are actually applied.
	 *
	 * This is used after direct activation-time provisioning so runtime code can
	 * trust the schema map without relying on optimistic "current" markers.
	 */
	public static function sync_registry_state( string $runner = 'runtime' ): bool {
		SchemaInspector::flush_cache();

		$schema_map = [];
		$current    = true;

		foreach ( MigrationRegistry::all() as $migration ) {
			if ( ! $migration->is_applied() ) {
				$current = false;
				continue;
			}

			$module = $migration->module_key();
			$schema_map[ $module ] = max(
				(int) ( $schema_map[ $module ] ?? 0 ),
				$migration->target_version()
			);
		}

		self::persist_schema_map( $schema_map );
		delete_option( self::LAST_ERROR_OPTION );

		update_option(
			self::LAST_RUN_OPTION,
			[
				'status'     => $current ? 'current' : 'pending',
				'runner'     => sanitize_key( $runner ),
				'completed'  => time(),
				'schema_map' => self::get_schema_map(),
			],
			false
		);

		if ( $current ) {
			wp_clear_scheduled_hook( self::CRON_HOOK );
			return true;
		}

		self::maybe_schedule();

		return false;
	}

	/**
	 * Determine whether a specific module has reached its registered schema target.
	 */
	public static function is_module_current( string $module_key ): bool {
		$module_key = sanitize_key( $module_key );
		if ( '' === $module_key ) {
			return false;
		}

		$target_map = MigrationRegistry::target_schema_map();
		$target     = (int) ( $target_map[ $module_key ] ?? 0 );
		if ( $target < 1 ) {
			return false;
		}

		$stored_map = self::get_schema_map();

		return (int) ( $stored_map[ $module_key ] ?? 0 ) >= $target;
	}

	public static function clear_runtime_state(): void {
		delete_option( self::SCHEMA_MAP_OPTION );
		delete_option( self::LAST_ERROR_OPTION );
		delete_option( self::LAST_RUN_OPTION );
		delete_option( 'securwp_db_migration_lock' );
		wp_clear_scheduled_hook( self::CRON_HOOK );
	}

	public static function render_admin_notice(): void {
		if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
			return;
		}

		$error = get_option( self::LAST_ERROR_OPTION, null );
		if ( ! is_array( $error ) || empty( $error['message'] ) ) {
			return;
		}

		$message = sanitize_text_field( (string) $error['message'] );
		echo '<div class="notice notice-error"><p>SecurWP database migration failed: ' . esc_html( $message ) . '</p></div>';
	}

	private static function should_run_inline(): bool {
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return true;
		}

		if ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) {
			return true;
		}

		if ( is_admin() ) {
			return true;
		}

		return self::is_login_request();
	}

	private static function detect_runner(): string {
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return 'cli';
		}

		if ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) {
			return 'cron';
		}

		if ( self::is_login_request() ) {
			return 'login';
		}

		if ( is_admin() ) {
			return 'admin';
		}

		return 'runtime';
	}

	private static function is_login_request(): bool {
		return isset( $GLOBALS['pagenow'] ) && $GLOBALS['pagenow'] === 'wp-login.php';
	}

	private static function has_pending_version_delta(): bool {
		$stored = self::get_schema_map();
		$target = MigrationRegistry::target_schema_map();

		foreach ( $target as $module => $version ) {
			if ( (int) ( $stored[ $module ] ?? 0 ) < (int) $version ) {
				return true;
			}
		}

		return false;
	}

	private static function has_last_error(): bool {
		return is_array( get_option( self::LAST_ERROR_OPTION, null ) );
	}

	private static function get_schema_map(): array {
		$value = get_option( self::SCHEMA_MAP_OPTION, [] );
		return self::sanitize_schema_map( is_array( $value ) ? $value : [] );
	}

	private static function persist_schema_map( array $schema_map ): void {
		update_option( self::SCHEMA_MAP_OPTION, self::sanitize_schema_map( $schema_map ), false );
	}

	private static function sanitize_schema_map( array $schema_map ): array {
		$clean = [];

		foreach ( $schema_map as $module => $version ) {
			$key = sanitize_key( (string) $module );
			if ( $key === '' ) {
				continue;
			}

			$clean[ $key ] = max( 0, (int) $version );
		}

		ksort( $clean );

		return $clean;
	}
}
