<?php

declare(strict_types=1);

namespace SiteFort\Core\Database;

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

final class MigrationLock {

	private const OPTION_NAME = 'sitefort_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 );

		$previous_error_suppression = $wpdb->suppress_errors( true );
		try {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic insert against the unique option_name key; duplicate-key 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' ]
			);
		} finally {
			$wpdb->suppress_errors( $previous_error_suppression );
		}

		if ( (int) $inserted === 1 ) {
			self::clear_option_cache();
			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 ) {
			self::clear_option_cache();
			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' ]
		);
		self::clear_option_cache();
	}

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

	private static function clear_option_cache(): void {
		wp_cache_delete( self::OPTION_NAME, 'options' );
		wp_cache_delete( 'notoptions', 'options' );
	}
}
