<?php
namespace EncypherProvenance;

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

/**
 * Secure v2 site-capability activation client.
 *
 * Proves control of THIS exact host to the Encypher API, which returns a
 * site-scoped install credential sealed (libsodium crypto_box_seal) to a
 * one-time Curve25519 key this plugin generates. No account, admin email, or
 * organization selection is ever sent: public host proof yields only an
 * unclaimed, free, site-scoped credential. Account linking/claim happens later
 * through the authenticated dashboard flow.
 *
 * The challenge starts in a manage_options request and completion runs in a
 * bounded cron worker. Attempt-bound state keeps the one-time delivery key and
 * decrypted credential on the server; the browser sees only safe status data.
 */
class ProvisionV2
{
    const SETTINGS_KEY = 'encypher_provenance_settings';
    const WELL_KNOWN_SETTING = 'well_known_token';
    const ACTIVATION_ID_SETTING = 'provision_activation_id';
    // The one-time X25519 delivery private key. Stored non-autoloaded and
    // deleted immediately after the credential is decrypted and acked.
    const DELIVERY_KEY_OPTION = 'encypher_provenance_delivery_keypair';
    const PENDING_OPTION = 'encypher_provenance_pending_activation';
    const COMPLETION_LEASE_OPTION = 'encypher_provenance_completion_lease';
    const START_LEASE_OPTION = 'encypher_provenance_activation_start_lease';
    const COMPLETION_HOOK = 'encypher_provision_v2_complete';
    const MAX_COMPLETION_ATTEMPTS = 5;
    const COMPLETION_LEASE_SECONDS = 45;
    const START_LEASE_SECONDS = 30;
    // Credential delivery origin is PINNED to Encypher production. The freely
    // configurable api_base_url is NOT a credential-delivery authority (a
    // public origin could otherwise receive a sealed credential). Override only
    // via an explicit constant for staging/testing.
    const DEFAULT_PROVISION_ORIGIN = 'https://api.encypher.com';

    public function register_hooks(): void
    {
        add_action('rest_api_init', [$this, 'register_routes']);
        add_action(self::COMPLETION_HOOK, [$this, 'run_queued_site_activate_complete'], 10, 1);
    }

    public function register_routes(): void
    {
        register_rest_route('encypher-provenance/v1', '/site-activate', [
            'methods' => \WP_REST_Server::CREATABLE,
            'permission_callback' => function () {
                return current_user_can('manage_options');
            },
            'callback' => [$this, 'handle_site_activate'],
        ]);
        register_rest_route('encypher-provenance/v1', '/site-activate/complete', [
            'methods' => \WP_REST_Server::CREATABLE,
            'permission_callback' => function () {
                return current_user_can('manage_options');
            },
            'callback' => [$this, 'queue_site_activate_complete'],
        ]);
        register_rest_route('encypher-provenance/v1', '/site-activate/status', [
            'methods' => \WP_REST_Server::READABLE,
            'permission_callback' => function () {
                return current_user_can('manage_options');
            },
            'callback' => [$this, 'handle_site_activate_status'],
        ]);
    }

    private function provision_origin(): string
    {
        if (defined('ENCYPHER_PROVISION_API_ORIGIN') && is_string(ENCYPHER_PROVISION_API_ORIGIN) && '' !== ENCYPHER_PROVISION_API_ORIGIN) {
            return rtrim(ENCYPHER_PROVISION_API_ORIGIN, '/');
        }
        return self::DEFAULT_PROVISION_ORIGIN;
    }

    private function ensure_install_id(array &$settings): string
    {
        $install_id = isset($settings['install_id']) ? trim((string) $settings['install_id']) : '';
        // Upgrade legacy/short (64-bit truncated) ids to >=128-bit random.
        if ('' === $install_id || strlen($install_id) < 24) {
            $install_id = 'wpi_' . bin2hex(random_bytes(16));
            $settings['install_id'] = $install_id;
            update_option(self::SETTINGS_KEY, $settings);
        }
        return $install_id;
    }

    /**
     * Orchestrate: challenge -> serve nonce -> complete -> decrypt -> store -> ack.
     */
    public function handle_site_activate(\WP_REST_Request $request)
    {
        $tos_accepted = $request->get_param('tos_accepted');
        if (! in_array($tos_accepted, [true, 'true', 1, '1'], true)) {
            return new \WP_Error(
                'tos_not_accepted',
                __('You must accept the Encypher Terms of Service and Privacy Policy before activating provenance.', 'encypher-provenance'),
                ['status' => 400]
            );
        }

        if (! function_exists('sodium_crypto_box_keypair') || ! function_exists('sodium_crypto_box_seal_open')) {
            return new \WP_Error(
                'sodium_unavailable',
                __('Secure activation needs the Sodium PHP extension. Connect an Encypher account instead.', 'encypher-provenance'),
                ['status' => 501]
            );
        }

        $settings = get_option(self::SETTINGS_KEY, []);
        if (! is_array($settings)) {
            $settings = [];
        }
        if (! empty($settings['api_key'])) {
            return new \WP_REST_Response(['state' => 'already_connected'], 200);
        }

        update_option('encypher_tos_accepted', [
            'version' => class_exists(Rest::class) ? Rest::TOS_VERSION : '2.0',
            'accepted_at' => gmdate('c'),
            'user_id' => get_current_user_id(),
        ]);

        $host = (string) wp_parse_url(home_url('/'), PHP_URL_HOST);
        if ('' === $host) {
            return new \WP_Error('no_host', __('Could not determine this site host.', 'encypher-provenance'), ['status' => 400]);
        }

        $start_token = $this->acquire_start_lease();
        if ('' === $start_token) {
            return new \WP_Error('activation_busy', __('Another activation request is starting. Try again.', 'encypher-provenance'), ['status' => 409]);
        }

        $activation_id = 'wpa_' . bin2hex(random_bytes(16));
        $origin = $this->provision_origin();
        try {
            $settings = get_option(self::SETTINGS_KEY, []);
            if (! is_array($settings)) {
                $settings = [];
            }
            if (! empty($settings['api_key'])) {
                return new \WP_REST_Response(['state' => 'already_connected'], 200);
            }

            $install_id = $this->ensure_install_id($settings);
            $settings = get_option(self::SETTINGS_KEY, []);
            if (! is_array($settings)) {
                $settings = [];
            }
            $activation_settings = $settings;
            $activation_settings[self::ACTIVATION_ID_SETTING] = $activation_id;
            unset($activation_settings[self::WELL_KNOWN_SETTING]);
            if (! $this->compare_and_swap_option(self::SETTINGS_KEY, $settings, $activation_settings)) {
                return new \WP_Error('activation_busy', __('The site connection changed while activation was starting. Try again.', 'encypher-provenance'), ['status' => 409]);
            }
            if (! $this->owns_named_lease(self::START_LEASE_OPTION, $start_token)) {
                $this->clear_activation_settings_at_start($activation_id);
                return new \WP_Error('activation_busy', __('A newer activation request replaced this start.', 'encypher-provenance'), ['status' => 409]);
            }


            $pending = [
                'activation_id' => $activation_id,
                'state' => 'challenge_starting',
                'host' => $host,
                'created_at' => time(),
                'attempts' => 0,
            ];
            if (! update_option(self::PENDING_OPTION, $pending, false)) {
                $this->clear_activation_settings_at_start($activation_id);
                return new \WP_Error('activation_storage_failed', __('Could not store the activation attempt.', 'encypher-provenance'), ['status' => 500]);
            }
            if (! $this->owns_named_lease(self::START_LEASE_OPTION, $start_token)) {
                $this->cleanup_attempt($activation_id);
                return new \WP_Error('activation_busy', __('A newer activation request replaced this start.', 'encypher-provenance'), ['status' => 409]);
            }


            $keypair = sodium_crypto_box_keypair();
            $public_b64 = base64_encode(sodium_crypto_box_publickey($keypair));
            $delivery = [
                'activation_id' => $activation_id,
                'keypair' => base64_encode($keypair),
            ];
            sodium_memzero($keypair);
            if (! update_option(self::DELIVERY_KEY_OPTION, $delivery, false)) {
                $this->cleanup_attempt($activation_id);
                return new \WP_Error('activation_storage_failed', __('Could not store the activation delivery key.', 'encypher-provenance'), ['status' => 500]);
            }
        } finally {
            $this->release_named_lease(self::START_LEASE_OPTION, $start_token);
        }

        $res = $this->post_json($origin . '/api/v2/integrations/wordpress/provision/challenges', [
            'host' => $host,
            'install_id' => $install_id,
            'delivery_public_key' => $public_b64,
            'plugin_version' => ENCYPHER_PROVENANCE_VERSION,
        ], null);
        if (is_wp_error($res)) {
            $this->cleanup_attempt($activation_id);
            return $res;
        }
        list($code, $body) = $res;
        if (409 === $code) {
            $this->cleanup_attempt($activation_id);
            return new \WP_REST_Response(['state' => 'existing_site_sign_in_required', 'host' => $host], 200);
        }
        if (429 === $code) {
            $this->cleanup_attempt($activation_id);
            return new \WP_Error('rate_limited', $this->msg($body, __('Too many activation attempts. Try again shortly.', 'encypher-provenance')), ['status' => 429]);
        }
        if (202 !== $code || empty($body['challenge_id']) || empty($body['challenge_token'])) {
            $this->cleanup_attempt($activation_id);
            return new \WP_Error('challenge_failed', $this->msg($body, __('Could not start activation.', 'encypher-provenance')), ['status' => 502]);
        }

        $pending = $this->pending_for_activation($activation_id);
        if (null === $pending) {
            return new \WP_REST_Response(['state' => 'activation_superseded'], 409);
        }
        $challenge_id = sanitize_text_field((string) $body['challenge_id']);
        $challenge_token = sanitize_text_field((string) $body['challenge_token']);
        $ready = $pending;
        $ready['state'] = 'proof_pending';
        $ready['challenge_id'] = $challenge_id;
        $ready['challenge_token'] = $challenge_token;
        if (! $this->compare_and_swap_option(self::PENDING_OPTION, $pending, $ready)) {
            return new \WP_REST_Response(['state' => 'activation_superseded'], 409);
        }
        if (! $this->update_settings_for_activation(
            $activation_id,
            function (array $current) use ($challenge_token): array {
                $current[self::WELL_KNOWN_SETTING] = $challenge_token;
                return $current;
            }
        )) {
            $this->cleanup_attempt($activation_id);
            return new \WP_REST_Response(['state' => 'activation_superseded'], 409);
        }

        return new \WP_REST_Response([
            'state' => 'proof_pending',
            'host' => $host,
        ], 202);
    }

    /**
     * Queue bounded proof and sealed credential delivery outside the admin
     * request so reverse proxies never wait on the public HTTPS proof.
     */
    public function queue_site_activate_complete(\WP_REST_Request $request)
    {
        unset($request);
        $pending = get_option(self::PENDING_OPTION, []);
        if (
            ! is_array($pending)
            || empty($pending['activation_id'])
            || empty($pending['challenge_id'])
            || empty($pending['challenge_token'])
            || (int) ($pending['created_at'] ?? 0) < time() - 900
            || ! in_array((string) ($pending['state'] ?? ''), ['proof_pending', 'verification_queued', 'verification_running'], true)
        ) {
            return new \WP_Error('challenge_expired', __('The activation proof expired. Start again.', 'encypher-provenance'), ['status' => 410]);
        }

        $activation_id = (string) $pending['activation_id'];
        $queued = $pending;
        $queued['state'] = 'verification_queued';
        $queued['queued_at'] = time();
        $queued['next_attempt_at'] = time();
        if (! $this->compare_and_swap_option(self::PENDING_OPTION, $pending, $queued)) {
            return new \WP_Error('activation_superseded', __('A newer activation attempt replaced this one.', 'encypher-provenance'), ['status' => 409]);
        }

        $cron_args = [$activation_id];
        if (! wp_next_scheduled(self::COMPLETION_HOOK, $cron_args)) {
            wp_schedule_single_event(time(), self::COMPLETION_HOOK, $cron_args);
        }
        if (function_exists('spawn_cron')) {
            spawn_cron(time());
        }
        return new \WP_REST_Response(['state' => 'verification_queued'], 202);
    }

    public function handle_site_activate_status(\WP_REST_Request $request)
    {
        unset($request);
        $pending = get_option(self::PENDING_OPTION, []);
        if (is_array($pending) && ! empty($pending['activation_id'])) {
            $activation_id = (string) $pending['activation_id'];
            $state = (string) ($pending['state'] ?? '');
            $due = (int) ($pending['next_attempt_at'] ?? 0) <= time();
            $orphaned_running = 'verification_running' === $state && ! $this->has_active_attempt_lease($activation_id);
            if (('verification_queued' === $state && $due) || $orphaned_running) {
                $cron_args = [$activation_id];
                if (! wp_next_scheduled(self::COMPLETION_HOOK, $cron_args)) {
                    wp_schedule_single_event(time(), self::COMPLETION_HOOK, $cron_args);
                }
                if (function_exists('spawn_cron')) {
                    spawn_cron(time());
                }
            }
            if (in_array($state, ['activation_failed', 'site_connected_unclaimed', 'already_connected'], true)) {
                $status = 'activation_failed' === $state ? 502 : 200;
                return new \WP_REST_Response([
                    'state' => $state,
                    'code' => (string) ($pending['code'] ?? ''),
                    'message' => (string) ($pending['message'] ?? ''),
                    'organization_id' => (string) ($pending['organization_id'] ?? ''),
                    'site_id' => (string) ($pending['site_id'] ?? ''),
                    'host' => (string) ($pending['host'] ?? ''),
                ], $status);
            }
        }

        $settings = get_option(self::SETTINGS_KEY, []);
        if (is_array($settings) && ! empty($settings['api_key'])) {
            return new \WP_REST_Response(['state' => 'already_connected'], 200);
        }
        return new \WP_REST_Response(['state' => 'verification_queued'], 202);
    }

    public function run_queued_site_activate_complete($activation_id = ''): void
    {
        $activation_id = trim((string) $activation_id);
        if ('' === $activation_id) {
            return;
        }

        $lease_token = $this->acquire_attempt_lease($activation_id);
        if ('' === $lease_token) {
            return;
        }

        try {
            $pending = $this->pending_for_activation($activation_id);
            if (
                null === $pending
                || ! in_array((string) ($pending['state'] ?? ''), ['verification_queued', 'verification_running'], true)
                || ! $this->owns_attempt($activation_id, $lease_token)
            ) {
                return;
            }
            $running = $pending;
            $running['state'] = 'verification_running';
            $running['worker_started_at'] = time();
            if (! $this->compare_and_swap_option(self::PENDING_OPTION, $pending, $running)) {
                return;
            }

            $result = $this->handle_site_activate_complete(new \WP_REST_Request(), $activation_id, $lease_token);
            if (! is_wp_error($result)) {
                return;
            }
            if (! $this->owns_attempt($activation_id, $lease_token)) {
                return;
            }

            $pending = $this->pending_for_activation($activation_id);
            if (null === $pending) {
                return;
            }
            $attempts = (int) ($pending['attempts'] ?? 0) + 1;
            if (
                $attempts < self::MAX_COMPLETION_ATTEMPTS
                && in_array($result->get_error_code(), ['site_control_not_verified', 'request_failed'], true)
            ) {
                $retry = $pending;
                $retry['state'] = 'verification_queued';
                $retry['attempts'] = $attempts;
                $retry['next_attempt_at'] = time() + 2;
                if (
                    $this->owns_attempt($activation_id, $lease_token)
                    && $this->compare_and_swap_option(self::PENDING_OPTION, $pending, $retry)
                    && $this->owns_attempt($activation_id, $lease_token)
                ) {
                    $cron_args = [$activation_id];
                    if (! wp_next_scheduled(self::COMPLETION_HOOK, $cron_args)) {
                        wp_schedule_single_event($retry['next_attempt_at'], self::COMPLETION_HOOK, $cron_args);
                    }
                }
                return;
            }

            $failed = $pending;
            $failed['state'] = 'activation_failed';
            $failed['attempts'] = $attempts;
            $failed['code'] = $result->get_error_code();
            $failed['message'] = $result->get_error_message();
            $failed['failed_at'] = time();
            if (
                $this->owns_attempt($activation_id, $lease_token)
                && $this->compare_and_swap_option(self::PENDING_OPTION, $pending, $failed)
                && $this->owns_attempt($activation_id, $lease_token)
            ) {
                $this->clear_activation_settings($activation_id, $lease_token);
                $this->cleanup_delivery($activation_id, $lease_token);
            }
        } finally {
            $this->release_attempt_lease($activation_id, $lease_token);
        }
    }

    public function handle_site_activate_complete(
        \WP_REST_Request $request,
        string $activation_id = '',
        string $lease_token = ''
    ) {
        unset($request);
        if ('' === $activation_id || '' === $lease_token || ! $this->owns_attempt($activation_id, $lease_token)) {
            return new \WP_REST_Response(['state' => 'activation_superseded'], 200);
        }

        $pending = $this->pending_for_activation($activation_id);
        if (null === $pending) {
            return new \WP_REST_Response(['state' => 'activation_superseded'], 200);
        }
        $settings = get_option(self::SETTINGS_KEY, []);
        if (! is_array($settings)) {
            $settings = [];
        }
        if (! empty($settings['api_key'])) {
            $connected = $pending;
            $connected['state'] = 'already_connected';
            $connected['completed_at'] = time();
            if ($this->owns_attempt($activation_id, $lease_token)) {
                $this->compare_and_swap_option(self::PENDING_OPTION, $pending, $connected);
                $this->clear_activation_settings($activation_id, $lease_token);
                $this->cleanup_delivery($activation_id, $lease_token);
            }
            return new \WP_REST_Response(['state' => 'already_connected'], 200);
        }
        if (
            empty($pending['challenge_id'])
            || empty($pending['challenge_token'])
            || (int) ($pending['created_at'] ?? 0) < time() - 900
        ) {
            return new \WP_Error('challenge_expired', __('The activation proof expired. Start again.', 'encypher-provenance'), ['status' => 410]);
        }

        $challenge_id = (string) $pending['challenge_id'];
        $challenge_token = (string) $pending['challenge_token'];
        $origin = $this->provision_origin();
        $res = $this->post_json($origin . '/api/v2/integrations/wordpress/provision/complete', [
            'challenge_id' => $challenge_id,
            'challenge_token' => $challenge_token,
        ], null, 18);
        if (is_wp_error($res)) {
            return $res;
        }
        if (! $this->owns_attempt($activation_id, $lease_token)) {
            return new \WP_REST_Response(['state' => 'activation_superseded'], 200);
        }
        $current_settings = get_option(self::SETTINGS_KEY, []);
        if (is_array($current_settings) && ! empty($current_settings['api_key'])) {
            $current_pending = $this->pending_for_activation($activation_id);
            if (null !== $current_pending && $this->owns_attempt($activation_id, $lease_token)) {
                $connected = $current_pending;
                $connected['state'] = 'already_connected';
                $connected['completed_at'] = time();
                $this->compare_and_swap_option(self::PENDING_OPTION, $current_pending, $connected);
                $this->clear_activation_settings($activation_id, $lease_token);
                $this->cleanup_delivery($activation_id, $lease_token);
            }
            return new \WP_REST_Response(['state' => 'already_connected'], 200);
        }


        list($code, $body) = $res;
        if (! in_array($code, [200, 201], true) || empty($body['credential_envelope'])) {
            return new \WP_Error('site_control_not_verified', $this->msg($body, __('Could not verify control of this site over HTTPS.', 'encypher-provenance')), ['status' => 502]);
        }

        $delivery = get_option(self::DELIVERY_KEY_OPTION, []);
        if (! is_array($delivery) || $activation_id !== (string) ($delivery['activation_id'] ?? '')) {
            return new \WP_REST_Response(['state' => 'activation_superseded'], 200);
        }
        $keypair_raw = base64_decode((string) ($delivery['keypair'] ?? ''), true);
        $envelope = base64_decode((string) $body['credential_envelope'], true);
        $api_key = false;
        if (is_string($keypair_raw) && is_string($envelope)) {
            $api_key = sodium_crypto_box_seal_open($envelope, $keypair_raw);
        }
        if (is_string($keypair_raw)) {
            sodium_memzero($keypair_raw);
        }
        if (false === $api_key || '' === $api_key) {
            return new \WP_Error('decrypt_failed', __('Could not decrypt the delivered credential.', 'encypher-provenance'), ['status' => 502]);
        }
        if (! $this->owns_attempt($activation_id, $lease_token)) {
            return new \WP_REST_Response(['state' => 'activation_superseded'], 200);
        }

        $stored = $this->update_settings_for_activation(
            $activation_id,
            function (array $current) use ($api_key, $origin, $body) {
                if (! empty($current['api_key'])) {
                    return false;
                }
                $current['api_key'] = sanitize_text_field((string) $api_key);
                $current['api_base_url'] = $origin . '/api/v1';
                $current['organization_id'] = isset($body['organization_id']) ? sanitize_text_field((string) $body['organization_id']) : '';
                $current['site_id'] = isset($body['site_id']) ? sanitize_text_field((string) $body['site_id']) : '';
                $current['credential_class'] = 'wordpress_install';
                $current['ownership'] = 'unclaimed';
                $current['tier'] = 'free';
                if (! in_array((string) ($current['connected_method'] ?? ''), ['provision_v2', 'oauth', 'manual'], true)) {
                    $current['connected_method'] = 'provision_v2';
                }
                if (empty($current['connected_at'])) {
                    $current['connected_at'] = gmdate('c');
                }
                unset($current[self::WELL_KNOWN_SETTING], $current[self::ACTIVATION_ID_SETTING]);
                return $current;
            },
            $lease_token
        );
        if (! $stored) {
            if (! $this->owns_attempt($activation_id, $lease_token)) {
                return new \WP_REST_Response(['state' => 'activation_superseded'], 200);
            }
            return new \WP_Error('credential_store_failed', __('Could not store the delivered credential.', 'encypher-provenance'), ['status' => 500]);
        }

        $pending = $this->pending_for_activation($activation_id);
        if (null === $pending || ! $this->owns_attempt($activation_id, $lease_token)) {
            return new \WP_REST_Response(['state' => 'activation_superseded'], 200);
        }
        $connected = $pending;
        $connected['state'] = 'site_connected_unclaimed';
        $connected['organization_id'] = isset($body['organization_id']) ? sanitize_text_field((string) $body['organization_id']) : '';
        $connected['site_id'] = isset($body['site_id']) ? sanitize_text_field((string) $body['site_id']) : '';
        $connected['completed_at'] = time();
        if (! $this->compare_and_swap_option(self::PENDING_OPTION, $pending, $connected)) {
            $this->cleanup_delivery($activation_id, $lease_token);
            return new \WP_REST_Response(['state' => 'activation_superseded'], 200);
        }

        if ($this->owns_attempt($activation_id, $lease_token)) {
            $prov_id = isset($body['provisioning_id']) ? (string) $body['provisioning_id'] : $challenge_id;
            $this->post_json(
                $origin . '/api/v2/integrations/wordpress/provision/' . rawurlencode($prov_id) . '/ack',
                new \stdClass(),
                sanitize_text_field((string) $api_key),
                2
            );
        }
        $this->cleanup_delivery($activation_id, $lease_token);

        return new \WP_REST_Response([
            'state' => 'site_connected_unclaimed',
            'organization_id' => $connected['organization_id'],
            'site_id' => $connected['site_id'],
            'host' => (string) ($connected['host'] ?? ''),
        ], 200);
    }

    private function pending_for_activation(string $activation_id): ?array
    {
        $pending = get_option(self::PENDING_OPTION, []);
        if (! is_array($pending) || $activation_id !== (string) ($pending['activation_id'] ?? '')) {
            return null;
        }
        return $pending;
    }

    private function acquire_start_lease(): string
    {
        $token = bin2hex(random_bytes(16));
        $lease = [
            'token' => $token,
            'expires_at' => time() + self::START_LEASE_SECONDS,
        ];
        if (add_option(self::START_LEASE_OPTION, $lease, '', false)) {
            return $token;
        }
        $existing = get_option(self::START_LEASE_OPTION, []);
        if (is_array($existing) && (int) ($existing['expires_at'] ?? 0) < time()) {
            $this->delete_option_if_value(self::START_LEASE_OPTION, $existing);
            if (add_option(self::START_LEASE_OPTION, $lease, '', false)) {
                return $token;
            }
        }
        return '';
    }

    private function owns_named_lease(string $option, string $token): bool
    {
        $lease = get_option($option, []);
        return is_array($lease)
            && $token === (string) ($lease['token'] ?? '')
            && (int) ($lease['expires_at'] ?? 0) >= time();
    }

    private function acquire_attempt_lease(string $activation_id): string
    {
        if (null === $this->pending_for_activation($activation_id)) {
            return '';
        }
        $token = bin2hex(random_bytes(16));
        $lease = [
            'activation_id' => $activation_id,
            'token' => $token,
            'expires_at' => time() + self::COMPLETION_LEASE_SECONDS,
        ];
        if (! add_option(self::COMPLETION_LEASE_OPTION, $lease, '', false)) {
            $existing = get_option(self::COMPLETION_LEASE_OPTION, []);
            if (
                is_array($existing)
                && (
                    $activation_id !== (string) ($existing['activation_id'] ?? '')
                    || (int) ($existing['expires_at'] ?? 0) < time()
                )
            ) {
                $this->delete_option_if_value(self::COMPLETION_LEASE_OPTION, $existing);
            }
            if (! add_option(self::COMPLETION_LEASE_OPTION, $lease, '', false)) {
                return '';
            }
        }
        if (null === $this->pending_for_activation($activation_id)) {
            $this->release_named_lease(self::COMPLETION_LEASE_OPTION, $token);
            return '';
        }
        return $token;
    }

    private function owns_attempt(string $activation_id, string $lease_token): bool
    {
        if (null === $this->pending_for_activation($activation_id)) {
            return false;
        }
        $lease = get_option(self::COMPLETION_LEASE_OPTION, []);
        return is_array($lease)
            && $activation_id === (string) ($lease['activation_id'] ?? '')
            && $lease_token === (string) ($lease['token'] ?? '')
            && (int) ($lease['expires_at'] ?? 0) >= time();
    }

    private function has_active_attempt_lease(string $activation_id): bool
    {
        $lease = get_option(self::COMPLETION_LEASE_OPTION, []);
        return is_array($lease)
            && $activation_id === (string) ($lease['activation_id'] ?? '')
            && (int) ($lease['expires_at'] ?? 0) >= time();
    }

    private function release_attempt_lease(string $activation_id, string $lease_token): void
    {
        $lease = get_option(self::COMPLETION_LEASE_OPTION, []);
        if (
            is_array($lease)
            && $activation_id === (string) ($lease['activation_id'] ?? '')
            && $lease_token === (string) ($lease['token'] ?? '')
        ) {
            $this->delete_option_if_value(self::COMPLETION_LEASE_OPTION, $lease);
        }
    }

    private function release_named_lease(string $option, string $token): void
    {
        $lease = get_option($option, []);
        if (is_array($lease) && $token === (string) ($lease['token'] ?? '')) {
            $this->delete_option_if_value($option, $lease);
        }
    }

    private function cleanup_delivery(string $activation_id, string $lease_token = ''): void
    {
        if ('' !== $lease_token && ! $this->owns_attempt($activation_id, $lease_token)) {
            return;
        }
        if (null === $this->pending_for_activation($activation_id)) {
            return;
        }
        $delivery = get_option(self::DELIVERY_KEY_OPTION, []);
        if (is_array($delivery) && $activation_id === (string) ($delivery['activation_id'] ?? '')) {
            $this->delete_option_if_value(self::DELIVERY_KEY_OPTION, $delivery);
        }
    }

    private function clear_activation_settings(string $activation_id, string $lease_token = ''): void
    {
        if ('' !== $lease_token && ! $this->owns_attempt($activation_id, $lease_token)) {
            return;
        }
        $this->update_settings_for_activation(
            $activation_id,
            function (array $current): array {
                unset($current[self::WELL_KNOWN_SETTING], $current[self::ACTIVATION_ID_SETTING]);
                return $current;
            },
            $lease_token
        );
    }

    private function clear_activation_settings_at_start(string $activation_id): void
    {
        for ($attempt = 0; $attempt < 3; $attempt++) {
            $settings = get_option(self::SETTINGS_KEY, []);
            if (! is_array($settings) || $activation_id !== (string) ($settings[self::ACTIVATION_ID_SETTING] ?? '')) {
                return;
            }
            $replacement = $settings;
            unset($replacement[self::WELL_KNOWN_SETTING], $replacement[self::ACTIVATION_ID_SETTING]);
            if ($this->compare_and_swap_option(self::SETTINGS_KEY, $settings, $replacement)) {
                return;
            }
        }
    }


    private function cleanup_attempt(string $activation_id): void
    {
        $pending = $this->pending_for_activation($activation_id);
        if (null === $pending) {
            return;
        }
        $this->clear_activation_settings($activation_id);
        $this->cleanup_delivery($activation_id);
        $current = $this->pending_for_activation($activation_id);
        if (null !== $current) {
            $this->delete_option_if_value(self::PENDING_OPTION, $current);
        }
    }

    private function update_settings_for_activation(string $activation_id, callable $mutate, string $lease_token = ''): bool
    {
        for ($attempt = 0; $attempt < 3; $attempt++) {
            if (null === $this->pending_for_activation($activation_id)) {
                return false;
            }
            if ('' !== $lease_token && ! $this->owns_attempt($activation_id, $lease_token)) {
                return false;
            }
            $settings = get_option(self::SETTINGS_KEY, []);
            if (! is_array($settings) || $activation_id !== (string) ($settings[self::ACTIVATION_ID_SETTING] ?? '')) {
                return false;
            }
            $replacement = $mutate($settings);
            if (! is_array($replacement)) {
                return false;
            }
            if ($this->compare_and_swap_option(self::SETTINGS_KEY, $settings, $replacement)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Atomically replace one exact serialized option generation.
     */
    private function compare_and_swap_option(string $option, $expected, $replacement): bool
    {
        global $wpdb;
        if (! is_object($wpdb) || ! isset($wpdb->options) || ! method_exists($wpdb, 'prepare') || ! method_exists($wpdb, 'query')) {
            return false;
        }
        $sql = $wpdb->prepare(
            "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s",
            maybe_serialize($replacement),
            $option,
            maybe_serialize($expected)
        );
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- $sql is prepared above; exact-value CAS is unavailable through the options API; cache is invalidated below.
        $updated = $wpdb->query($sql);
        if (1 !== $updated) {
            $this->clear_option_cache($option);
            return false;
        }
        $this->clear_option_cache($option);
        return true;
    }

    /**
     * Delete only the exact serialized option generation still owned.
     */
    private function delete_option_if_value(string $option, $expected): bool
    {
        global $wpdb;
        if (! is_object($wpdb) || ! isset($wpdb->options) || ! method_exists($wpdb, 'prepare') || ! method_exists($wpdb, 'query')) {
            return false;
        }
        $sql = $wpdb->prepare(
            "DELETE FROM {$wpdb->options} WHERE option_name = %s AND BINARY option_value = BINARY %s",
            $option,
            maybe_serialize($expected)
        );
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- $sql is prepared above; exact-value delete is unavailable through the options API; cache is invalidated below.
        $deleted = $wpdb->query($sql);
        if (1 !== $deleted) {
            return false;
        }
        $this->clear_option_cache($option);
        return true;
    }

    private function clear_option_cache(string $option): void
    {
        wp_cache_delete($option, 'options');
        wp_cache_delete('alloptions', 'options');
        wp_cache_delete('notoptions', 'options');
    }

    private function msg($body, string $fallback): string
    {
        if (is_array($body)) {
            if (isset($body['detail']['message']) && is_string($body['detail']['message'])) {
                return (string) $body['detail']['message'];
            }
            if (isset($body['detail']) && is_string($body['detail'])) {
                return (string) $body['detail'];
            }
            if (isset($body['message']) && is_string($body['message'])) {
                return (string) $body['message'];
            }
        }
        return $fallback;
    }

    /**
     * @return array{0:int,1:array}|\WP_Error
     */
    private function post_json(string $url, $payload, ?string $bearer, int $timeout = 20)
    {
        $headers = [
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ];
        if (null !== $bearer && '' !== $bearer) {
            $headers['Authorization'] = 'Bearer ' . $bearer;
        }
        $response = wp_remote_post($url, [
            'headers' => $headers,
            // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout -- Explicit bounded admin activation request.
            'timeout' => max(1, $timeout),
            'redirection' => 0,
            'body' => wp_json_encode($payload),
        ]);
        if (is_wp_error($response)) {
            return new \WP_Error('request_failed', $response->get_error_message(), ['status' => 502]);
        }
        $code = (int) wp_remote_retrieve_response_code($response);
        $decoded = json_decode(wp_remote_retrieve_body($response), true);
        return [$code, is_array($decoded) ? $decoded : []];
    }
}
