<?php

declare(strict_types=1);

namespace SecurWP\Core\Database;

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

final class MigrationLock {

	private const OPTION_NAME = 'securwp_db_migration_lock';
	private const LEASE_TTL = 300;

	public static function acquire( string $runner ): ?string {
		global $wpdb;

		$token = wp_generate_password( 32, false, false );
		$payload = [
			'token'       => $token,
			'runner'      => sanitize_key( $runner ),
			'acquired_at' => time(),
			'expires_at'  => time() + self::LEASE_TTL,
		];
		$serialized = maybe_serialize( $payload );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic insert against the options table; duplicate-key failure simply means another runner already holds the lease.
		$inserted = $wpdb->insert(
			$wpdb->options,
			[
				'option_name'  => self::OPTION_NAME,
				'option_value' => $serialized,
				'autoload'     => 'no',
			],
			[ '%s', '%s', '%s' ]
		);

		if ( (int) $inserted === 1 ) {
			wp_cache_delete( self::OPTION_NAME, 'options' );
			return $token;
		}

		$current = self::read_payload();
		if ( ! is_array( $current ) ) {
			return null;
		}

		$expires_at = (int) ( $current['expires_at'] ?? 0 );
		if ( $expires_at > time() ) {
			return null;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Compare-and-swap style lease renewal on a single options row.
		$updated = $wpdb->update(
			$wpdb->options,
			[ 'option_value' => $serialized ],
			[
				'option_name'  => self::OPTION_NAME,
				'option_value' => maybe_serialize( $current ),
			],
			[ '%s' ],
			[ '%s', '%s' ]
		);

		if ( (int) $updated === 1 ) {
			wp_cache_delete( self::OPTION_NAME, 'options' );
			return $token;
		}

		return null;
	}

	public static function release( string $token ): void {
		if ( $token === '' ) {
			return;
		}

		$current = self::read_payload();
		if ( ! is_array( $current ) || (string) ( $current['token'] ?? '' ) !== $token ) {
			return;
		}

		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Releases only the lease row that still matches this runner's tokenized payload.
		$wpdb->delete(
			$wpdb->options,
			[
				'option_name'  => self::OPTION_NAME,
				'option_value' => maybe_serialize( $current ),
			],
			[ '%s', '%s' ]
		);
		wp_cache_delete( self::OPTION_NAME, 'options' );
	}

	private static function read_payload(): ?array {
		$value = get_option( self::OPTION_NAME, null );
		return is_array( $value ) ? $value : null;
	}
}
