<?php

declare(strict_types=1);

namespace Australcode\Rewards\Infrastructure\Database\Migrations;

use Australcode\Rewards\Infrastructure\Database\Schema;

defined( 'ABSPATH' ) || exit;

/**
 * Rebrand data migration: renames legacy `pointly_*` persisted data to the
 * `acrwd_*` prefix for sites upgrading from the pre-Australcode (Pointly) build.
 *
 * On a fresh install (no legacy `pointly_*` data) every step is a guarded
 * no-op, so this ships safely as future-proof insurance. The migrations control
 * table itself is renamed earlier, by the Migrator bootstrap, so this migration
 * is even seen as "pending" on a legacy install.
 *
 * The only intentional `pointly` references left in the codebase live here (the
 * legacy source names) and in the Migrator bootstrap — by design, since a rename
 * migration must name what it migrates from.
 */
final class M0005RenameLegacyData implements Migration {

	/** Legacy prefix from the Pointly-branded builds (pre-Australcode rebrand). */
	private const LEGACY_PREFIX = 'pointly_';

	/** Current prefix; matches Schema's table/option naming. */
	private const NEW_PREFIX = 'acrwd_';

	public function version(): int {
		return 5;
	}

	public function description(): string {
		return 'Rename legacy pointly_* data (tables, options, meta, crons) to acrwd_*';
	}

	public function up(): void {
		$this->rename_tables();
		$this->rename_options();
		$this->rename_meta();
		$this->clear_legacy_transients();
		$this->clear_legacy_crons();
	}

	/**
	 * RENAME the five data tables. Guarded (old exists && new absent) so it is
	 * idempotent and a no-op on fresh installs. The migrations control table is
	 * handled separately by the Migrator bootstrap.
	 */
	private function rename_tables(): void {
		global $wpdb;

		foreach ( Schema::all_tables() as $new_logical ) {
			if ( Schema::TABLE_MIGRATIONS === $new_logical ) {
				continue;
			}
			$suffix = substr( $new_logical, strlen( self::NEW_PREFIX ) );
			$old    = $wpdb->prefix . self::LEGACY_PREFIX . $suffix;
			$new    = $wpdb->prefix . $new_logical;

			if ( $this->table_exists( $old ) && ! $this->table_exists( $new ) ) {
				// 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; one-off rename migration.
				$wpdb->query( "RENAME TABLE `{$old}` TO `{$new}`" );
			}
		}
	}

	private function table_exists( string $table ): bool {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- existence probe for a one-off rename migration.
		return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
	}

	/**
	 * Rename pointly_* options to acrwd_* (value + autoload preserved). UPDATE
	 * IGNORE skips rows whose target already exists (option_name is unique); any
	 * remaining legacy rows are then deleted so the new value wins on collision.
	 */
	private function rename_options(): void {
		global $wpdb;

		$like = $wpdb->esc_like( self::LEGACY_PREFIX ) . '%';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off rename migration on internal option names.
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE IGNORE {$wpdb->options} SET option_name = CONCAT( %s, SUBSTRING( option_name, %d ) ) WHERE option_name LIKE %s",
				self::NEW_PREFIX,
				strlen( self::LEGACY_PREFIX ) + 1,
				$like
			)
		);
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off rename migration on internal option names.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like ) );
	}

	/**
	 * Rename _pointly_* meta keys to _acrwd_* across user, post and term meta.
	 */
	private function rename_meta(): void {
		global $wpdb;

		$legacy = '_' . self::LEGACY_PREFIX; // _pointly_
		$new    = '_' . self::NEW_PREFIX;    // _acrwd_
		$like   = $wpdb->esc_like( $legacy ) . '%';

		foreach ( array( $wpdb->usermeta, $wpdb->postmeta, $wpdb->termmeta ) as $table ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- core meta tables; one-off rename migration.
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE IGNORE {$table} SET meta_key = CONCAT( %s, SUBSTRING( meta_key, %d ) ) WHERE meta_key LIKE %s",
					$new,
					strlen( $legacy ) + 1,
					$like
				)
			);
		}
	}

	/**
	 * Delete legacy transients (value + timeout rows); they regenerate on demand.
	 */
	private function clear_legacy_transients(): void {
		global $wpdb;

		$value   = $wpdb->esc_like( '_transient_' . self::LEGACY_PREFIX ) . '%';
		$timeout = $wpdb->esc_like( '_transient_timeout_' . self::LEGACY_PREFIX ) . '%';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-off cleanup of legacy transients.
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
				$value,
				$timeout
			)
		);
	}

	/**
	 * Unschedule legacy recurring crons; the new acrwd_* hooks are re-registered
	 * on the next boot.
	 */
	private function clear_legacy_crons(): void {
		foreach ( array( 'pointly_license_revalidate', 'pointly_expire_points', 'pointly_birthday_tick' ) as $hook ) {
			wp_clear_scheduled_hook( $hook );
		}
	}
}
