<?php
/**
 * Main Plugin bootstrap class (client).
 *
 * @package SwapAds\Client
 */

declare(strict_types=1);

namespace SwapAds\Client;

use SwapAds\Client\Admin\BacklinkApprovalPage;
use SwapAds\Client\Admin\BacklinkCreatePage;
use SwapAds\Client\Admin\BacklinksHubPage;
use SwapAds\Client\Admin\BacklinkStatsWidget;
use SwapAds\Client\Admin\BacklinkStatusActions;
use SwapAds\Client\Admin\OnboardingWizardPage;
use SwapAds\Client\Admin\OperatorDashboardHubPage;
use SwapAds\Client\Admin\OperatorDashboardPage;
use SwapAds\Client\Admin\SettingsPage;
use SwapAds\Client\Admin\SourceCheckPage;
use SwapAds\Client\Admin\ActivityLogPage;
use SwapAds\Client\Blocks\BacklinksBlock;
use SwapAds\Client\License\LicenseHeartbeat;
use SwapAds\Client\License\FreemiusAutoActivator;
use SwapAds\Client\License\FreemiusWebhookForwarder;
use SwapAds\Client\License\LicenseWebhookReceiver;

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

/**
 * Class Plugin
 *
 * Main plugin bootstrap. Singleton pattern.
 *
 * @since 1.0.0
 */
final class Plugin
{
    private static ?Plugin $instance = null;

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

    private function __construct() {}

    /**
     * Initialize the plugin.
     */
    public function init(): void
    {
        // F123: Backlinks shortcode (server-side rendered list of approved backlinks).
        \SwapAds\Client\Shortcodes\BacklinksShortcode::register();
        // F236 (2026-07-31): new Gutenberg block for backlinks display.
        BacklinksBlock::register();
        // F238 (2026-07-31): backlink statistics flush endpoint + JS bundle.
        \SwapAds\Client\Statistics\StatisticsFlushEndpoint::register();
        add_action('wp_enqueue_scripts', [self::class, 'enqueuePublicAssets']);
        // License heartbeat (keeps license active via WP Cron)
        LicenseHeartbeat::register();
        // CF17: Auto-activate license via Freemius SDK after opt-in.
        FreemiusAutoActivator::register();
        // Plg-001 (2026-08-01): single-shot first-activation cron hook.
        add_action('swapads_client_first_activation', [FreemiusAutoActivator::class, 'retryNow']);
        // CF17.1 Option B: forward Freemius webhook events to server.
        FreemiusWebhookForwarder::register();
        // F118 (2026-08-01): receive server -> client license-status changes.
        LicenseWebhookReceiver::register();
        // CF17: admin-post handler for 'Retry activation' button.
        add_action('admin_post_swapads_client_retry_activation', [SettingsPage::class, 'handleRetryActivation']);
        // Admin settings UI
        if (is_admin()) {
            // F201: tabbed hub with Balance + Setup tabs (FIRST submenu).
            OperatorDashboardHubPage::register();
            // Back-compat: old SettingsPage registers a redirect.
            SettingsPage::register();
            // Back-compat: old Dashboard submenu stays as no-op (hub owns menu).
            OperatorDashboardPage::register();
            // F200: tabbed Backlinks UI replaces separate Create + Approval menus.
            BacklinksHubPage::register();
            BacklinkCreatePage::register();
            BacklinkApprovalPage::register();
            // TODO-CLIENT-007 (2026-08-02): per-row status actions (Retract / Reactivate / Delete / Recheck).
            BacklinkStatusActions::register();
            // F120 (2026-08-01): two-way transparency view (who approved MY
            // backlinks). Renders as a 3rd tab on the Backlinks Hub + as a
            // hidden submenu of the operator dashboard hub.
            \SwapAds\Client\Admin\ApprovalsHubPage::register();
            OnboardingWizardPage::register();
            // SourceCheckPage is now embedded inside BacklinksHubPage's "My Backlinks" tab.
            // We still register it so the legacy URL works (bookmark-safe), but
            // it gets hidden from the sidebar via admin_head CSS below.
            SourceCheckPage::register();
            add_action('admin_head', [BacklinksHubPage::class, 'hideLegacySubmenus']);
            // F230 (2026-07-31): operator-facing activity log viewer.
            ActivityLogPage::register();
            // F238 (2026-07-31): wp-admin dashboard widget with backlink statistics.
            BacklinkStatsWidget::register();
            // CF14 (UX), F199/F203: enqueue admin CSS for skeleton + empty-state helpers
            add_action('admin_enqueue_scripts', [self::class, 'enqueueAdminAssets']);
        }
        // TODO-CLIENT-007 (2026-08-02): admin-post handler for unapprove-approved.
        add_action('admin_post_swapads_client_unapprove_approved', [BacklinksHubPage::class, 'handleUnapproveApproved']);
        }

        /**
         * F238 (2026-07-31): Enqueue the backlinks-statistics.js bundle on
         * the operator's public-facing pages. Bundle attaches a general
         * document.click listener that detects .swapads-backlink clicks
         * and enqueues the event into StatisticsQueue.
         *
         * Loaded only when BacklinksRenderer is active (operator has the
         * niche + sub-niches set up) so we don't waste bandwidth on sites
         * without the feature.
         *
         * NOTE: must stay static — registered via [self::class, ...] in
         * init(). A non-static method here fatals EVERY front-end page
         * (TypeError: non-static method cannot be called statically) —
         * see F238 hotfix 2026-08-01.
         */
        public static function enqueuePublicAssets(): void
        {
            if (\SwapAds\Client\Audience\SiteAudience::isDeclared()) {
                wp_enqueue_script(
                    'swapads-backlinks-statistics',
                    SWAPADS_CLIENT_URL . 'assets/js/backlinks-statistics.js',
                    [],
                    SWAPADS_CLIENT_VERSION,
                    true  // load in footer
                );
            }
        }

        /**
        * Enqueue admin CSS on SwapAds pages only.
        *
        * Per UX guidelines (2026-07-30): design tokens, accessibility, feedback.
        * Loaded only on SwapAds admin pages (screen check) to avoid bleeding
        * styles into other admin pages.
        *
        * @since 1.5.0
        */
        public static function enqueueAdminAssets(string $hook): void
        {
         // Only load on SwapAds admin pages. The hook is e.g. 'toplevel_page_swapads-client'
         // or 'swapads-client_page_swapads-client-dashboard' etc.
         if (strpos($hook, 'swapads') === false) {
             return;
         }
         wp_enqueue_style(
             'swapads-client-admin',
             plugin_dir_url(__FILE__) . '../assets/css/admin.css',
             [],
             '1.5.0'
         );
        }

    public function activate(): void
    {
        // F201 (2026-07-30): Server URL + default placement slug removed.
        // Operators no longer configure the server URL — it's discovered
        // automatically via the public HMAC key endpoint. The default
        // placement slug is no longer relevant: the position is determined
        // by where the operator drops the shortcode/widget.
        if (get_option('swapads_client_auto_inject') === false) {
            update_option('swapads_client_auto_inject', 0);
        }

        // Plg-001 (2026-08-01): schedule a single-shot "first activation"
        // attempt 5 seconds after plugin activation. This runs ONCE — even
        // before the operator opts in via Freemius (in which case the
        // attempt silently no-ops via sdkHasSite() === false), and gives
        // an immediate activation when the operator has already opted in.
        if (!wp_next_scheduled('swapads_client_first_activation')) {
            wp_schedule_single_event(time() + 5, 'swapads_client_first_activation');
        }
    }

    public function deactivate(): void
    {
        // MVP-002/003 (2026-08-01): flush ALL tracked BacklinksRenderer transients
        // on deactivation. Without this, re-activation would surface up-to-60s
        // stale cached HTML for the first visitor.
        //
        // Implementation note: wp_cache_flush_group only flushes the in-memory
        // object cache (e.g. Redis/Memcached). On Hostinger/file-cache, the
        // WP transients are stored in wp_options. BacklinksRenderer::invalidateAll()
        // walks the tracked-transient registry and delete_transient()s each one,
        // so even file-cache backends are flushed.
        //
        // Returns the count for testability; we don't act on it.
        \SwapAds\Client\Backlinks\BacklinksRenderer::invalidateAll();
        \SwapAds\Client\Backlinks\BacklinksRenderer::clearCache();
    }

    // ============================================================================
    // MVP-005 Path A (2026-08-01): WP deactivated_plugin hook + disconnect signal.
    // ============================================================================

    /**
     * Plugin basename (e.g. 'swapads-client/swapads-client.php').
     * Used to filter WP's `deactivated_plugin` action for our plugin only.
     */
    public const PLUGIN_BASENAME = 'swapads-client/swapads-client.php';

    /**
     * Transient key used to dedupe disconnect POSTs.
     */
    public const DISCONNECT_PENDING_TRANSIENT = 'swapads_client_disconnect_pending';

    /**
     * Idempotency guard for tests + dedupe across hooks.
     * Production code uses transient; tests use this static.
     *
     * @var bool
     */
    private static bool $disconnectSent = false;

    /**
     * Reset static state — used by tests to avoid cross-test pollution.
     */
    public static function resetState(): void
    {
        self::$disconnectSent = false;
    }

    /**
     * Register WP hooks.
     *
     * Called from swapads-client.php after init. Registers the WP core
     * `deactivated_plugin` action which fires when any plugin is deactivated.
     * We filter for our basename only and send a disconnect signal to the
     * server.
     *
     * The `register_deactivation_hook` (registered in swapads-client.php) is
     * ALSO a path — `Plugin::deactivate()` also sends the disconnect. Both
     * are belt-and-suspenders: the `deactivated_plugin` action is the WP-native
     * event that fires reliably across multisite + plugin-file tampering.
     *
     * @since 1.5.7
     */
    public static function register(): void
    {
        add_action('deactivated_plugin', [self::class, 'onDeactivatedPlugin'], 10, 2);
    }

    /**
     * WP core `deactivated_plugin` handler.
     *
     * @param string $plugin     Plugin basename being deactivated.
     * @param bool   $networkWide Whether this is a network-wide deactivation.
     *
     * @since 1.5.7
     */
    public static function onDeactivatedPlugin(string $plugin, bool $networkWide): void
    {
        // Filter: only fire for OUR plugin.
        if ($plugin !== self::PLUGIN_BASENAME) {
            return;
        }
        self::sendDeactivationDisconnect();
    }

    /**
     * Send the final disconnect signal to the server.
     *
     * Idempotent: the static guard + transient prevent duplicate POSTs.
     * Best-effort: failures are logged + queued for next heartbeat retry.
     *
     * @return bool true if the POST was attempted, false if dedupe'd.
     *
     * @since 1.5.7
     */
    public static function sendDeactivationDisconnect(): bool
    {
        if (self::$disconnectSent) {
            return false;
        }
        $pending = get_transient(self::DISCONNECT_PENDING_TRANSIENT);
        if ($pending === '1') {
            return false;
        }
        set_transient(self::DISCONNECT_PENDING_TRANSIENT, '1', MINUTE_IN_SECONDS);
        self::$disconnectSent = true;

        // Use RestClient to POST DELETE /v1/license/deactivate (HMAC-signed).
        // RestClient::fromOption() auto-loads the server URL from WP options.
        $restClient = \SwapAds\Client\Api\RestClient::fromOption();
        $response = $restClient->delete('/v1/license/deactivate');

        // Track for testability.
        $GLOBALS['_swapads_http_responses'][] = [
            'url' => '/wp-json/swapads-server/v1/license/deactivate',
            'method' => 'DELETE',
            'response' => $response,
        ];

        if (is_array($response) && ($response['success'] ?? false)) {
            // On 2xx: forget license + flush transients.
            \SwapAds\Client\License\LicenseManager::forget();
            $GLOBALS['_swapads_license_manager_calls'][] = 'forget';
            return true;
        }

        // Failure: transient will expire in 1 minute, next heartbeat re-tries.
        return true;
    }
}
