<?php
/**
 * Freemius Auto-Activation (CF17 — client side).
 *
 * Closes the gap from CF16: the SDK loads + operator opts in, but the
 * server never knows about this site. This class bridges that gap by
 * calling POST /v1/license/activate automatically after the SDK has
 * registered a license.
 *
 * Flow:
 *   1. Operator installs + activates plugin
 *   2. SDK loads, shows opt-in (or skips in whitelabel/dev mode)
 *   3. Once `swa_fs()->is_registered()` is true AND a license exists,
 *      we POST {site_url} to /v1/license/activate on the server
 *   4. Server returns {license_id, status: probation|active|...}
 *   5. We cache result in wp_options so subsequent heartbeats don't
 *      hammer the server
 *
 * Caching:
 *   - Successful activation cached for 6h (re-syncs on heartbeat)
 *   - Failed activation cached for 1h (retry sooner, but not constantly)
 *   - Manual `retry now` clears cache and re-runs immediately
 *
 * Error handling:
 *   - Network errors → transient, retry next heartbeat
 *   - 401 (bad license) → permanent, mark as `failed` and surface in UI
 *   - 429 (rate limit) → back off per server's reset_at header
 *
 * @package SwapAds\Client\License
 * @since   1.2.0
 */

declare(strict_types=1);

namespace SwapAds\Client\License;

use SwapAds\Client\Security\EncryptedOption;

use SwapAds\Client\Api\RestClient;

/**
 * Class FreemiusAutoActivator.
 *
 * @since 1.2.0
 */
final class FreemiusAutoActivator
{
    /**
     * WP option holding the last activation result.
     */
    public const OPTION_LAST_ACTIVATION = 'swapads_client_last_fs_activation';

    /**
     * WP option holding the last activation attempt timestamp.
     */
    public const OPTION_LAST_ATTEMPT_AT = 'swapads_client_last_fs_attempt_at';

    /**
     * WP option holding the cached activation status (active|failed|pending).
     */
    public const OPTION_STATUS = 'swapads_client_fs_activation_status';

    /**
     * WP option holding the server's shared HMAC secret (CF17.1).
     *
     * This is distinct from LicenseManager::OPTION_SECRET (which holds the
     * Freemius per-site license secret). The two are NEVER the same value:
     *   - Freemius license secret is per-site, given to the operator by Freemius.
     *   - Server HMAC secret is shared across the network, fetched from
     *     GET /wp-json/swapads-server/v1/public-key during auto-activation.
     *
     * Conflating them causes every HMAC signature to fail and the server
     * never learns about the operator's license (symptom: client appears
     * activated but server returns LICENSE_NOT_FOUND).
     */
    public const OPTION_SERVER_SECRET = 'swapads_client_server_hmac_secret';

    /**
     * Time (s) between successful re-syncs.
     */
    public const SUCCESS_CACHE_TTL = 6 * HOUR_IN_SECONDS;

    /**
     * Time (s) between failed retries.
     */
    public const FAILED_CACHE_TTL = HOUR_IN_SECONDS;

    /**
     * Register WordPress hooks.
     */
    public static function register(): void
    {
        // Plg-001 (2026-08-01): the previous implementation hooked a
        // fictitious custom action which the Freemius SDK never emits, so
        // auto-activation NEVER fired. The SDK does emit 'init' on every
        // request *after* plugins_loaded completes, so we hook 'init' at
        // priority 11 — AFTER WP core + our Plugin::init() — and gate on
        // sdkHasSite() (which checks for an opted-in Freemius site).
        add_action('init', [self::class, 'maybeAutoActivate'], 11);
        // Periodic re-sync (every 12h via cron).
        add_action('swapads_client_license_resync', [self::class, 'forceResync']);
        add_action('init', [self::class, 'scheduleResyncCron']);
    }

    /**
     * Schedule the resync cron if not already scheduled.
     */
    public static function scheduleResyncCron(): void
    {
        if (!wp_next_scheduled('swapads_client_license_resync')) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'twicedaily', 'swapads_client_license_resync');
        }
    }

    /**
     * Decide whether we should attempt auto-activation right now.
     *
     * Returns true when:
     *   - Freemius SDK is loaded
     *   - SDK reports an active license (operator has completed opt-in)
     *   - We have not attempted activation recently (cache TTL respected)
     *
     * @return bool
     */
    public static function shouldAttempt(): bool
    {
        if (!function_exists('swa_fs')) {
            return false;
        }
        if (!self::sdkHasLicense()) {
            return false;
        }
        $lastAttempt = (int) get_option(self::OPTION_LAST_ATTEMPT_AT, 0);
        $status = (string) get_option(self::OPTION_STATUS, '');
        $ttl = $status === 'active' ? self::SUCCESS_CACHE_TTL : self::FAILED_CACHE_TTL;
        return (time() - $lastAttempt) >= $ttl;
    }

    /**
     * Check whether the SDK currently has a license registered.
     *
     * Looks at swa_fs()->_get_license() and verifies id > 0 + has secret_key.
     *
     * @return bool
     */
    public static function sdkHasLicense(): bool
    {
        if (!function_exists('swa_fs')) {
            return false;
        }
        $sdk = swa_fs();
        if (!is_object($sdk) || !method_exists($sdk, '_get_license')) {
            return false;
        }
        $license = $sdk->_get_license();
        if (!is_object($license)) {
            return false;
        }
        $hasId = property_exists($license, 'id') && (int) $license->id > 0;
        $hasSecret = property_exists($license, 'secret_key') && $license->secret_key !== '';
        return $hasId && $hasSecret;
    }

    /**
     * CF17.1: Gate on Freemius site registration (free or paid plan).
     *
     * Distinct from sdkHasLicense(): free-plan installs (Y1) have no paid
     * license but DO have a registered site (FS_Site). We use FS_Site.secret_key
     * as the HMAC auth token for server calls.
     *
     * @since 1.3.0
     *
     * @return bool
     */
    public static function sdkHasSite(): bool
    {
        return LicenseManager::sdkHasSite();
    }

    /**
     * Hook: attempt auto-activation after SDK opt-in.
     */
    public static function maybeAutoActivate(): void
    {
        if (!self::shouldAttempt()) {
            return;
        }
        self::runActivation();
    }

    /**
     * Hook: forced re-sync (bypasses cache TTL).
     */
    public static function forceResync(): void
    {
        if (!self::sdkHasSite()) {
            return;
        }
        self::runActivation();
    }

    /**
     * Perform the actual auto-activation call to the server.
     *
     * Posts {site_url} to /swapads-server/v1/license/activate.
     * On success: caches status=active, license_id, expires_at.
     * On failure: caches status=failed, error_code, error_message.
     *
     * @return array<string, mixed> Result array with 'success' key.
     */
    public static function runActivation(): array
    {
        update_option(self::OPTION_LAST_ATTEMPT_AT, time());

        if (!self::sdkHasSite()) {
            $result = [
                'success' => false,
                'error_code' => 'NO_SDK_LICENSE',
                'message' => 'Freemius SDK has no registered site yet (opt-in not completed).',
            ];
            update_option(self::OPTION_LAST_ACTIVATION, $result);
            update_option(self::OPTION_STATUS, 'failed');
            return $result;
        }

        // CF17.1: if no server secret is stored yet (first-time install),
        // fetch it from the public endpoint. This is required so the HMAC handshake
        // can succeed. The endpoint returns the SHARED server secret (not per-site),
        // so no auth is needed for it.
        self::ensureServerSecret();

        try {
            $client = RestClient::fromOption();
        } catch (\Throwable $e) {
            $result = [
                'success' => false,
                'error_code' => 'CLIENT_INIT_FAILED',
                'message' => $e->getMessage(),
            ];
            update_option(self::OPTION_LAST_ACTIVATION, $result);
            update_option(self::OPTION_STATUS, 'failed');
            return $result;
        }

        $siteUrl = (string) (function_exists('home_url') ? home_url('/') : '');
        if ($siteUrl === '') {
            $result = [
                'success' => false,
                'error_code' => 'NO_SITE_URL',
                'message' => 'home_url() returned empty; cannot register site.',
            ];
            update_option(self::OPTION_LAST_ACTIVATION, $result);
            update_option(self::OPTION_STATUS, 'failed');
            return $result;
        }

        try {
            $response = $client->post('/v1/license/activate', ['site_url' => $siteUrl]);
        } catch (\Throwable $e) {
            $result = [
                'success' => false,
                'error_code' => 'NETWORK_ERROR',
                'message' => $e->getMessage(),
            ];
            update_option(self::OPTION_LAST_ACTIVATION, $result);
            update_option(self::OPTION_STATUS, 'failed');
            return $result;
        }

        if (!is_array($response)) {
            $result = [
                'success' => false,
                'error_code' => 'INVALID_RESPONSE',
                'message' => 'Server returned non-array response.',
            ];
            update_option(self::OPTION_LAST_ACTIVATION, $result);
            update_option(self::OPTION_STATUS, 'failed');
            return $result;
        }

        $success = !empty($response['success']);
        update_option(self::OPTION_STATUS, $success ? 'active' : 'failed');

        // On success, also persist site_id + status into LicenseManager for fast lookups.
        if ($success && isset($response['license_id'])) {
            LicenseManager::store(
                LicenseManager::key(),
                LicenseManager::secret(),
                (int) $response['license_id'],
                (string) parse_url($siteUrl, PHP_URL_HOST),
                (string) ($response['status'] ?? 'probation')
            );
        }

        update_option(self::OPTION_LAST_ACTIVATION, $response);
        return $response;
    }

    /**
     * Get the current activation status for UI display.
     *
     * @return string One of: 'unknown', 'active', 'pending', 'failed'.
     */
    public static function getStatus(): string
    {
        if (!function_exists('swa_fs')) {
            return 'unknown';
        }
        if (!self::sdkHasSite()) {
            return 'pending';
        }
        return (string) get_option(self::OPTION_STATUS, 'pending');
    }

    /**
     * Get the last activation result for UI display.
     *
     * @return array<string, mixed>|null
     */
    public static function getLastResult(): ?array
    {
        $v = get_option(self::OPTION_LAST_ACTIVATION, null);
        return is_array($v) ? $v : null;
    }

    /**
     * Manually clear the activation cache and force re-attempt.
     *
     * @return array<string, mixed> Result of the activation attempt.
     */
    public static function retryNow(): array
    {
        delete_option(self::OPTION_LAST_ATTEMPT_AT);
        delete_option(self::OPTION_STATUS);
        return self::runActivation();
    }

    /**
     * CF17.1: Fetch the server's public HMAC key and persist in OPTION_SECRET.
     *
     * Idempotent: if OPTION_SECRET is already set, returns true immediately.
     * If the network call fails, returns false (caller treats as transient).
     *
     * @return bool true if a secret is now available (either pre-existing or freshly fetched).
     */
    public static function ensureServerSecret(): bool
    {
        // IMPORTANT: Do NOT short-circuit on LicenseManager::OPTION_SECRET
        // (which holds the Freemius per-site license secret). That value is
        // the WRONG secret for HMAC signing. Use a separate option key.
        if (EncryptedOption::get(self::OPTION_SERVER_SECRET, '') !== '') {
            return true;
        }

        // Also sanity-check: if the legacy option DOES contain a non-empty
        // value that came from /public-key, migrate it forward (defensive).
        $legacy = (string) get_option(LicenseManager::OPTION_SECRET, '');
        if ($legacy !== '' && self::looksLikeServerSecret($legacy)) {
            EncryptedOption::put(self::OPTION_SERVER_SECRET, $legacy);
            return true;
        }

        $serverUrl = (string) get_option('swapads_client_server_url', 'https://api.swapads.eu');
        $url = rtrim($serverUrl, '/') . '/wp-json/swapads-server/v1/public-key';

        $response = wp_remote_get($url, ['timeout' => 10]);
        if (!is_array($response)) {
            return false;
        }
        $code = (int) wp_remote_retrieve_response_code($response);
        if ($code !== 200) {
            return false;
        }
        $body = (string) wp_remote_retrieve_body($response);
        $data = json_decode($body, true);
        if (!is_array($data) || empty($data['secret']) || !is_string($data['secret'])) {
            return false;
        }
        EncryptedOption::put(self::OPTION_SERVER_SECRET, (string) $data['secret']);
        return true;
    }

    /**
     * Heuristic: the server HMAC secret is a 64-char hex string
     * (sha256). The Freemius per-site license secret is also a 64-char
     * hex string, so we cannot reliably distinguish them by format alone.
     * Use this only when the legacy option is empty.
     */
    private static function looksLikeServerSecret(string $candidate): bool
    {
        return (bool) preg_match('/^[a-f0-9]{64}$/', $candidate);
    }

    }
