<?php
/**
 * Runtime locks and loopback request signatures.
 *
 * @package Niwa_AI
 */

defined( 'ABSPATH' ) || exit;

class Niwa_AI_Admin_Autonomy_Runtime_Locks {
	private $telegram;
	private $owners = array();

	public function __construct( $telegram = null ) {
		$this->telegram = $telegram;
	}

	public function build_loopback_signature_body( $prefix, $secret ) {
		$ts    = time();
		$nonce = wp_generate_password( 32, false, false );
		set_transient( 'niwa_ai_loopback_nonce_' . md5( $prefix . '|' . $nonce ), 1, 120 );
		$sig = hash_hmac( 'sha256', $prefix . '|' . $ts . '|' . $nonce, (string) $secret );

		return array(
			'niwa_ts'  => $ts,
			'niwa_sig' => $sig,
			'niwa_nonce' => $nonce,
		);
	}

	public function verify_loopback_signature( $prefix, $secret_override = '' ) {
		$ts  = isset( $_POST['niwa_ts'] ) ? intval( $_POST['niwa_ts'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification
		$sig = isset( $_POST['niwa_sig'] ) ? sanitize_text_field( wp_unslash( $_POST['niwa_sig'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
		$nonce = isset( $_POST['niwa_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['niwa_nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification

		if ( 0 === $ts || '' === $sig || '' === $nonce ) {
			return false;
		}

		if ( abs( time() - $ts ) > 120 ) {
			return false;
		}

		if ( '' !== $secret_override ) {
			$secret = $secret_override;
		} else {
			$state  = $this->telegram && method_exists( $this->telegram, 'get_runner_state' ) ? (array) $this->telegram->get_runner_state() : array();
			$secret = isset( $state['secret'] ) ? (string) $state['secret'] : '';
		}

		if ( '' === $secret ) {
			return false;
		}

		$expected = hash_hmac( 'sha256', $prefix . '|' . $ts . '|' . $nonce, $secret );
		if ( ! hash_equals( $expected, $sig ) ) {
			return false;
		}

		$nonce_key = 'niwa_ai_loopback_nonce_' . md5( $prefix . '|' . $nonce );
		if ( false === get_transient( $nonce_key ) ) {
			return false;
		}
		delete_transient( $nonce_key );
		return true;
	}

	public function acquire( $name, $ttl ) {
		global $wpdb;

		$option_name = $this->option_name( $name );
		$now         = time();
		$expires     = $now + max( 5, intval( $ttl ) );
		$token       = wp_generate_password( 32, false, false );
		$value       = $token . '|' . $expires;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$inserted = $wpdb->query(
			$wpdb->prepare(
				"INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')",
				$option_name,
				$value
			)
		);

		if ( $inserted ) {
			$this->owners[ sanitize_key( $name ) ] = $value;
			wp_cache_delete( $option_name, 'options' );
			return true;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$claimed = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND CAST(SUBSTRING_INDEX(option_value, '|', -1) AS UNSIGNED) < %d",
				$value,
				$option_name,
				$now
			)
		);

		if ( $claimed ) {
			$this->owners[ sanitize_key( $name ) ] = $value;
			wp_cache_delete( $option_name, 'options' );
		}

		return $claimed > 0;
	}

	public function release( $name ) {
		global $wpdb;

		$key = sanitize_key( $name );
		if ( empty( $this->owners[ $key ] ) ) {
			return false;
		}

		$option_name = $this->option_name( $name );
		$value       = (string) $this->owners[ $key ];
		unset( $this->owners[ $key ] );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$deleted = $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s",
				$option_name,
				$value
			)
		);
		wp_cache_delete( $option_name, 'options' );
		return $deleted > 0;
	}

	public function refresh( $name, $ttl ) {
		global $wpdb;

		$key = sanitize_key( $name );
		if ( empty( $this->owners[ $key ] ) ) {
			return false;
		}

		$current = (string) $this->owners[ $key ];
		$parts   = explode( '|', $current );
		if ( intval( end( $parts ) ) <= time() ) {
			return false;
		}

		$token       = (string) reset( $parts );
		$next        = $token . '|' . ( time() + max( 5, intval( $ttl ) ) );
		$option_name = $this->option_name( $name );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$updated = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value = %s",
				$next,
				$option_name,
				$current
			)
		);
		if ( $updated ) {
			$this->owners[ $key ] = $next;
			wp_cache_delete( $option_name, 'options' );
		}

		return $updated > 0;
	}

	public function is_locked( $name ) {
		$option_name = $this->option_name( $name );
		wp_cache_delete( $option_name, 'options' );
		$value = get_option( $option_name, '' );
		$parts = is_string( $value ) ? explode( '|', $value ) : array();

		return intval( end( $parts ) ) > time();
	}

	public function force_release( $name ) {
		unset( $this->owners[ sanitize_key( $name ) ] );
		return delete_option( $this->option_name( $name ) );
	}

	private function option_name( $name ) {
		return 'niwa_ai_admin_autonomy_lock_' . sanitize_key( $name );
	}
}
