<?php

declare(strict_types=1);

namespace Australcode\Rewards\Infrastructure\Database;

use Australcode\Rewards\Infrastructure\Database\Migrations\Migration;

defined( 'ABSPATH' ) || exit;

/**
 * Versioned migration runner. Auto-bootstraps its own control table
 * (wp_acrwd_migrations) before running anything, then applies pending
 * migrations in ascending version order. Idempotent: already-applied versions
 * are skipped, so it is safe to call on every boot.
 */
final class Migrator {

	/** @var Migration[] */
	private array $migrations;

	/**
	 * @param Migration[] $migrations
	 */
	public function __construct( array $migrations ) {
		usort(
			$migrations,
			static fn ( Migration $a, Migration $b ): int => $a->version() <=> $b->version()
		);
		$this->migrations = $migrations;
	}

	public function migrate(): void {
		$this->ensure_control_table();
		$applied = $this->applied_versions();

		foreach ( $this->migrations as $migration ) {
			if ( in_array( $migration->version(), $applied, true ) ) {
				continue;
			}
			$migration->up();
			$this->record_applied( $migration );
		}
	}

	private function ensure_control_table(): void {
		global $wpdb;

		$this->bootstrap_legacy_migrations_table_rename();

		$table   = Schema::table_name( Schema::TABLE_MIGRATIONS );
		$charset = $wpdb->get_charset_collate();

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		// dbDelta-strict format: single-space indent, lowercase sized types,
		// two spaces after PRIMARY KEY.
		$sql = "CREATE TABLE {$table} (
 version bigint(20) unsigned NOT NULL,
 description varchar(191) NOT NULL DEFAULT '',
 applied_at datetime NOT NULL,
 PRIMARY KEY  (version)
) {$charset};";

		dbDelta( $sql );
	}

	/**
	 * Bootstrap step for the Australcode rebrand: a pre-rebrand install has its
	 * control table at `{prefix}pointly_migrations`. Rename it to the current
	 * name BEFORE reading applied versions, otherwise the runner would see an
	 * empty control table and re-run M0001..M0004 over data tables M0005 has not
	 * renamed yet — splitting the schema. Guarded (old exists && new absent), so
	 * it is idempotent and a no-op on fresh installs.
	 *
	 * `pointly_migrations` is one of the two intentional legacy references in the
	 * codebase (the other is M0005); a rename must name its source.
	 */
	private function bootstrap_legacy_migrations_table_rename(): void {
		global $wpdb;
		$old = $wpdb->prefix . 'pointly_migrations';
		$new = Schema::table_name( Schema::TABLE_MIGRATIONS );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- existence probe for a one-off rename bootstrap.
		$old_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $old ) ) === $old;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- existence probe for a one-off rename bootstrap.
		$new_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $new ) ) === $new;

		if ( $old_exists && ! $new_exists ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- table names from Schema + fixed legacy prefix, not user input.
			$wpdb->query( "RENAME TABLE `{$old}` TO `{$new}`" );
		}
	}

	/**
	 * @return int[]
	 */
	private function applied_versions(): array {
		global $wpdb;
		$table = Schema::table_name( Schema::TABLE_MIGRATIONS );
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal Schema constant, not user input.
		$rows = $wpdb->get_col( "SELECT version FROM {$table}" );
		return array_map( 'intval', is_array( $rows ) ? $rows : array() );
	}

	private function record_applied( Migration $migration ): void {
		global $wpdb;
		$wpdb->insert(
			Schema::table_name( Schema::TABLE_MIGRATIONS ),
			array(
				'version'     => $migration->version(),
				'description' => $migration->description(),
				'applied_at'  => current_time( 'mysql', true ),
			),
			array( '%d', '%s', '%s' )
		);
	}
}
