<?php

declare(strict_types=1);

namespace SiteFort\Modules\Tools;

// If this file is called directly, abort.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Move the legacy admin user from ID 1 to a new ID.
 */
class AdminIdChanger {

	private const LOCK_NAME = 'admin_id_change';

	/**
	 * Generate an unused user ID.
	 *
	 * @return int A user ID that is not in use.
	 */
	public function generate_safe_id(): int {
		global $wpdb;
		$users_table = ToolDatabase::table_identifier( $wpdb->users );

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Validated table identifier.
		$max_id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM {$users_table}" );
		$new_id = max( 2, $max_id + 1 );

		// Safety loop: ensure the generated ID is truly unused.
		while ( get_user_by( 'id', $new_id ) ) {
			$new_id++;
		}

		return $new_id;
	}

	/**
	 * Change the user with ID 1 to a new ID.
	 *
	 * @param int $new_id The new user ID (must be >= 2).
	 * @return true|\WP_Error True on success, WP_Error on failure.
	 */
	public function change( int $new_id ) {
		global $wpdb;

		if ( $new_id < 2 ) {
			return new \WP_Error(
				'invalid_id',
				'New ID must be at least 2.'
			);
		}

		// Check if user ID 1 exists.
		$user = get_user_by( 'id', 1 );
		if ( ! $user ) {
			return new \WP_Error(
				'no_user',
				'No user with ID 1 exists.'
			);
		}

		// Check if new ID is already taken.
		if ( get_user_by( 'id', $new_id ) ) {
			return new \WP_Error(
				'id_taken',
				'User ID ' . $new_id . ' is already in use.'
			);
		}

		$transactional_check = ToolDatabase::assert_transactional_tables(
			[
				$wpdb->users,
				$wpdb->usermeta,
				$wpdb->posts,
				$wpdb->comments,
				$wpdb->links,
			],
			'user ID change'
		);
		if ( is_wp_error( $transactional_check ) ) {
			return $transactional_check;
		}

		$lock = ToolLock::acquire( self::LOCK_NAME );
		if ( is_wp_error( $lock ) ) {
			return $lock;
		}

		if ( ! $lock ) {
			return new \WP_Error(
				'locked',
				'This tool is already running. Please try again in a few minutes.'
			);
		}

		$transaction_started = false;

		try {
			// Run all mutations in one transaction.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Transaction control.
			if ( false === $wpdb->query( 'START TRANSACTION' ) ) {
				return new \WP_Error(
					'transaction_start_failed',
					'Unable to start a database transaction for the user ID change.'
				);
			}
			$transaction_started = true;

			$ok = true;

			// Update users table.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Core table update.
			$user_update_result = $wpdb->update( $wpdb->users, [ 'ID' => $new_id ], [ 'ID' => 1 ] );
			if ( false === $user_update_result ) {
				$ok = false;
			} elseif ( 1 !== (int) $user_update_result ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Transaction control.
				$wpdb->query( 'ROLLBACK' );
				$transaction_started = false;
				return new \WP_Error(
					'concurrent_change',
					'The user with ID 1 changed while this tool was running. Refresh the page and try again.'
				);
			}

			// Update usermeta.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Core table update.
			$ok = $ok && ( false !== $wpdb->update( $wpdb->usermeta, [ 'user_id' => $new_id ], [ 'user_id' => 1 ] ) );

			// Update posts (post_author).
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Core table update.
			$ok = $ok && ( false !== $wpdb->update( $wpdb->posts, [ 'post_author' => $new_id ], [ 'post_author' => 1 ] ) );

			// Update comments.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Core table update.
			$ok = $ok && ( false !== $wpdb->update( $wpdb->comments, [ 'user_id' => $new_id ], [ 'user_id' => 1 ] ) );

			// Update links.
			$links_table = ToolDatabase::table_identifier( $wpdb->links );
			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
			$link_update_sql = $wpdb->prepare(
				"UPDATE {$links_table} SET link_owner = %d WHERE link_owner = 1",
				$new_id
			);
			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared core table update.
			$ok = $ok && ( false !== $wpdb->query( $link_update_sql ) );

			if ( ! $ok ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Transaction control.
				$wpdb->query( 'ROLLBACK' );
				$transaction_started = false;
				return new \WP_Error(
					'db_error',
					'A database error occurred during migration. All changes have been rolled back.'
				);
			}

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Transaction control.
			if ( false === $wpdb->query( 'COMMIT' ) ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Transaction control.
				$wpdb->query( 'ROLLBACK' );
				$transaction_started = false;
				return new \WP_Error(
					'transaction_commit_failed',
					'Unable to commit the user ID change. The transaction could not be confirmed.'
				);
			}
			$transaction_started = false;

			// Destroy all sessions for the migrated user. After COMMIT, usermeta
			// (including session_tokens) is now keyed by $new_id, not 1.
			$session_tokens = \WP_Session_Tokens::get_instance( $new_id );
			$session_tokens->destroy_all();

			// Flush WordPress user object cache for both old and new IDs.
			clean_user_cache( 1 );
			clean_user_cache( $new_id );

			/**
			 * Fires after the admin user ID has been changed from 1.
			 *
			 * @param int      $new_id The new user ID.
			 * @param \WP_User $user   The original user object (with ID 1).
			 */
			do_action( 'sitefort_admin_user_id_changed', $new_id, $user );

			return true;
		} catch ( \Throwable $e ) {
			if ( $transaction_started ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Transaction control.
				$wpdb->query( 'ROLLBACK' );
			}

			throw $e;
		} finally {
			$lock->release();
		}
	}

}
