<?php

namespace Fotogenic_AI;

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

final class Auth_Session {
    public const OPTION_NAME = 'fotogenic_ai_auth_session';
    public const CRON_HOOK = 'fotogenic_ai_auth_refresh';
    public const SYNC_EVENT_KEY = 'fotogenic_ai_auth_sync';
    public const SYNC_CHANNEL = 'fotogenic_ai_auth';
    private const NOTICE_PREFIX = 'fotogenic_ai_auth_notice_';
    private const REFRESH_THRESHOLD_MS = 300000;
    private const LOW_CREDITS_THRESHOLD = 3;
    private const DEFAULT_PRICING_URL = 'https://fotogenic.ai/pricing?entry_context=wp';
    private const DEFAULT_ACCOUNT_URL = 'https://account.fotogenic.ai/?entry_context=wp';
    private const DEFAULT_MANAGE_URL = 'https://account.fotogenic.ai/?entry_context=wp&destination=pricing';

    private static ?Auth_Session $instance = null;

    private Auth_Client $client;
    private Auth_State_Store $state_store;
    private Auth_Refresh_Lock $refresh_lock;

    private function __construct() {
        $this->client       = new Auth_Client();
        $this->state_store  = new Auth_State_Store();
        $this->refresh_lock = new Auth_Refresh_Lock();
    }

    public static function instance(): Auth_Session {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function init(): void {
        add_action( 'init', [ $this, 'initialize_cron_schedule' ] );
        add_action( 'admin_post_fotogenic_auth_cb', [ $this, 'handle_callback' ] );
        add_action( self::CRON_HOOK, [ $this, 'handle_scheduled_refresh' ] );
    }

    public function initialize_cron_schedule(): void {
        if ( $this->is_connected() ) {
            $this->ensure_refresh_schedule();
        } else {
            $this->clear_refresh_schedule();
        }
    }

    public function is_connected(): bool {
        return $this->is_connected_session( $this->get_session() );
    }

    public function get_pricing_url(): string {
        return (string) apply_filters( 'fotogenic_ai_pricing_url', self::DEFAULT_PRICING_URL );
    }

    public function get_account_url(): string {
        return (string) apply_filters( 'fotogenic_ai_account_url', self::DEFAULT_ACCOUNT_URL );
    }

    public function get_manage_url(): string {
        return (string) apply_filters( 'fotogenic_ai_manage_url', self::DEFAULT_MANAGE_URL );
    }

    public function get_cache_identity( ?array $session = null ): string {
        $session = is_array( $session ) ? $session : $this->get_session();
        if ( ! $this->is_connected_session( $session ) ) {
            return 'guest';
        }

        $uid = trim( (string) ( $session['userUid'] ?? '' ) );
        return '' !== $uid ? 'account_' . hash( 'sha256', $uid ) : 'guest';
    }

    public function get_public_status( ?Platform_Capabilities $capabilities = null ): array {
		$capabilities = $capabilities ?? Platform_Capabilities::instance();
		$stored = get_option( self::OPTION_NAME, [] );
		$stored_payload = is_array( $stored ) ? trim( (string) ( $stored['refreshToken'] ?? '' ) ) : '';
		$crypto = $capabilities->crypto_status( $stored_payload );
        $session   = $this->get_session( $capabilities );
        $connected = $crypto['readable'] && $this->is_connected_session( $session );
        $status    = [
            'connected'  => $connected,
            'user'       => [
                'uid'         => '',
                'email'       => '',
                'displayName' => '',
                'photoURL'    => '',
            ],
            'hasPlan'    => false,
            'balance'    => null,
            'lowCredits' => false,
            'pricingUrl' => $this->get_pricing_url(),
            'accountUrl' => $this->get_account_url(),
			'crypto'     => $crypto,
        ];

        if ( ! $connected ) {
            return $status;
        }

        $status['user'] = [
            'uid'         => (string) ( $session['userUid'] ?? '' ),
            'email'       => (string) ( $session['userEmail'] ?? '' ),
            'displayName' => (string) ( $session['displayName'] ?? '' ),
            'photoURL'    => (string) ( $session['photoURL'] ?? '' ),
        ];

        $credits = Credits_Service::instance()->get_status( 0 );
        if ( is_array( $credits ) ) {
            $balance = array_key_exists( 'balance', $credits ) ? (float) $credits['balance'] : null;
            $status['balance'] = $balance;
            $status['hasPlan'] = ! empty( $credits['hasPlan'] );
            $status['lowCredits'] = null !== $balance && $balance < self::LOW_CREDITS_THRESHOLD;
        }

        return $status;
    }

    /** @return array|\WP_Error */
    public function start_login( string $source, ?Platform_Capabilities $capabilities = null ) {
        if ( ! is_user_logged_in() || ! fotogenic_ai_user_can_access() ) {
            $this->log_debug(
                'warn',
                'auth_login_forbidden',
                [
                    'source'  => $source,
                    'user_id' => get_current_user_id(),
                ]
            );
            return new \WP_Error(
                'fotogenic_ai_forbidden',
                __( 'You are not allowed to connect this site.', 'fotogenic-ai' ),
                [ 'status' => 403 ]
            );
        }

		$crypto_error = $this->writable_crypto_error( $capabilities );
		if ( null !== $crypto_error ) {
			return $crypto_error;
		}

        $source       = 'studio' === $source ? 'studio' : 'settings';
        $user_id      = get_current_user_id();
        try {
            $state = bin2hex( random_bytes( 16 ) );
        } catch ( \Throwable $throwable ) {
            $this->log_debug(
                'error',
                'auth_login_state_generation_failed',
                [
                    'source'  => $source,
                    'user_id' => $user_id,
                ]
            );
            return new \WP_Error(
                'fotogenic_ai_auth_state_error',
                __( 'Login could not be started securely. Please try again.', 'fotogenic-ai' ),
                [ 'status' => 500 ]
            );
        }
        $return_url   = $this->get_return_url_for_source( $source );
        $redirect_uri = $this->get_redirect_uri();
        $auth_source  = $this->get_oauth_source();

        $this->state_store->store(
            $user_id,
            $state,
            [
                'source'      => $source,
                'return_url'  => $return_url,
                'auth_source' => $auth_source,
                'created_at'  => time(),
            ]
        );

        $response = $this->client->start_login( $redirect_uri, $state, $auth_source );
        if ( is_wp_error( $response ) ) {
            $this->state_store->delete( $user_id, $state );
            $this->log_debug(
                'warn',
                'auth_login_start_failed',
                [
                    'source'     => $source,
                    'user_id'    => $user_id,
                    'status'     => (int) ( $response->get_error_data()['status'] ?? 0 ),
                    'error_code' => $response->get_error_code(),
                ]
            );
            return $response;
        }

        $authorization_url = $this->get_trusted_external_url( (string) ( $response['authorizationUrl'] ?? '' ) );
        if ( '' === $authorization_url ) {
            $this->state_store->delete( $user_id, $state );
            $this->log_debug(
                'warn',
                'auth_login_authorization_url_invalid',
                [
                    'source'  => $source,
                    'user_id' => $user_id,
                ]
            );
            return Messages::error( 'invalid_response', 500 );
        }

        $this->log_debug(
            'info',
            'auth_login_started',
            [
                'source'  => $source,
                'user_id' => $user_id,
            ]
        );

        return [
            'authorizationUrl' => esc_url_raw( $authorization_url ),
            'state'            => $state,
        ];
    }

    /** @return array|\WP_Error */
    public function logout() {
        $user_id = get_current_user_id();
        $owner   = $this->refresh_lock->acquire();

        if ( null === $owner ) {
            return Messages::error( 'auth_refresh_failed', 503 );
        }

        try {
            $session = $this->get_session();

            if ( $this->is_connected_session( $session ) ) {
                $refresh_token = trim( (string) ( $session['refreshToken'] ?? '' ) );
                if ( '' !== $refresh_token ) {
                    $revoke = $this->client->revoke_session( $refresh_token );
                    if ( is_wp_error( $revoke ) ) {
                        $this->log_debug(
                            'warn',
                            'auth_disconnect_revoke_failed',
                            [
                                'user_id'    => $user_id,
                                'status'     => (int) ( $revoke->get_error_data()['status'] ?? 0 ),
                                'error_code' => $revoke->get_error_code(),
                            ]
                        );

                        return new \WP_Error(
                            'fotogenic_ai_auth_revoke_failed',
                            __( 'Fotogenic AI could not confirm the remote logout. This site remains connected so you can retry safely.', 'fotogenic-ai' ),
                            [ 'status' => 503 ]
                        );
                    }
                }
            }

            $this->clear_session( $session );
            $this->log_debug(
                'info',
                'auth_disconnected',
                [
                    'user_id' => $user_id,
                ]
            );

            return $this->get_public_status();
        } finally {
            $this->refresh_lock->release( $owner );
        }
    }

    public function handle_callback(): void {
        if ( ! is_user_logged_in() || ! fotogenic_ai_user_can_access() ) {
            wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'fotogenic-ai' ), 403 );
        }

        $user_id      = get_current_user_id();
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- OAuth callback integrity is enforced by the one-time state token consumed immediately below.
        $state        = isset( $_GET['state'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['state'] ) ) : '';
        $code         = isset( $_GET['code'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['code'] ) ) : '';
        $error        = isset( $_GET['error'] ) ? sanitize_text_field( (string) wp_unslash( $_GET['error'] ) ) : '';
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        $state_payload = $this->state_store->consume( $user_id, $state );
        $return_url   = $this->sanitize_return_url( is_array( $state_payload ) ? (string) ( $state_payload['return_url'] ?? '' ) : '' );
        $source       = is_array( $state_payload ) ? sanitize_key( (string) ( $state_payload['source'] ?? 'settings' ) ) : 'settings';
        $auth_source  = is_array( $state_payload ) ? $this->sanitize_oauth_source( (string) ( $state_payload['auth_source'] ?? '' ) ) : '';

        if ( ! is_array( $state_payload ) ) {
            $this->log_debug(
                'warn',
                'auth_callback_state_invalid',
                [
                    'user_id' => $user_id,
                ]
            );
            $this->render_callback_bridge_page(
                'error',
                __( 'Login could not be verified. Please try again.', 'fotogenic-ai' ),
                $return_url
            );
            exit;
        }

        if ( '' !== $error ) {
            $this->log_debug(
                'warn',
                'auth_callback_error',
                [
                    'source'    => $source,
                    'user_id'   => $user_id,
                    'authError' => $error,
                ]
            );
            $this->render_callback_bridge_page(
                'error',
                __( 'Login to Fotogenic AI was canceled or failed. Please try again.', 'fotogenic-ai' ),
                $return_url
            );
            exit;
        }

        if ( '' === $code ) {
            $this->log_debug(
                'warn',
                'auth_callback_code_missing',
                [
                    'source'  => $source,
                    'user_id' => $user_id,
                ]
            );
            $this->render_callback_bridge_page(
                'error',
                __( 'Login code was missing. Please try again.', 'fotogenic-ai' ),
                $return_url
            );
            exit;
        }

		$exchange = $this->exchange_callback_code( $code, $this->get_redirect_uri(), $state, $auth_source );
        if ( is_wp_error( $exchange ) ) {
            $this->log_debug(
                'warn',
                'auth_exchange_failed',
                [
                    'source'     => $source,
                    'user_id'    => $user_id,
                    'status'     => (int) ( $exchange->get_error_data()['status'] ?? 0 ),
                    'error_code' => $exchange->get_error_code(),
                ]
            );
            $this->render_callback_bridge_page(
                'error',
                $exchange->get_error_message(),
                $return_url
            );
            exit;
        }

        $stored = $this->store_authenticated_session( $exchange );
        if ( is_wp_error( $stored ) ) {
            $this->log_debug(
                'error',
                'auth_session_store_failed',
                [
                    'source'     => $source,
                    'user_id'    => $user_id,
                    'status'     => (int) ( $stored->get_error_data()['status'] ?? 0 ),
                    'error_code' => $stored->get_error_code(),
                ]
            );
            $this->render_callback_bridge_page(
                'error',
                $stored->get_error_message(),
                $return_url
            );
            exit;
        }

        $refresh = Plugin_Cache_Refresher::instance()->refresh();
        $credits = is_array( $refresh['credits'] ?? null ) ? $refresh['credits'] : [];
        if ( empty( $credits['available'] ) ) {
            $this->log_debug(
                'warn',
                'auth_connected_cache_refresh_partial',
                [
                    'source'     => $source,
                    'user_id'    => $user_id,
                    'status'     => (int) ( $credits['errorStatus'] ?? 0 ),
                    'error_code' => (string) ( $credits['errorCode'] ?? '' ),
                ]
            );
            $this->render_callback_bridge_page(
                'success',
                __( 'Connected to Fotogenic AI. Plugin cache will finish refreshing shortly.', 'fotogenic-ai' ),
                $return_url
            );
            exit;
        }

        $balance  = array_key_exists( 'balance', $credits ) ? (float) $credits['balance'] : null;
        $low_credits = null !== $balance && $balance < self::LOW_CREDITS_THRESHOLD;

        $this->log_debug(
            'info',
            'auth_connected',
            [
                'source'      => $source,
                'user_id'     => $user_id,
                'balance'     => null !== $balance ? (float) $balance : null,
                'low_credits' => $low_credits,
            ]
        );
        $this->render_callback_bridge_page(
            'success',
            __( 'Connected to Fotogenic AI.', 'fotogenic-ai' ),
            $return_url
        );
		exit;
	}

	/** @return array|\WP_Error */
	private function exchange_callback_code(
		string $code,
		string $redirect_uri,
		string $state,
		string $auth_source,
		?Platform_Capabilities $capabilities = null
	) {
		$crypto_error = $this->writable_crypto_error( $capabilities );
		if ( null !== $crypto_error ) {
			return $crypto_error;
		}

		return $this->client->exchange_code( $code, $redirect_uri, $state, $auth_source );
	}

	private function writable_crypto_error( ?Platform_Capabilities $capabilities = null ): ?\WP_Error {
		$crypto = ( $capabilities ?? Platform_Capabilities::instance() )->crypto_status();
		if ( $crypto['available'] ) {
			return null;
		}

		return new \WP_Error(
			'fotogenic_ai_auth_encryption_unavailable',
			$crypto['message'],
			[ 'status' => 503 ]
		);
	}

	public function handle_scheduled_refresh(): void {
		if ( ! Background_Runtime_State::is_enabled() ) {
			$this->clear_refresh_schedule();
			return;
		}
        if ( ! $this->is_connected() ) {
            $this->clear_refresh_schedule();
            return;
        }

        $this->maybe_refresh_session( $this->get_session(), false, false );
    }

    /** @return string|\WP_Error */
    public function get_access_token_for_request() {
        $session = $this->get_session();
        if ( ! $this->is_connected_session( $session ) ) {
            return Messages::error( 'missing_auth_session', 400 );
        }

        $session = $this->maybe_refresh_session( $session, false, false );
        if ( is_wp_error( $session ) ) {
            return $session;
        }

        $access_token = trim( (string) ( $session['accessToken'] ?? '' ) );
        if ( '' === $access_token ) {
            return Messages::error( 'auth_session_expired', 401 );
        }

        return $access_token;
    }

    /** @return string|\WP_Error */
    public function force_refresh_after_unauthorized( string $rejected_access_token = '' ) {
        $session = $this->get_session();
        if ( ! $this->is_connected_session( $session ) ) {
            return Messages::error( 'missing_auth_session', 400 );
        }

        $session = $this->maybe_refresh_session( $session, true, true, $rejected_access_token );
        if ( is_wp_error( $session ) ) {
            return $session;
        }

        $access_token = trim( (string) ( $session['accessToken'] ?? '' ) );
        if ( '' === $access_token ) {
            return Messages::error( 'auth_session_expired', 401 );
        }

        return $access_token;
    }

    public function get_session( ?Platform_Capabilities $capabilities = null ): array {
        $stored = get_option( self::OPTION_NAME, [] );
        if ( ! is_array( $stored ) ) {
            return [];
        }

        $session = $stored;
        $session['accessToken']  = fotogenic_ai_decrypt_string( (string) ( $stored['accessToken'] ?? '' ), $capabilities );
        $session['refreshToken'] = fotogenic_ai_decrypt_string( (string) ( $stored['refreshToken'] ?? '' ), $capabilities );

        return $session;
    }

    public function store_authenticated_session( array $payload ) {
        $payload = $this->validate_oauth_success_payload( $payload, true );
        if ( is_wp_error( $payload ) ) {
            return $payload;
        }

		$owner = $this->refresh_lock->acquire();
		if ( null === $owner ) {
			return Messages::error( 'auth_refresh_failed', 503 );
		}

		try {
			$existing = $this->get_session();
			$incoming_identity = $this->extract_identity_from_payload( $payload );

			if ( $this->is_connected_session( $existing ) && ! hash_equals( trim( (string) $existing['userUid'] ), $incoming_identity ) ) {
				$this->clear_auth_dependent_caches( $existing );
			}

			$session = $this->normalize_session_from_payload( $payload, $existing, true );
			$persisted = $this->persist_session( $session );
			if ( is_wp_error( $persisted ) ) {
				return $persisted;
			}

			$this->ensure_refresh_schedule( $session );
			return true;
		} finally {
			$this->refresh_lock->release( $owner );
		}
    }

    private function persist_refreshed_session( array $payload, array $existing ) {
		$payload = $this->validate_oauth_success_payload( $payload, false );
		if ( is_wp_error( $payload ) ) {
			return $payload;
		}

		$existing_identity = trim( (string) ( $existing['userUid'] ?? '' ) );
		$incoming_identity = $this->extract_identity_from_payload( $payload );
		if (
			'' === $existing_identity
			|| ( '' !== $incoming_identity && ! hash_equals( $existing_identity, $incoming_identity ) )
		) {
			return new \WP_Error(
				'fotogenic_ai_auth_identity_changed',
				__( 'The connected Fotogenic account identity could not be verified. Reconnect before continuing.', 'fotogenic-ai' ),
				[ 'status' => 401 ]
			);
		}

        $session = $this->normalize_session_from_payload( $payload, $existing, false );
        $persisted = $this->persist_session( $session );
        if ( is_wp_error( $persisted ) ) {
            return $persisted;
        }

        $this->ensure_refresh_schedule( $session );
        return true;
    }

    private function maybe_refresh_session( array $session, bool $force, bool $after_unauthorized, string $rejected_access_token = '' ) {
        if ( ! $this->is_connected_session( $session ) ) {
            return Messages::error( 'missing_auth_session', 400 );
        }

        $now_ms = $this->now_ms();
        if ( ! $force && ! $after_unauthorized && ! $this->should_refresh( $session, $now_ms ) ) {
            return $session;
        }

        $expires_at = (int) ( $session['accessTokenExpiresAtMs'] ?? 0 );

        $owner = $this->refresh_lock->acquire();
        if ( null === $owner ) {
            if ( ! $after_unauthorized && $expires_at > $now_ms ) {
                return $session;
            }
            return Messages::error( 'auth_refresh_failed', 503 );
        }

        try {
            $current = $this->get_session();
            if ( ! $this->is_connected_session( $current ) ) {
                return Messages::error( 'missing_auth_session', 400 );
            }

			$current_access_token = trim( (string) ( $current['accessToken'] ?? '' ) );
			if (
				$after_unauthorized
				&& '' !== $rejected_access_token
				&& '' !== $current_access_token
				&& ! hash_equals( $rejected_access_token, $current_access_token )
			) {
				return $current;
			}

            if ( $this->has_rotated_token_pair( $session, $current ) ) {
                return $current;
            }

            $current_now_ms = $this->now_ms();
            if ( ! $after_unauthorized && ! $force && ! $this->should_refresh( $current, $current_now_ms ) ) {
                return $current;
            }

            $refresh_token = trim( (string) ( $current['refreshToken'] ?? '' ) );
            if ( '' === $refresh_token ) {
                $this->log_debug(
                    'warn',
                    'auth_refresh_token_missing',
                    [
                        'after_unauthorized' => $after_unauthorized,
                    ]
                );
                $this->expire_session( $current );
                return Messages::error( 'auth_session_expired', 401 );
            }

            $response = $this->client->refresh_session( $refresh_token );
            if ( is_wp_error( $response ) ) {
                $status = (int) ( $response->get_error_data()['status'] ?? 0 );
                $this->log_debug(
                    'warn',
                    'auth_refresh_failed',
                    [
                        'status'             => $status,
                        'after_unauthorized' => $after_unauthorized,
                    ]
                );
                $this->expire_session( $current );
                return Messages::error( 'auth_session_expired', 401 );
            }

            $persisted = $this->persist_refreshed_session( $response, $current );
            if ( is_wp_error( $persisted ) ) {
                $this->expire_session( $current );
                return $persisted;
            }

            return $this->get_session();
        } finally {
            $this->refresh_lock->release( $owner );
        }
    }

    private function normalize_session_from_payload( array $payload, array $existing, bool $is_exchange ): array {
        $user = isset( $payload['user'] ) && is_array( $payload['user'] ) ? $payload['user'] : [];
        $connected_at = (int) ( $existing['connectedAt'] ?? time() );
        $connected_by = (int) ( $existing['connectedByUserId'] ?? get_current_user_id() );

        $session = [
            'tokenType'               => $payload['tokenType'],
            'accessToken'             => $payload['accessToken'],
            'refreshToken'            => $payload['refreshToken'],
            'expiresIn'               => $payload['expiresIn'],
            'accessTokenExpiresAtMs'  => $payload['accessTokenExpiresAtMs'],
            'refreshTokenExpiresAtMs' => $payload['refreshTokenExpiresAtMs'],
            'nextTokenRefreshAtMs'    => $payload['nextTokenRefreshAtMs'],
            'userUid'                 => isset( $user['uid'] ) ? $user['uid'] : (string) ( $existing['userUid'] ?? '' ),
            'userEmail'               => sanitize_email( (string) ( $user['email'] ?? $payload['userEmail'] ?? $existing['userEmail'] ?? '' ) ),
            'displayName'             => sanitize_text_field( (string) ( $user['displayName'] ?? $payload['displayName'] ?? $existing['displayName'] ?? '' ) ),
            'photoURL'                => esc_url_raw( (string) ( $user['photoURL'] ?? $payload['photoURL'] ?? $existing['photoURL'] ?? '' ) ),
            'platform'                => $payload['platform'],
            'connectedAt'             => $connected_at > 0 ? $connected_at : time(),
            'updatedAt'               => time(),
            'connectedByUserId'       => $connected_by > 0 ? $connected_by : get_current_user_id(),
        ];

        if ( $is_exchange && array_key_exists( 'accountStatus', $payload ) ) {
            $session['accountStatus'] = $payload['accountStatus'];
        } elseif ( ! $is_exchange && array_key_exists( 'accountStatus', $existing ) ) {
            $session['accountStatus'] = (string) $existing['accountStatus'];
        }

        return $session;
    }

    private function persist_session( array $session ) {
        $access_token  = trim( (string) ( $session['accessToken'] ?? '' ) );
        $refresh_token = trim( (string) ( $session['refreshToken'] ?? '' ) );

        if ( '' === $access_token || '' === $refresh_token ) {
            return Messages::error( 'invalid_response', 500 );
        }

        $encrypted_access  = fotogenic_ai_encrypt_string( $access_token );
        $encrypted_refresh = fotogenic_ai_encrypt_string( $refresh_token );
        if ( '' === $encrypted_access || '' === $encrypted_refresh ) {
            return new \WP_Error(
                'fotogenic_ai_auth_encryption_error',
                __( 'Fotogenic login could not be stored because encryption is unavailable.', 'fotogenic-ai' ),
                [ 'status' => 500 ]
            );
        }

        $stored = $session;
        $stored['accessToken']  = $encrypted_access;
        $stored['refreshToken'] = $encrypted_refresh;

        if ( false === get_option( self::OPTION_NAME, false ) ) {
            $written = add_option( self::OPTION_NAME, $stored, '', false );
        } else {
            $written = update_option( self::OPTION_NAME, $stored, false );
        }

        if ( ! $written ) {
            return Messages::error( 'auth_refresh_failed', 503 );
        }

        return true;
    }

    private function is_connected_session( array $session ): bool {
        return '' !== trim( (string) ( $session['refreshToken'] ?? '' ) )
            && '' !== trim( (string) ( $session['userUid'] ?? '' ) );
    }

    private function has_rotated_token_pair( array $before_lock, array $after_lock ): bool {
        $before_access  = trim( (string) ( $before_lock['accessToken'] ?? '' ) );
        $before_refresh = trim( (string) ( $before_lock['refreshToken'] ?? '' ) );
        $after_access   = trim( (string) ( $after_lock['accessToken'] ?? '' ) );
        $after_refresh  = trim( (string) ( $after_lock['refreshToken'] ?? '' ) );

        return '' !== $before_access
            && '' !== $before_refresh
            && '' !== $after_access
            && '' !== $after_refresh
            && ( ! hash_equals( $before_access, $after_access ) || ! hash_equals( $before_refresh, $after_refresh ) );
    }

    private function should_refresh( array $session, int $now_ms ): bool {
        $next_refresh_at = (int) ( $session['nextTokenRefreshAtMs'] ?? 0 );
        if ( $next_refresh_at > 0 && $now_ms >= $next_refresh_at ) {
            return true;
        }

        $expires_at = (int) ( $session['accessTokenExpiresAtMs'] ?? 0 );
        if ( $expires_at <= 0 ) {
            return true;
        }

        return $now_ms >= ( $expires_at - self::REFRESH_THRESHOLD_MS );
    }

    private function now_ms(): int {
        return (int) round( microtime( true ) * 1000 );
    }

	private function ensure_refresh_schedule( ?array $session = null ): void {
		if ( ! Background_Runtime_State::is_enabled() ) {
			$this->clear_refresh_schedule();
			return;
		}

        $session = is_array( $session ) ? $session : $this->get_session();
        $next_refresh_at_ms = (int) ( $session['nextTokenRefreshAtMs'] ?? 0 );
        $scheduled_at = $next_refresh_at_ms > 0
            ? max( time() + 1, (int) ceil( $next_refresh_at_ms / 1000 ) )
            : time() + ( 15 * MINUTE_IN_SECONDS );
        $existing_at = wp_next_scheduled( self::CRON_HOOK );
        if ( false === $existing_at || abs( (int) $existing_at - $scheduled_at ) > 1 ) {
            if ( false !== $existing_at ) {
                $this->clear_refresh_schedule();
            }
            wp_schedule_single_event( $scheduled_at, self::CRON_HOOK );
		}
		if ( ! Background_Runtime_State::is_enabled() ) {
			$this->clear_refresh_schedule();
		}
    }

    public function clear_refresh_schedule(): void {
        wp_clear_scheduled_hook( self::CRON_HOOK );
    }

    public function consume_notice_for_current_user(): array {
        if ( ! is_user_logged_in() || ! fotogenic_ai_user_can_access() ) {
            return [];
        }

        $key = $this->get_notice_key( get_current_user_id() );
        $notice = get_transient( $key );
        delete_transient( $key );

        if ( ! is_array( $notice ) || empty( $notice['message'] ) ) {
            return [];
        }

        $type = (string) ( $notice['type'] ?? 'info' );
        if ( ! in_array( $type, [ 'success', 'error', 'info' ], true ) ) {
            $type = 'info';
        }

        return [
            'type'    => $type,
            'message' => sanitize_text_field( (string) $notice['message'] ),
        ];
    }

    public function clear_session( ?array $session = null ): void {
        $session = is_array( $session ) ? $session : $this->get_session();

        delete_option( self::OPTION_NAME );
        $this->clear_refresh_schedule();
        $this->clear_auth_dependent_caches( $session );
    }

    public function expire_session( ?array $session = null ): void {
        $session = is_array( $session ) ? $session : $this->get_session();
        $this->clear_session( $session );
        $this->set_notice( 'error', Messages::t( 'auth_session_expired' ) );
    }

    private function clear_auth_dependent_caches( array $session ): void {
        $identity = $this->get_cache_identity( $session );
        $config_service = new Service_Configuration();
        $config_service->clear_cache_for_identity( $identity );
        Config_Provider::clear_memory();
        Credits_Service::instance()->clear_user_cache( $identity );
    }

    private function sanitize_return_url( string $url ): string {
        $url = trim( $url );
        if ( '' === $url ) {
            return fotogenic_ai_get_settings_url();
        }

        $validated = wp_validate_redirect( $url, '' );
        if ( '' === $validated ) {
            return fotogenic_ai_get_settings_url();
        }

        return $validated;
    }

    private function get_redirect_uri(): string {
        return set_url_scheme( admin_url( 'admin-post.php?action=fotogenic_auth_cb' ), 'https' );
    }

    private function get_return_url_for_source( string $source ): string {
        return 'studio' === $source ? fotogenic_ai_get_studio_url() : fotogenic_ai_get_settings_url();
    }

    private function get_oauth_source(): string {
        $candidates = [
            home_url( '/' ),
            site_url( '/' ),
        ];

        foreach ( $candidates as $candidate ) {
            $host = wp_parse_url( $candidate, PHP_URL_HOST );
            if ( ! is_string( $host ) || '' === $host ) {
                continue;
            }

            $source = $this->sanitize_oauth_source( $host );
            if ( '' !== $source ) {
                return $source;
            }
        }

        return '';
    }

    private function sanitize_oauth_source( string $source ): string {
        $source = trim( wp_strip_all_tags( $source ) );
        $source = preg_replace( '/[\x00-\x1F\x7F]+/u', '', $source );
        if ( ! is_string( $source ) ) {
            return '';
        }

        if ( function_exists( 'mb_substr' ) ) {
            $source = mb_substr( $source, 0, 255 );
        } else {
            $source = substr( $source, 0, 255 );
        }

        return sanitize_text_field( $source );
    }

    private function set_notice( string $type, string $message ): void {
        if ( ! is_user_logged_in() ) {
            return;
        }

        set_transient(
            $this->get_notice_key( get_current_user_id() ),
            [
                'type'    => $type,
                'message' => $message,
            ],
            5 * MINUTE_IN_SECONDS
        );
    }

    private function get_notice_key( int $user_id ): string {
        return self::NOTICE_PREFIX . $user_id;
    }

    private function extract_identity_from_payload( array $payload ): string {
        $user = isset( $payload['user'] ) && is_array( $payload['user'] ) ? $payload['user'] : [];
        return isset( $user['uid'] ) && is_string( $user['uid'] ) ? trim( $user['uid'] ) : '';
    }

    /** @return array|\WP_Error */
    private function validate_oauth_success_payload( array $payload, bool $is_exchange ) {
        $required_strings = [ 'tokenType', 'accessToken', 'refreshToken', 'platform' ];
        foreach ( $required_strings as $field ) {
            if ( ! isset( $payload[ $field ] ) || ! is_string( $payload[ $field ] ) || '' === trim( $payload[ $field ] ) ) {
                return Messages::error( 'invalid_response', 500 );
            }
        }

        if ( true !== ( $payload['success'] ?? null )
            || ! hash_equals( 'Bearer', $payload['tokenType'] )
            || ! hash_equals( 'wp', $payload['platform'] ) ) {
            return Messages::error( 'invalid_response', 500 );
        }

        $required_integers = [ 'expiresIn', 'accessTokenExpiresAtMs', 'refreshTokenExpiresAtMs', 'nextTokenRefreshAtMs' ];
        foreach ( $required_integers as $field ) {
            if ( ! isset( $payload[ $field ] ) || ! is_int( $payload[ $field ] ) || $payload[ $field ] <= 0 ) {
                return Messages::error( 'invalid_response', 500 );
            }
        }

        if ( $payload['nextTokenRefreshAtMs'] > $payload['accessTokenExpiresAtMs']
            || $payload['accessTokenExpiresAtMs'] >= $payload['refreshTokenExpiresAtMs'] ) {
            return Messages::error( 'invalid_response', 500 );
        }

        if ( ! $is_exchange ) {
            return $payload;
        }

        if ( array_key_exists( 'accountStatus', $payload )
            && ( ! is_string( $payload['accountStatus'] )
            || ! in_array( $payload['accountStatus'], [ 'created', 'existing' ], true ) ) ) {
            return Messages::error( 'invalid_response', 500 );
        }

        if ( ! isset( $payload['user'] ) || ! is_array( $payload['user'] ) || ! isset( $payload['user']['uid'] ) || ! is_string( $payload['user']['uid'] ) || '' === trim( $payload['user']['uid'] ) ) {
            return Messages::error( 'invalid_response', 500 );
        }

        foreach ( [ 'email', 'displayName', 'photoURL' ] as $field ) {
            if ( array_key_exists( $field, $payload['user'] ) && null !== $payload['user'][ $field ] && ! is_string( $payload['user'][ $field ] ) ) {
                return Messages::error( 'invalid_response', 500 );
            }
        }

        return $payload;
    }

    private function get_trusted_external_url( string $url ): string {
        $url    = esc_url_raw( trim( $url ) );
        $host   = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
        $scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
        $allowed_hosts = (array) apply_filters(
            'fotogenic_ai_allowed_external_hosts',
            []
        );
        $allowed_root_domains = (array) apply_filters(
            'fotogenic_ai_allowed_external_root_domains',
            [ 'fotogenic.ai' ]
        );
        $allowed_hosts = array_values(
            array_unique(
                array_filter(
                    array_map(
                        static fn( $value ) => strtolower( sanitize_text_field( (string) $value ) ),
                        $allowed_hosts
                    )
                )
            )
        );
        $allowed_root_domains = array_values(
            array_unique(
                array_filter(
                    array_map(
                        static fn( $value ) => strtolower( ltrim( sanitize_text_field( (string) $value ), '.' ) ),
                        $allowed_root_domains
                    )
                )
            )
        );
        $allowed_host = in_array( $host, $allowed_hosts, true );

        if ( ! $allowed_host ) {
            foreach ( $allowed_root_domains as $allowed_root_domain ) {
                if ( $host === $allowed_root_domain || fotogenic_ai_str_ends_with( $host, '.' . $allowed_root_domain ) ) {
                    $allowed_host = true;
                    break;
                }
            }
        }

        if ( '' === $url || '' === $host || 'https' !== $scheme || ! $allowed_host ) {
            return '';
        }

        return $url;
    }

    private function render_callback_bridge_page( string $notice_type, string $notice_message, string $return_url, string $external_redirect_url = '' ): void {
        $return_url = $this->sanitize_return_url( $return_url );
        $external_redirect_url = '' !== $external_redirect_url ? $this->get_trusted_external_url( $external_redirect_url ) : '';
        $notice_type = in_array( $notice_type, [ 'success', 'error', 'info' ], true ) ? $notice_type : 'info';
        $payload = [
            'id'        => wp_generate_uuid4(),
            'type'      => $notice_type,
            'message'   => sanitize_text_field( $notice_message ),
            'connected' => $this->is_connected(),
            'ts'        => time(),
        ];

        nocache_headers();
        status_header( 200 );

        $style_handle  = 'fotogenic-ai-auth-bridge';
        $script_handle = 'fotogenic-ai-auth-bridge';

        if ( ! wp_style_is( $style_handle, 'registered' ) ) {
            wp_register_style( $style_handle, false, [], FOTOGENIC_AI_VERSION );
        }
        wp_enqueue_style( $style_handle );
        wp_add_inline_style( $style_handle, 'html,body{margin:0;padding:0;background:#fff;}' );

        if ( ! wp_script_is( $script_handle, 'registered' ) ) {
            wp_register_script( $script_handle, '', [], FOTOGENIC_AI_VERSION, false );
        }
        wp_enqueue_script( $script_handle );
        wp_add_inline_script(
            $script_handle,
            implode(
                '',
                [
                    '(function(){',
                    'var payload=' . fotogenic_ai_json_encode_for_inline( $payload ) . ';',
                    'var channelName=' . fotogenic_ai_json_encode_for_inline( self::SYNC_CHANNEL, '""' ) . ';',
                    'var storageKey=' . fotogenic_ai_json_encode_for_inline( self::SYNC_EVENT_KEY, '""' ) . ';',
                    'var returnUrl=' . fotogenic_ai_json_encode_for_inline( $return_url, '""' ) . ';',
                    'var externalUrl=' . fotogenic_ai_json_encode_for_inline( $external_redirect_url, '""' ) . ';',
                    'function publishChannel(){try{if(typeof BroadcastChannel==="undefined"){return false;}var channel=new BroadcastChannel(channelName);channel.postMessage(payload);channel.close();return true;}catch(error){return false;}}',
                    'function publishStorage(){try{localStorage.setItem(storageKey, JSON.stringify(payload));return true;}catch(error){return false;}}',
                    'publishChannel()||publishStorage();',
                    'if(externalUrl){window.setTimeout(function(){window.location.replace(externalUrl);},60);return;}',
                    'window.setTimeout(function(){window.close();},20);',
                    'window.setTimeout(function(){window.location.replace(returnUrl);},280);',
                    '})();',
                ]
            ),
            'after'
        );

        echo '<!doctype html><html><head>';
        echo '<meta charset="' . esc_attr( get_bloginfo( 'charset' ) ) . '" />';
        echo '<meta name="viewport" content="width=device-width, initial-scale=1" />';
        echo '<title>' . esc_html__( 'Fotogenic AI', 'fotogenic-ai' ) . '</title>';
        wp_print_styles( [ $style_handle ] );
        echo '</head><body>';
        wp_print_scripts( [ $script_handle ] );
        echo '</body></html>';
        exit;
    }

    private function log_debug( string $level, string $event, array $context = [] ): void {
        $logger = Logger::instance();
        if ( 'error' === $level ) {
            $logger->error( $event, $context );
            return;
        }
        if ( 'warn' === $level ) {
            $logger->warn( $event, $context );
            return;
        }
        $logger->info( $event, $context );
    }
}
