<?php

/**
 * Lead Connector Migrator — Secure Access Link
 *
 * Manages temporary "LeadConnector expert" administrator accounts and a one-time
 * encrypted auto-login URL. The raw token NEVER leaves the server: the URL
 * Flow:
 * 1. Generate token A, store server-side only.
 * 2. Encrypt token A with the bundled public key; store ciphertext in options.
 * 3. FE shows a short REST URL: /wp-json/leadconnector-token/secure/{token}
 * 4. Expert backend GETs that URL → plain-text base64 RSA ciphertext in the body.
 *    Metadata uses the same access key on a sibling route with hybrid encryption
 *    (AES-256-GCM + RSA-wrapped key): GET /wp-json/leadconnector-token/meta/{token}
 * 5. Expert decrypts with the private key → recover token A and metadata JSON.
 * 6. Expert hits the site login URL with ?lc_sal=tokenA (plain); we compare
 *    and log in, then redirect to the LC Migrator admin screen (respecting
 *    custom login / wp-admin URL plugins).
 */

namespace Lcmigrator\LcMigrator\SecureAccess;

use Lcmigrator\LcMigrator\LcQaMode;
use Lcmigrator\LcMigrator\Logger\Logger;
use Lcmigrator\LcMigrator\OptionManager\OptionManager;

class SecureAccessManager
{
    const OPTION_NAME = 'lc_migrator_secure_access';
    const RECORD_KEY = 'record';
    const CRON_HOOK = 'lc_migrator_secure_access_expire';
    const USER_META_MANAGED = 'lc_secure_access_managed';
    const USER_META_TOKEN = 'lc_secure_access_token_hash';
    const LOGIN_QUERY_PARAM = 'lc_sal';
    const REST_NAMESPACE = 'leadconnector-token';
    const REST_ROUTE = 'secure';
    const REST_META_ROUTE = 'meta';
    const EXPERT_ACCESS_HEADER = 'x_lc_secure_link';
    const EXPERT_ACCESS_HEADER_VALUE = 'leadconnector';
    /** Query fallback when a host strips custom X- headers before PHP. */
    const EXPERT_ACCESS_QUERY_PARAM = 'lc_secure_link';
    /** LiteSpeed tag used to mark secure-access responses as non-cacheable. */
    const LITESPEED_NOCACHE_TAG = 'lc_migrator_secure_access';
    /** Support engineers are based in India — weekend rule uses IST. */
    const SUPPORT_ENGINEER_TIMEZONE = 'Asia/Kolkata';
    const DEFAULT_DURATION_HOURS = 72;
    const WEEKEND_DEFAULT_DURATION_HOURS = 96;
    const WEEKEND_MIN_DURATION_HOURS = 96;

    /**
     * Common English first names — used to seed the "LeadConnector expert" identity.
     * Kept generic on purpose; nothing here is PII.
     */
    const FIRST_NAMES = [
        'LeadConnector'
    ];

    const LAST_NAMES = [
        'Expert'
    ];

    /** @var OptionManager */
    private $store;

    /** @var SecureAccessTokenCipher */
    private $cipher;

    public function __construct()
    {
        $this->store = OptionManager::getInstance(self::OPTION_NAME);
        $this->cipher = new SecureAccessTokenCipher();
    }

    /**
     * Resolved login page URL (honours rename-wp-login / hide-login plugins).
     */
    public static function getLoginPageUrl(): string
    {
        return wp_login_url();
    }

    /**
     * Full login URL with the plain secure-access token in the query string.
     */
    public static function buildLoginUrlWithToken(string $token): string
    {
        return add_query_arg(self::LOGIN_QUERY_PARAM, $token, self::getLoginPageUrl());
    }

    /**
     * Post-login destination inside wp-admin (honours custom wp-admin paths).
     */
    public static function getMigratorAdminUrl(): string
    {
        return admin_url('admin.php?page=lc-migrator');
    }

    /**
     * True when the current HTTP request targets the site's login page,
     * including custom login slugs from security plugins.
     */
    public static function isLoginPageRequest(): bool
    {
        global $pagenow;

        if (isset($pagenow) && $pagenow === 'wp-login.php') {
            return true;
        }

        $loginPath = wp_parse_url(self::getLoginPageUrl(), PHP_URL_PATH);
        $requestUri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
        $requestPath = wp_parse_url($requestUri, PHP_URL_PATH);

        if (!is_string($loginPath) || !is_string($requestPath) || $loginPath === '' || $requestPath === '') {
            return false;
        }

        return untrailingslashit($loginPath) === untrailingslashit($requestPath);
    }

    /**
     * Register hooks that prevent LiteSpeed / QUIC.cloud from caching the public
     * secure-access REST routes (browser HTML vs expert ciphertext share one URL).
     */
    public static function registerPublicRouteCacheBypassHooks(): void
    {
        add_action('plugins_loaded', [self::class, 'bootstrapPublicRouteCacheBypass'], 0);
        add_action('init', [self::class, 'bootstrapPublicRouteCacheBypass'], 0);
        add_action('litespeed_init', [self::class, 'bootstrapPublicRouteCacheBypass'], 0);
        add_filter('litespeed_control_cacheable', [self::class, 'filterLiteSpeedCacheable']);
        add_action('litespeed_control_finalize', [self::class, 'finalizeLiteSpeedNoCache'], 0);
    }

    /**
     * Early request hook: disable page cache before LiteSpeed decides cacheability.
     */
    public static function bootstrapPublicRouteCacheBypass(): void
    {
        if (!self::isPublicSecureAccessHttpRequest()) {
            return;
        }

        self::disableExternalPageCache();
    }

    /**
     * @param bool $cacheable
     * @return bool
     */
    public static function filterLiteSpeedCacheable($cacheable)
    {
        if (self::isPublicSecureAccessHttpRequest()) {
            return false;
        }

        return (bool) $cacheable;
    }

    /**
     * Last-chance LiteSpeed override before response headers are sent.
     */
    public static function finalizeLiteSpeedNoCache(): void
    {
        if (!self::isPublicSecureAccessHttpRequest()) {
            return;
        }

        if (defined('LSCWP_V')) {
            do_action('litespeed_control_set_nocache', 'lc-migrator-secure-access');
        }
    }

    /**
     * True when the incoming HTTP request targets a public secure-access REST URL.
     */
    public static function isPublicSecureAccessHttpRequest(): bool
    {
        $uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
        if (!is_string($uri) || $uri === '') {
            return false;
        }

        $restPrefix = function_exists('rest_get_url_prefix') ? rest_get_url_prefix() : 'wp-json';
        $pattern = '#/' . preg_quote($restPrefix, '#') . '/'
            . preg_quote(self::REST_NAMESPACE, '#') . '/'
            . '(?:' . preg_quote(self::REST_ROUTE, '#') . '|' . preg_quote(self::REST_META_ROUTE, '#') . ')'
            . '/[a-zA-Z0-9_-]+#i';

        return (bool) preg_match($pattern, $uri);
    }

    /**
     * Tell LiteSpeed, QUIC.cloud, and other full-page caches not to store this response.
     */
    public static function disableExternalPageCache(): void
    {
        if (!defined('DONOTCACHEPAGE')) {
            define('DONOTCACHEPAGE', true);
        }
        if (!defined('LSCACHE_NO_CACHE')) {
            define('LSCACHE_NO_CACHE', true);
        }

        if (defined('LSCWP_V')) {
            do_action('litespeed_control_set_nocache', 'lc-migrator-secure-access');
        }

        if (headers_sent()) {
            return;
        }

        nocache_headers();
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, private');
        header('Pragma: no-cache');
        header('Expires: 0');
        header('Surrogate-Control: no-store');
        header('CDN-Cache-Control: no-cache');
        header('Vary: X-LC-Secure-Link');
        header('X-LiteSpeed-Cache-Control: no-cache, no-store, esi=off');
        header('X-LiteSpeed-Tag: ' . self::LITESPEED_NOCACHE_TAG);
    }

    // ------------------------------------------------------------------
    // Public API (called from the REST proxy and the auto-login flow)
    // ------------------------------------------------------------------

    /**
     * Issue a fresh secure-access link. If a record already exists it is
     * revoked first, which is exactly what "Regenerate" wants.
     *
     * @param int $requestedHours raw value chosen by the admin in the UI.
     * @return array sanitized record (no raw token, with `url` when active).
     */
    public function generate(int $requestedHours): array
    {
        $this->revokeInternal();

        if ($requestedHours < 1) {
            $requestedHours = $this->isSupportEngineerWeekend()
                ? self::WEEKEND_DEFAULT_DURATION_HOURS
                : self::DEFAULT_DURATION_HOURS;
        }

        list($hours, $weekendExtended) = $this->applyWeekendRule($requestedHours);

        $first = self::FIRST_NAMES[array_rand(self::FIRST_NAMES)];
        $last = self::LAST_NAMES[array_rand(self::LAST_NAMES)];
        $suffix = wp_generate_password(6, false);
        $slug = strtolower($first) . '_' . strtolower($last) . '_' . strtolower($suffix);
        $login = 'lc_expert_' . $slug;

        $host = wp_parse_url(home_url(), PHP_URL_HOST) ?: 'localhost';
        $email = $slug . '@' . $host;
        $pass = wp_generate_password(24, true, true);

        $userId = wp_insert_user([
            'user_login' => $login,
            'user_pass' => $pass,
            'user_email' => $email,
            'first_name' => $first,
            'last_name' => $last,
            'display_name' => $first . ' ' . $last,
            'role' => 'administrator',
        ]);

        if (is_wp_error($userId)) {
            Logger::getInstance()->error('SecureAccessManager: failed to create user — ' . $userId->get_error_message());
            return [
                'status' => 'none',
                'error' => 'Failed to create secure-access user: ' . $userId->get_error_message(),
            ];
        }

        $rawToken = bin2hex(random_bytes(16));
        add_user_meta($userId, self::USER_META_MANAGED, 1, true);
        add_user_meta($userId, self::USER_META_TOKEN, hash('sha256', $rawToken), true);

        $encryptedToken = $this->cipher->encrypt($rawToken);
        if ($encryptedToken === false || $encryptedToken === '') {
            $this->deleteManagedUser((int) $userId);
            Logger::getInstance()->error('SecureAccessManager: public-key token encryption failed');
            return [
                'status' => 'none',
                'error' => 'Failed to encrypt secure-access token.',
            ];
        }

        // Short public REST URL; ciphertext is fetched from the open route by key.
        $accessKey = wp_generate_password(16, false, false);
        $url = rest_url(self::REST_NAMESPACE . '/' . self::REST_ROUTE . '/' . $accessKey);
        $urlMasked = $host . '/wp-json/' . self::REST_NAMESPACE . '/' . self::REST_ROUTE . '/'
            . substr($accessKey, 0, 6) . '...';

        $now = time();
        $expiresAt = $now + $this->durationUnitsToSeconds($hours);

        $metaPayload = wp_json_encode([
            'created_at' => $now,
            'expires_at' => $expiresAt,
            'login_url_base' => self::getLoginPageUrl(),
            'redirect_url' => self::getMigratorAdminUrl(),
        ]);
        if (!is_string($metaPayload) || $metaPayload === '') {
            $this->deleteManagedUser((int) $userId);
            Logger::getInstance()->error('SecureAccessManager: metadata JSON encode failed');
            return [
                'status' => 'none',
                'error' => 'Failed to prepare secure-access metadata.',
            ];
        }

        $encryptedMeta = $this->cipher->encryptPayload($metaPayload);
        if ($encryptedMeta === false || $encryptedMeta === '') {
            $this->deleteManagedUser((int) $userId);
            Logger::getInstance()->error('SecureAccessManager: metadata encryption failed');
            return [
                'status' => 'none',
                'error' => 'Failed to encrypt secure-access metadata.',
            ];
        }

        $record = [
            'token' => $rawToken,
            'access_key' => $accessKey,
            'encrypted_token' => $encryptedToken,
            'encrypted_meta' => $encryptedMeta,
            'user_id' => (int) $userId,
            'user_login' => $login,
            'first_name' => $first,
            'last_name' => $last,
            'url' => $url,
            'url_masked' => $urlMasked,
            'login_url_base' => self::getLoginPageUrl(),
            'redirect_url' => self::getMigratorAdminUrl(),
            'duration_hours' => $hours,
            'requested_hours' => $requestedHours,
            'weekend_extended' => $weekendExtended,
            'created_at' => $now,
            'expires_at' => $expiresAt,
            'revoked_at' => null,
            'status' => 'active',
        ];

        $this->store->set(self::RECORD_KEY, $record);

        wp_clear_scheduled_hook(self::CRON_HOOK);
        wp_schedule_single_event($expiresAt, self::CRON_HOOK, [$rawToken]);

        return $this->sanitize($record);
    }

    /**
     * Return the current record, with lazy `active → expired` transition
     * when `expires_at` has passed (covers DISABLE_WP_CRON) and lazy
     * `active → revoked` when the WP user was deleted out of band.
     */
    public function getCurrent(): array
    {
        $record = $this->store->get(self::RECORD_KEY);
        if (!is_array($record) || empty($record)) {
            return ['status' => 'none'];
        }

        if (($record['status'] ?? '') === 'active') {
            $userId = (int) ($record['user_id'] ?? 0);
            $userGone = $userId && (get_userdata($userId) === false);

            if ($userGone) {
                $record = $this->markRevoked($record);
            } elseif (!empty($record['expires_at']) && (int) $record['expires_at'] <= time()) {
                $record = $this->markExpired($record);
            }
        }

        return $this->sanitize($record);
    }

    /**
     * Admin-initiated revoke. No-op when nothing exists or already revoked.
     */
    public function revoke(): array
    {
        $record = $this->store->get(self::RECORD_KEY);
        if (!is_array($record) || empty($record)) {
            return ['status' => 'none'];
        }

        if (($record['status'] ?? '') === 'active') {
            $record = $this->markRevoked($record);
        }

        return $this->sanitize($record);
    }

    /**
     * WP-Cron callback. Stale events from a previous link no-op so they
     * cannot kill a freshly regenerated link.
     */
    public function handleExpire(string $tokenArg): void
    {
        $record = $this->store->get(self::RECORD_KEY);
        if (!is_array($record) || empty($record)) {
            return;
        }
        if (($record['status'] ?? '') !== 'active') {
            return;
        }
        if (empty($record['token']) || !is_string($record['token'])) {
            return;
        }
        if (!hash_equals((string) $record['token'], (string) $tokenArg)) {
            return;
        }

        $this->markExpired($record);
    }

    /**
     * Public REST handler: GET /wp-json/leadconnector-token/secure/{token}
     * Returns the RSA-encrypted token as plain text (base64) for expert backends.
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public static function restResolveEncryptedToken($request)
    {
        try {
            $token = is_object($request) && method_exists($request, 'get_param')
                ? (string) $request->get_param('token')
                : '';

            return (new self())->resolveEncryptedToken($token, $request);
        } catch (\Throwable $e) {
            Logger::getInstance()->error('SecureAccessManager::restResolveEncryptedToken: ' . $e->getMessage());
            return new \WP_Error(
                'secure_access_error',
                __('Secure access link could not be resolved.', 'lc-migrator'),
                ['status' => 500]
            );
        }
    }

    /**
     * Public REST handler: GET /wp-json/leadconnector-token/meta/{token}
     * Returns hybrid-encrypted metadata as plain text for expert backends.
     * Uses the same access key as the encrypted-token route.
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response|\WP_Error
     */
    public static function restResolvePublicMeta($request)
    {
        try {
            $token = is_object($request) && method_exists($request, 'get_param')
                ? (string) $request->get_param('token')
                : '';

            return (new self())->resolvePublicMeta($token, $request);
        } catch (\Throwable $e) {
            Logger::getInstance()->error('SecureAccessManager::restResolvePublicMeta: ' . $e->getMessage());
            return new \WP_Error(
                'secure_access_error',
                __('Secure access metadata could not be resolved.', 'lc-migrator'),
                ['status' => 500]
            );
        }
    }

    /**
     * @param \WP_REST_Request|null $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function resolvePublicMeta(string $token, $request = null)
    {
        self::sendPublicRouteNocacheHeaders();

        $record = $this->resolveActiveRecordByAccessKey($token);
        if (is_wp_error($record)) {
            return $record;
        }

        if (!self::hasExpertAccessHeader($request)) {
            return self::buildBrowserValidLinkResponse();
        }

        $encrypted = (string) ($record['encrypted_meta'] ?? '');
        if ($encrypted === '') {
            return new \WP_Error(
                'secure_access_unavailable',
                __('This secure access metadata is unavailable.', 'lc-migrator'),
                ['status' => 410]
            );
        }

        $response = new \WP_REST_Response($encrypted, 200);
        $response->header('Content-Type', 'text/plain; charset=utf-8');
        self::applyPublicRouteResponseHeaders($response, 'expert');
        return $response;
    }

    /**
     * @param \WP_REST_Request|null $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function resolveEncryptedToken(string $token, $request = null)
    {
        self::sendPublicRouteNocacheHeaders();

        $record = $this->resolveActiveRecordByAccessKey($token);
        if (is_wp_error($record)) {
            return $record;
        }

        if (!self::hasExpertAccessHeader($request)) {
            return self::buildBrowserValidLinkResponse();
        }

        $encrypted = (string) ($record['encrypted_token'] ?? '');
        if ($encrypted === '') {
            return new \WP_Error(
                'secure_access_unavailable',
                __('This secure access link is unavailable.', 'lc-migrator'),
                ['status' => 410]
            );
        }

        $response = new \WP_REST_Response($encrypted, 200);
        $response->header('Content-Type', 'text/plain; charset=utf-8');
        self::applyPublicRouteResponseHeaders($response, 'expert');
        $response->header('X-LC-Secure-Access-Login-Base', self::getLoginPageUrl());
        $response->header('X-LC-Secure-Access-Redirect-Url', self::getMigratorAdminUrl());
        return $response;
    }

    /**
     * @param \WP_REST_Request|null $request
     */
    private static function hasExpertAccessHeader($request): bool
    {
        return self::readExpertAccessHeaderValue($request) !== null;
    }

    /**
     * Resolve the expert handoff header from every source PHP/WordPress may expose.
     *
     * @param \WP_REST_Request|null $request
     */
    private static function readExpertAccessHeaderValue($request): ?string
    {
        $expected = strtolower(self::EXPERT_ACCESS_HEADER_VALUE);

        foreach (self::collectExpertAccessCredentialValues($request) as $candidate) {
            $actual = strtolower(trim((string) $candidate));
            if ($actual !== '' && hash_equals($expected, $actual)) {
                return (string) $candidate;
            }
        }

        return null;
    }

    /**
     * @param \WP_REST_Request|null $request
     * @return string[]
     */
    private static function collectExpertAccessCredentialValues($request): array
    {
        $values = [];

        $push = static function ($value) use (&$values) {
            if (is_array($value)) {
                foreach ($value as $item) {
                    if (is_string($item) && trim($item) !== '') {
                        $values[] = trim($item);
                    }
                }
                return;
            }

            if (is_string($value) && trim($value) !== '') {
                $values[] = trim($value);
            }
        };

        foreach (['HTTP_X_LC_SECURE_LINK', 'REDIRECT_HTTP_X_LC_SECURE_LINK'] as $serverKey) {
            if (!empty($_SERVER[$serverKey])) {
                $push(wp_unslash($_SERVER[$serverKey]));
            }
        }

        foreach ($_SERVER as $name => $value) {
            if (!is_string($name) || strpos($name, 'HTTP_') !== 0) {
                continue;
            }
            if (self::canonicalHeaderName(substr($name, 5)) === self::EXPERT_ACCESS_HEADER) {
                $push(wp_unslash($value));
            }
        }

        $headerSources = [];
        if (function_exists('getallheaders')) {
            $headerSources[] = getallheaders();
        }
        if (function_exists('apache_request_headers')) {
            $headerSources[] = apache_request_headers();
        }

        foreach ($headerSources as $all) {
            if (!is_array($all)) {
                continue;
            }
            foreach ($all as $name => $headerValue) {
                if (self::canonicalHeaderName((string) $name) === self::EXPERT_ACCESS_HEADER) {
                    $push($headerValue);
                }
            }
        }

        if ($request instanceof \WP_REST_Request) {
            foreach (array('x-lc-secure-link', 'x_lc_secure_link', 'X-LC-Secure-Link') as $headerName) {
                $push($request->get_header($headerName));
            }

            foreach ((array) $request->get_headers() as $name => $headerValues) {
                if (self::canonicalHeaderName((string) $name) !== self::EXPERT_ACCESS_HEADER) {
                    continue;
                }
                $push($headerValues);
            }

            $push($request->get_param(self::EXPERT_ACCESS_QUERY_PARAM));
        }

        if (!empty($_GET[self::EXPERT_ACCESS_QUERY_PARAM])) {
            $push(wp_unslash($_GET[self::EXPERT_ACCESS_QUERY_PARAM]));
        }

        return array_values(array_unique($values));
    }

    /**
     * @param \WP_REST_Response $response
     * @param string            $mode `expert` or `browser`
     * @return \WP_REST_Response
     */
    private static function applyPublicRouteResponseHeaders($response, string $mode)
    {
        $response->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, private');
        $response->header('Pragma', 'no-cache');
        $response->header('Expires', '0');
        $response->header('Vary', 'X-LC-Secure-Link');
        $response->header('X-LiteSpeed-Cache-Control', 'no-cache, no-store, esi=off');
        $response->header('X-LiteSpeed-Tag', self::LITESPEED_NOCACHE_TAG);
        $response->header('Surrogate-Control', 'no-store');
        $response->header('CDN-Cache-Control', 'no-cache');
        $response->header('X-LC-Secure-Access-Mode', $mode);
        return $response;
    }

    /**
     * Send cache-busting headers before the REST body (LiteSpeed / CDN layers).
     */
    public static function sendPublicRouteNocacheHeaders(): void
    {
        self::disableExternalPageCache();
    }

    private static function canonicalHeaderName(string $name): string
    {
        return str_replace('-', '_', strtolower($name));
    }

    /**
     * @return \WP_REST_Response
     */
    private static function buildBrowserValidLinkResponse()
    {
        $title = esc_html__('LeadConnector Link', 'lc-migrator');
        $heading = esc_html__('Your LeadConnector Magic link is valid', 'lc-migrator');
        $lead = esc_html__(
            'Please send this full page URL to your LeadConnector expert so they can access your site and help you with migration or support.',
            'lc-migrator'
        );
        $note = esc_html__(
            'Keep this link private. It expires automatically when the access period ends or if you revoke it from your WordPress admin.',
            'lc-migrator'
        );

        $html = '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>' . $title . '</title>
<style>
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:#f4f6f8;color:#1f2937;line-height:1.5}
.wrap{max-width:560px;margin:48px auto;padding:0 20px}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:32px;box-shadow:0 8px 24px rgba(15,23,42,.06)}
.brand{display:block;margin-bottom:8px;font-size:14px;font-weight:600;color:#155dfc;letter-spacing:.01em}
h1{margin:0 0 12px;font-size:24px;line-height:1.3}
p{margin:0 0 14px;font-size:16px;color:#4b5563}
p:last-child{margin-bottom:0}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>' . $heading . '</h1>
<p>' . $lead . '</p>
<p>' . $note . '</p>
</div>
</div>
</body>
</html>';

        $response = new \WP_REST_Response($html, 200);
        $response->header('Content-Type', 'text/html; charset=utf-8');
        self::applyPublicRouteResponseHeaders($response, 'browser');
        return $response;
    }

    /**
     * Shared lookup for open REST routes keyed by the public access token.
     *
     * @return array|\WP_Error
     */
    private function resolveActiveRecordByAccessKey(string $token)
    {
        if ($token === '' || !preg_match('/^[a-zA-Z0-9_-]{8,64}$/', $token)) {
            return new \WP_Error(
                'secure_access_invalid',
                __('This secure access link is invalid.', 'lc-migrator'),
                ['status' => 404]
            );
        }

        $record = $this->store->get(self::RECORD_KEY);
        if (!is_array($record) || empty($record)) {
            return new \WP_Error(
                'secure_access_not_found',
                __('This secure access link has expired or been revoked.', 'lc-migrator'),
                ['status' => 410]
            );
        }

        $status = (string) ($record['status'] ?? '');
        if ($status !== 'active') {
            return new \WP_Error(
                'secure_access_inactive',
                __('This secure access link has expired or been revoked.', 'lc-migrator'),
                ['status' => 410]
            );
        }

        $storedKey = (string) ($record['access_key'] ?? '');
        if ($storedKey === '' || !hash_equals($storedKey, $token)) {
            return new \WP_Error(
                'secure_access_invalid',
                __('This secure access link is invalid.', 'lc-migrator'),
                ['status' => 404]
            );
        }

        if (!empty($record['expires_at']) && (int) $record['expires_at'] <= time()) {
            $this->markExpired($record);
            return new \WP_Error(
                'secure_access_expired',
                __('This secure access link has expired or been revoked.', 'lc-migrator'),
                ['status' => 410]
            );
        }

        return $record;
    }

    /**
     * WordPress REST JSON-encodes string bodies by default. Serve the encrypted
     * token as raw plain text for expert backends.
     *
     * @param bool              $served
     * @param \WP_HTTP_Response $result
     * @param \WP_REST_Request  $request
     * @param \WP_REST_Server   $server
     * @return bool
     */
    public static function servePlainTextSecureToken($served, $result, $request, $server)
    {
        if ($served || !($request instanceof \WP_REST_Request)) {
            return $served;
        }

        $route = $request->get_route();
        if (!is_string($route) || !self::isPlainTextSecureAccessRoute($route)) {
            return $served;
        }

        if (!($result instanceof \WP_REST_Response) || $result->get_status() !== 200) {
            return $served;
        }

        $data = $result->get_data();
        if (!is_string($data) || $data === '') {
            return $served;
        }

        self::sendPublicRouteNocacheHeaders();
        status_header(200);
        foreach ($result->get_headers() as $name => $value) {
            foreach ((array) $value as $single) {
                header($name . ': ' . $single, false);
            }
        }

        echo $data;
        return true;
    }

    /**
     * @param string $route REST route path, e.g. /leadconnector-token/secure/abc
     */
    private static function isPlainTextSecureAccessRoute(string $route): bool
    {
        $prefix = '/' . self::REST_NAMESPACE . '/';
        if (strpos($route, $prefix) !== 0) {
            return false;
        }

        $suffix = substr($route, strlen($prefix));
        return strpos($suffix, self::REST_ROUTE . '/') === 0
            || strpos($suffix, self::REST_META_ROUTE . '/') === 0;
    }

    /**
     * `login_init` callback. Expert systems fetch + decrypt off-site, then
     * hit this endpoint with ?lc_sal=<plain token A>. We compare only — no
     * private-key decrypt on this request.
     */
    public function handleLoginIntercept(): void
    {
        if (empty($_GET[self::LOGIN_QUERY_PARAM])) {
            return;
        }

        $raw = is_string($_GET[self::LOGIN_QUERY_PARAM]) ? $_GET[self::LOGIN_QUERY_PARAM] : '';
        $incomingToken = rawurldecode($raw);

        $record = $this->store->get(self::RECORD_KEY);
        if (!is_array($record) || empty($record) || ($record['status'] ?? '') !== 'active' || empty($record['token'])) {
            wp_die(
                esc_html__('This secure access link has expired or been revoked.', 'lc-migrator'),
                esc_html__('Link expired', 'lc-migrator'),
                ['response' => 410]
            );
        }

        if (!hash_equals((string) $record['token'], (string) $incomingToken)) {
            wp_die(
                esc_html__('This secure access link is invalid.', 'lc-migrator'),
                esc_html__('Invalid link', 'lc-migrator'),
                ['response' => 410]
            );
        }

        if (!empty($record['expires_at']) && (int) $record['expires_at'] <= time()) {
            $this->markExpired($record);
            wp_die(
                esc_html__('This secure access link has expired or been revoked.', 'lc-migrator'),
                esc_html__('Link expired', 'lc-migrator'),
                ['response' => 410]
            );
        }

        $userId = (int) ($record['user_id'] ?? 0);
        $userdata = $userId ? get_userdata($userId) : false;
        if (!$userdata) {
            $this->markRevoked($record);
            wp_die(
                esc_html__('This secure access link has expired or been revoked.', 'lc-migrator'),
                esc_html__('Link expired', 'lc-migrator'),
                ['response' => 410]
            );
        }

        wp_clear_auth_cookie();
        wp_set_current_user($userId);
        wp_set_auth_cookie($userId, true);
        do_action('wp_login', $userdata->user_login, $userdata);
        wp_safe_redirect(self::getMigratorAdminUrl());
        exit;
    }

    // ------------------------------------------------------------------
    // Static thin wrappers — hooks register before the manager exists.
    // ------------------------------------------------------------------

    public static function handleExpireStatic($token): void
    {
        try {
            (new self())->handleExpire((string) $token);
        } catch (\Throwable $e) {
            Logger::getInstance()->error('SecureAccessManager::handleExpireStatic: ' . $e->getMessage());
        }
    }

    public static function handleLoginInterceptStatic(): void
    {
        if (empty($_GET[self::LOGIN_QUERY_PARAM])) {
            return;
        }
        try {
            (new self())->handleLoginIntercept();
        } catch (\Throwable $e) {
            Logger::getInstance()->error('SecureAccessManager::handleLoginInterceptStatic: ' . $e->getMessage());
            wp_die(
                esc_html__('This secure access link could not be processed.', 'lc-migrator'),
                esc_html__('Secure access error', 'lc-migrator'),
                ['response' => 500]
            );
        }
    }

    /**
     * Custom login slugs (rename-wp-login, etc.) may not load wp-login.php
     * before login_init runs — catch lc_sal on init for those routes.
     */
    public static function handleLoginInterceptStaticEarly(): void
    {
        if (empty($_GET[self::LOGIN_QUERY_PARAM])) {
            return;
        }

        global $pagenow;
        if (isset($pagenow) && $pagenow === 'wp-login.php') {
            return;
        }

        if (!self::isLoginPageRequest()) {
            return;
        }

        self::handleLoginInterceptStatic();
    }

    // ------------------------------------------------------------------
    // Internals
    // ------------------------------------------------------------------

    /**
     * Friday/Saturday/Sunday in India (IST) → minimum 96h so weekend
     * coverage holds even if the admin asked for less. Mirrors the UI guard
     * but is the source of truth (UI cannot bypass it).
     *
     * @return array{0:int,1:bool}
     */
    private function isSupportEngineerWeekend(): bool
    {
        $dow = (int) $this->nowInSupportEngineerTimezone()->format('N'); // 1=Mon … 7=Sun
        return in_array($dow, [5, 6, 7], true);
    }

    private function applyWeekendRule(int $requestedHours): array
    {
        if ($this->isSupportEngineerWeekend() && $requestedHours < self::WEEKEND_MIN_DURATION_HOURS) {
            return [self::WEEKEND_MIN_DURATION_HOURS, true];
        }
        return [$requestedHours, false];
    }

    private function durationUnitsToSeconds(int $units): int
    {
        if (LcQaMode::isEnabled()) {
            return $units * MINUTE_IN_SECONDS;
        }

        return $units * HOUR_IN_SECONDS;
    }

    /**
     * @return \DateTimeImmutable
     */
    private function nowInSupportEngineerTimezone()
    {
        return new \DateTimeImmutable('now', new \DateTimeZone(self::SUPPORT_ENGINEER_TIMEZONE));
    }

    /**
     * Called by `generate()` so a regenerate cleanly supersedes any prior
     * record (user deleted, cron cleared, status flipped to revoked).
     */
    private function revokeInternal(): void
    {
        $record = $this->store->get(self::RECORD_KEY);
        if (!is_array($record) || empty($record)) {
            return;
        }

        $this->deleteManagedUser((int) ($record['user_id'] ?? 0));
        $this->clearCron($record['token'] ?? null);
        $this->store->remove(self::RECORD_KEY);
    }

    private function markExpired(array $record): array
    {
        $this->deleteManagedUser((int) ($record['user_id'] ?? 0));
        $this->clearCron($record['token'] ?? null);

        $record['status'] = 'expired';
        if (empty($record['expires_at'])) {
            $record['expires_at'] = time();
        }
        $record['token'] = null;
        $record['access_key'] = null;
        $record['encrypted_token'] = null;
        $record['encrypted_meta'] = null;
        $record['url'] = null;

        $this->store->set(self::RECORD_KEY, $record);
        return $record;
    }

    private function markRevoked(array $record): array
    {
        $this->deleteManagedUser((int) ($record['user_id'] ?? 0));
        $this->clearCron($record['token'] ?? null);

        $record['status'] = 'revoked';
        $record['revoked_at'] = time();
        $record['token'] = null;
        $record['access_key'] = null;
        $record['encrypted_token'] = null;
        $record['encrypted_meta'] = null;
        $record['url'] = null;

        $this->store->set(self::RECORD_KEY, $record);
        return $record;
    }

    /**
     * Only ever deletes users that carry our managed flag. Belt-and-braces
     * against ever wiping a real WP admin.
     */
    private function deleteManagedUser(int $userId): void
    {
        if ($userId <= 0) {
            return;
        }
        $userdata = get_userdata($userId);
        if (!$userdata) {
            return;
        }
        if (!get_user_meta($userId, self::USER_META_MANAGED, true)) {
            return;
        }

        if (!function_exists('wp_delete_user')) {
            require_once ABSPATH . 'wp-admin/includes/user.php';
        }
        wp_delete_user($userId);
    }

    /**
     * Clear both the specific scheduled event (single-event APIs are picky
     * about arg matching) and any orphaned hook entries.
     */
    private function clearCron(?string $token): void
    {
        if (is_string($token) && $token !== '') {
            $ts = wp_next_scheduled(self::CRON_HOOK, [$token]);
            if ($ts) {
                wp_unschedule_event($ts, self::CRON_HOOK, [$token]);
            }
        }
        wp_clear_scheduled_hook(self::CRON_HOOK);
    }

    /**
     * Strip the raw token (always) and the live URL (when not active)
     * before returning a record to the front-end.
     */
    private function sanitize(array $record): array
    {
        unset($record['token'], $record['access_key'], $record['encrypted_token'], $record['encrypted_meta']);
        if (($record['status'] ?? '') !== 'active') {
            unset($record['url']);
        }
        return $record;
    }
}
