<?php

namespace Fotogenic_AI;

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

final class Experience_State_Store {
	public const SCHEMA_VERSION = 1;

	private const META_KEY_PREFIX         = '_fotogenic_ai_experience_state_';
	private const LOCK_KEY_PREFIX         = 'fotogenic_ai_experience_lock_';
	private const LOCK_STALE_SECONDS      = 180;
	private const LOCK_WAIT_ATTEMPTS      = 5;
	private const LOCK_WAIT_MICROSECONDS  = 50000;
	private const MAX_ACCOUNT_SCOPES      = 8;
	private const MAX_CLAIMS              = 200;

	public function get( int $user_id ): array {
		if ( $user_id <= 0 ) {
			return $this->default_document();
		}

		$stored = get_user_meta( $user_id, $this->get_meta_key(), true );

		return $this->normalize_document( is_array( $stored ) ? $stored : [] );
	}

	public function save( int $user_id, array $document, string $current_scope = '' ): bool {
		if ( $user_id <= 0 ) {
			return false;
		}

		$document              = $this->compact( $document, $current_scope );
		$document['updatedAt'] = time();
		$meta_key              = $this->get_meta_key();
		$existing              = get_user_meta( $user_id, $meta_key, true );
		$result                = update_user_meta( $user_id, $meta_key, $document );

		if ( false !== $result ) {
			return true;
		}

		return is_array( $existing ) && $this->normalize_document( $existing ) === $document;
	}

	public function acquire_claim_lock( int $user_id ): ?string {
		if ( $user_id <= 0 ) {
			return null;
		}

		$option_key = $this->get_lock_key( $user_id );

		for ( $attempt = 0; $attempt < self::LOCK_WAIT_ATTEMPTS; $attempt++ ) {
			$owner = Background_Lock::instance()->acquire_option( $option_key, self::LOCK_STALE_SECONDS );
			if ( false !== $owner ) {
				return $owner;
			}

			usleep( self::LOCK_WAIT_MICROSECONDS );
		}

		return null;
	}

	public function release_claim_lock( int $user_id, string $owner ): void {
		if ( $user_id <= 0 || '' === $owner ) {
			return;
		}

		Background_Lock::instance()->release_option( $this->get_lock_key( $user_id ), $owner );
	}

	public function fingerprint_account( string $identity ): string {
		$identity = trim( $identity );
		if ( '' === $identity || 'guest' === $identity ) {
			return 'guest';
		}

		return 'account_' . hash_hmac(
			'sha256',
			get_current_blog_id() . '|account|' . $identity,
			wp_salt( 'auth' )
		);
	}

	public function fingerprint_identifier( string $identifier ): string {
		$identifier = trim( $identifier );
		if ( '' === $identifier ) {
			return '';
		}

		return 'id_' . hash_hmac(
			'sha256',
			get_current_blog_id() . '|experience|' . $identifier,
			wp_salt( 'auth' )
		);
	}

	public function get_scope( array $document, string $scope_key ): array {
		$document = $this->normalize_document( $document );
		if ( ! $this->is_valid_scope_key( $scope_key ) ) {
			return $this->default_scope();
		}

		$scope = $document['scopes'][ $scope_key ] ?? [];

		return $this->normalize_scope( is_array( $scope ) ? $scope : [] );
	}

	public function set_scope( array $document, string $scope_key, array $scope ): array {
		$document = $this->normalize_document( $document );
		if ( ! $this->is_valid_scope_key( $scope_key ) ) {
			return $document;
		}

		$scope              = $this->normalize_scope( $scope );
		$scope['updatedAt'] = time();
		$document['scopes'][ $scope_key ] = $scope;

		return $document;
	}

	public function get_claim( array $document, string $claim_id ): ?array {
		$document = $this->normalize_document( $document );
		if ( ! $this->is_valid_claim_id( $claim_id ) || ! isset( $document['claims'][ $claim_id ] ) ) {
			return null;
		}

		return $document['claims'][ $claim_id ];
	}

	public function set_claim( array $document, string $claim_id, array $claim ): array {
		$document = $this->normalize_document( $document );
		if ( ! $this->is_valid_claim_id( $claim_id ) ) {
			return $document;
		}

		$normalized = $this->normalize_claim( $claim );
		if ( empty( $normalized ) ) {
			return $document;
		}

		$document['claims'][ $claim_id ] = $normalized;

		return $document;
	}

	public function compact( array $document, string $current_scope = '' ): array {
		$document = $this->normalize_document( $document );

		if ( count( $document['scopes'] ) > self::MAX_ACCOUNT_SCOPES ) {
			uasort(
				$document['scopes'],
				static fn( array $left, array $right ): int => (int) ( $left['updatedAt'] ?? 0 ) <=> (int) ( $right['updatedAt'] ?? 0 )
			);
			foreach ( array_keys( $document['scopes'] ) as $scope_key ) {
				if ( count( $document['scopes'] ) <= self::MAX_ACCOUNT_SCOPES ) {
					break;
				}
				if ( 'guest' === $scope_key || $scope_key === $current_scope ) {
					continue;
				}
				unset( $document['scopes'][ $scope_key ] );
			}
		}

		foreach ( $document['claims'] as $claim_id => $claim ) {
			if ( ! isset( $document['scopes'][ $claim['scope'] ] ) ) {
				unset( $document['claims'][ $claim_id ] );
			}
		}

		if ( count( $document['claims'] ) > self::MAX_CLAIMS ) {
			uasort(
				$document['claims'],
				static fn( array $left, array $right ): int => (int) ( $left['shownAt'] ?? 0 ) <=> (int) ( $right['shownAt'] ?? 0 )
			);
			$remove_count = count( $document['claims'] ) - self::MAX_CLAIMS;
			foreach ( array_keys( $document['claims'] ) as $claim_id ) {
				if ( $remove_count <= 0 ) {
					break;
				}
				unset( $document['claims'][ $claim_id ] );
				--$remove_count;
			}
		}

		return $document;
	}

	private function get_meta_key(): string {
		return self::META_KEY_PREFIX . max( 1, get_current_blog_id() );
	}

	private function get_lock_key( int $user_id ): string {
		return sprintf(
			'%s%d_%d',
			self::LOCK_KEY_PREFIX,
			max( 1, get_current_blog_id() ),
			$user_id
		);
	}

	private function default_document(): array {
		return [
			'schemaVersion' => self::SCHEMA_VERSION,
			'blogId'        => max( 1, get_current_blog_id() ),
			'updatedAt'     => 0,
			'scopes'        => [],
			'claims'        => [],
		];
	}

	private function default_scope(): array {
		return [
			'updatedAt'      => 0,
			'welcomeOffers'  => [],
			'grantNotices'   => [],
			'creditEpisode'  => $this->default_credit_episode(),
		];
	}

	private function default_credit_episode(): array {
		return [
			'id'               => '',
			'status'           => 'normal',
			'lowShown'         => false,
			'zeroShown'        => false,
			'lowClaimId'       => '',
			'zeroClaimId'      => '',
			'lowOutcome'       => '',
			'zeroOutcome'      => '',
			'lastBalance'      => null,
			'threshold'        => null,
			'rearmThreshold'   => null,
			'startedAt'        => 0,
			'recoveredAt'      => 0,
			'updatedAt'        => 0,
		];
	}

	private function normalize_document( array $document ): array {
		$normalized = $this->default_document();
		if ( self::SCHEMA_VERSION !== (int) ( $document['schemaVersion'] ?? self::SCHEMA_VERSION ) ) {
			return $normalized;
		}

		$normalized['updatedAt'] = max( 0, (int) ( $document['updatedAt'] ?? 0 ) );
		$scopes                  = isset( $document['scopes'] ) && is_array( $document['scopes'] ) ? $document['scopes'] : [];
		foreach ( $scopes as $scope_key => $scope ) {
			$scope_key = (string) $scope_key;
			if ( ! $this->is_valid_scope_key( $scope_key ) || ! is_array( $scope ) ) {
				continue;
			}
			$normalized['scopes'][ $scope_key ] = $this->normalize_scope( $scope );
		}

		$claims = isset( $document['claims'] ) && is_array( $document['claims'] ) ? $document['claims'] : [];
		foreach ( $claims as $claim_id => $claim ) {
			$claim_id = (string) $claim_id;
			if ( ! $this->is_valid_claim_id( $claim_id ) || ! is_array( $claim ) ) {
				continue;
			}
			$claim = $this->normalize_claim( $claim );
			if ( ! empty( $claim ) ) {
				$normalized['claims'][ $claim_id ] = $claim;
			}
		}

		return $normalized;
	}

	private function normalize_scope( array $scope ): array {
		$normalized                    = $this->default_scope();
		$normalized['updatedAt']       = max( 0, (int) ( $scope['updatedAt'] ?? 0 ) );
		$normalized['welcomeOffers']   = $this->normalize_notice_map( $scope['welcomeOffers'] ?? [] );
		$normalized['grantNotices']    = $this->normalize_notice_map( $scope['grantNotices'] ?? [] );
		$normalized['creditEpisode']   = $this->normalize_credit_episode(
			isset( $scope['creditEpisode'] ) && is_array( $scope['creditEpisode'] ) ? $scope['creditEpisode'] : []
		);

		return $normalized;
	}

	private function normalize_notice_map( $notices ): array {
		if ( ! is_array( $notices ) ) {
			return [];
		}

		$normalized = [];
		foreach ( $notices as $key => $notice ) {
			$key = sanitize_key( (string) $key );
			if ( '' === $key || ! is_array( $notice ) ) {
				continue;
			}
			$normalized[ $key ] = [
				'contentVersion' => sanitize_key( (string) ( $notice['contentVersion'] ?? '' ) ),
				'credits'        => $this->normalize_number( $notice['credits'] ?? null ),
				'shownAt'        => max( 0, (int) ( $notice['shownAt'] ?? 0 ) ),
				'claimId'        => $this->sanitize_claim_id( (string) ( $notice['claimId'] ?? '' ) ),
				'outcome'        => sanitize_key( (string) ( $notice['outcome'] ?? '' ) ),
				'dismissedAt'    => max( 0, (int) ( $notice['dismissedAt'] ?? 0 ) ),
			];
		}

		return $normalized;
	}

	private function normalize_credit_episode( array $episode ): array {
		$normalized              = $this->default_credit_episode();
		$normalized['id']        = $this->sanitize_claim_id( (string) ( $episode['id'] ?? '' ) );
		$status                  = sanitize_key( (string) ( $episode['status'] ?? 'normal' ) );
		$normalized['status']    = in_array( $status, [ 'normal', 'low', 'zero', 'recovering' ], true ) ? $status : 'normal';
		$normalized['lowShown']  = ! empty( $episode['lowShown'] );
		$normalized['zeroShown'] = ! empty( $episode['zeroShown'] );

		foreach ( [ 'lowClaimId', 'zeroClaimId' ] as $key ) {
			$normalized[ $key ] = $this->sanitize_claim_id( (string) ( $episode[ $key ] ?? '' ) );
		}
		foreach ( [ 'lowOutcome', 'zeroOutcome' ] as $key ) {
			$normalized[ $key ] = sanitize_key( (string) ( $episode[ $key ] ?? '' ) );
		}
		foreach ( [ 'lastBalance', 'threshold', 'rearmThreshold' ] as $key ) {
			$normalized[ $key ] = $this->normalize_number( $episode[ $key ] ?? null );
		}
		foreach ( [ 'startedAt', 'recoveredAt', 'updatedAt' ] as $key ) {
			$normalized[ $key ] = max( 0, (int) ( $episode[ $key ] ?? 0 ) );
		}

		return $normalized;
	}

	private function normalize_claim( array $claim ): array {
		$scope = (string) ( $claim['scope'] ?? '' );
		$type  = sanitize_key( (string) ( $claim['type'] ?? '' ) );
		if ( ! $this->is_valid_scope_key( $scope ) || ! in_array( $type, [ 'welcome', 'zero', 'low' ], true ) ) {
			return [];
		}

		return [
			'scope'       => $scope,
			'type'        => $type,
			'variant'     => sanitize_key( (string) ( $claim['variant'] ?? '' ) ),
			'stateKey'    => sanitize_key( (string) ( $claim['stateKey'] ?? '' ) ),
			'shownAt'     => max( 0, (int) ( $claim['shownAt'] ?? 0 ) ),
			'outcome'     => sanitize_key( (string) ( $claim['outcome'] ?? '' ) ),
			'dismissedAt' => max( 0, (int) ( $claim['dismissedAt'] ?? 0 ) ),
		];
	}

	private function normalize_number( $value ): ?float {
		if ( ! is_numeric( $value ) ) {
			return null;
		}

		$value = (float) $value;

		return is_finite( $value ) && $value >= 0 ? $value : null;
	}

	private function is_valid_scope_key( string $scope_key ): bool {
		return 'guest' === $scope_key || 1 === preg_match( '/^account_[a-f0-9]{64}$/', $scope_key );
	}

	private function is_valid_claim_id( string $claim_id ): bool {
		return '' !== $this->sanitize_claim_id( $claim_id );
	}

	private function sanitize_claim_id( string $claim_id ): string {
		$claim_id = trim( $claim_id );
		if ( 1 !== preg_match( '/^[A-Za-z0-9-]{16,64}$/', $claim_id ) ) {
			return '';
		}

		return $claim_id;
	}
}
