<?php

namespace Fotogenic_AI;

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

/**
 * Issue and verify short-lived, server-signed Studio Review authority proofs.
 */
final class Workflow_Accepted_Authority {
	public const VERSION = 1;
	public const TTL_SECONDS = 300;

	private \Closure $context_provider;
	private \Closure $signing_key_provider;
	private \Closure $clock;

	public function __construct(
		?callable $context_provider = null,
		?callable $signing_key_provider = null,
		?callable $clock = null
	) {
		$this->context_provider = \Closure::fromCallable(
			$context_provider ?? [ $this, 'get_current_context' ]
		);
		$this->signing_key_provider = \Closure::fromCallable(
			$signing_key_provider ?? static fn(): string => wp_salt( 'auth' )
		);
		$this->clock = \Closure::fromCallable(
			$clock ?? static fn(): int => time()
		);
	}

	/**
	 * Sign one complete, server-derived Review evidence set.
	 *
	 * @return array{version:int,fingerprint:string,expiresAt:int}|\WP_Error
	 */
	public function issue( array $evidence ) {
		$expires_at = ( $this->clock )() + self::TTL_SECONDS;
		$fingerprint = $this->fingerprint( $evidence, $expires_at );
		if ( is_wp_error( $fingerprint ) ) {
			return $fingerprint;
		}

		return [
			'version'     => self::VERSION,
			'fingerprint' => $fingerprint,
			'expiresAt'   => $expires_at,
		];
	}

	/**
	 * Verify that current server evidence is identical to the accepted Review.
	 *
	 * @param mixed $proof Authority proof.
	 * @return array{version:int,fingerprint:string,expiresAt:int}|\WP_Error
	 */
	public function verify( $proof, array $evidence ) {
		$accepted = $this->normalize_proof( $proof, true );
		if ( is_wp_error( $accepted ) ) {
			return $accepted;
		}

		$current = $this->fingerprint( $evidence, $accepted['expiresAt'] );
		if ( is_wp_error( $current ) ) {
			return $current;
		}
		if ( ! hash_equals( $accepted['fingerprint'], $current ) ) {
			return new \WP_Error(
				'fotogenic_ai_review_authority_changed',
				__( 'Pricing, balance, access, or configuration changed after Review. Review the current request before starting.', 'fotogenic-ai' ),
				[ 'status' => 409, 'state' => 'review_changed' ]
			);
		}

		return $accepted;
	}

	/**
	 * Hash one canonical evidence component for durable, non-secret comparison.
	 * @param mixed $evidence Authority evidence.
	 * @return string|\WP_Error
	 */
	public function evidence_fingerprint( $evidence ) {
		$encoded = $this->encode_canonical( $evidence );
		if ( is_wp_error( $encoded ) ) {
			return $encoded;
		}

		return hash( 'sha256', $encoded );
	}

	/**
	 * Hash the current WordPress user, site, upstream account, and API environment.
	 *
	 * The digest is safe to persist with a Bulk run and lets later workers fail
	 * closed when billable authority changes without storing private identifiers.
	 * @return string|\WP_Error
	 */
	public function context_fingerprint() {
		$context = ( $this->context_provider )();
		if ( ! is_array( $context ) ) {
			return $this->signing_error();
		}

		return $this->evidence_fingerprint( $context );
	}

	/**
	 * Normalize the strict browser proof shape without accepting extra fields.
	 *
	 * @param mixed $proof Authority proof.
	 * @return array{version:int,fingerprint:string,expiresAt:int}|\WP_Error
	 */
	public function normalize_proof( $proof, bool $require_current = true ) {
		if ( $proof instanceof \stdClass ) {
			$proof = get_object_vars( $proof );
		}
		if (
			! is_array( $proof )
			|| [ 'version', 'fingerprint', 'expiresAt' ] !== array_keys( $proof )
			|| self::VERSION !== ( $proof['version'] ?? null )
			|| ! is_string( $proof['fingerprint'] ?? null )
			|| 1 !== preg_match( '/^[a-f0-9]{64}$/', $proof['fingerprint'] )
			|| ! is_int( $proof['expiresAt'] ?? null )
		) {
			return $this->invalid_proof();
		}

		$now = ( $this->clock )();
		if ( $require_current && $proof['expiresAt'] <= $now ) {
			return new \WP_Error(
				'fotogenic_ai_review_authority_expired',
				__( 'The authoritative Review expired. Refresh pricing and balance before starting.', 'fotogenic-ai' ),
				[ 'status' => 409, 'state' => 'review_expired' ]
			);
		}
		if ( $proof['expiresAt'] > $now + self::TTL_SECONDS ) {
			return $this->invalid_proof();
		}

		return [
			'version'     => self::VERSION,
			'fingerprint' => $proof['fingerprint'],
			'expiresAt'   => $proof['expiresAt'],
		];
	}

	/** @return array{userId:int,blogId:int,accountIdentity:string,environment:string} */
	private function get_current_context(): array {
		$options = Settings::instance()->get_options();

		return [
			'userId'          => get_current_user_id(),
			'blogId'          => get_current_blog_id(),
			'accountIdentity' => Auth_Session::instance()->get_cache_identity(),
			'environment'     => sanitize_key( (string) ( $options['api_environment'] ?? 'production' ) ),
		];
	}

	/** @return string|\WP_Error */
	private function fingerprint( array $evidence, int $expires_at ) {
		$context = ( $this->context_provider )();
		$key     = ( $this->signing_key_provider )();
		if ( ! is_array( $context ) || ! is_string( $key ) || strlen( $key ) < 32 ) {
			return $this->signing_error();
		}

		$encoded = $this->encode_canonical(
			[
				'version'   => self::VERSION,
				'expiresAt' => $expires_at,
				'context'   => $context,
				'evidence'  => $evidence,
			]
		);
		if ( is_wp_error( $encoded ) ) {
			return $encoded;
		}

		return hash_hmac( 'sha256', $encoded, $key );
	}

	/**
	 * @param mixed $value Candidate value.
	 * @return string|\WP_Error
	 */
	private function encode_canonical( $value ) {
		$valid     = true;
		$canonical = $this->canonicalize( $value, $valid );
		if ( ! $valid ) {
			return $this->signing_error();
		}
		$encoded = wp_json_encode(
			$canonical,
			JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION
		);
		if ( ! is_string( $encoded ) || '' === $encoded ) {
			return $this->signing_error();
		}

		return $encoded;
	}

	/**
	 * @param mixed $value Candidate value.
	 * @return mixed
	 */
	private function canonicalize( $value, bool &$valid ) {
		if ( $value instanceof \stdClass ) {
			$value = get_object_vars( $value );
		}
		if ( is_array( $value ) ) {
			if ( ! fotogenic_ai_array_is_list( $value ) ) {
				ksort( $value, SORT_STRING );
			}
			foreach ( $value as $key => $item ) {
				$value[ $key ] = $this->canonicalize( $item, $valid );
			}
			return $value;
		}
		if ( is_float( $value ) ) {
			if ( ! is_finite( $value ) ) {
				$valid = false;
				return null;
			}
			return $value;
		}
		if ( is_string( $value ) || is_int( $value ) || is_bool( $value ) || null === $value ) {
			return $value;
		}

		$valid = false;
		return null;
	}

	private function invalid_proof(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_invalid_review_authority',
			__( 'The Studio start is missing a valid authoritative Review proof.', 'fotogenic-ai' ),
			[ 'status' => 400 ]
		);
	}

	private function signing_error(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_review_authority_unavailable',
			__( 'The authoritative Review could not be secured. Please refresh and try again.', 'fotogenic-ai' ),
			[ 'status' => 503 ]
		);
	}
}
