<?php

declare(strict_types=1);

namespace Numinix\SeekmodoWordPress;

use Numinix\SeekmodoWordPress\Admin\ConnectFlow;
use Numinix\SeekmodoWordPress\Admin\HealthWidget;
use Numinix\SeekmodoWordPress\Admin\LocalIndexAdmin;
use Numinix\SeekmodoWordPress\Admin\ModeBadge;
use Numinix\SeekmodoWordPress\Admin\Privacy;
use Numinix\SeekmodoWordPress\Admin\PromoteFlow;
use Numinix\SeekmodoWordPress\Admin\SettingsPage;
use Numinix\SeekmodoWordPress\Blocks\Registry as BlocksRegistry;
use Numinix\SeekmodoWordPress\Cli\Commands;
use Numinix\SeekmodoWordPress\Elementor\Loader as ElementorLoader;
use Numinix\SeekmodoWordPress\WPBakery\Loader as WPBakeryLoader;
use Numinix\SeekmodoWordPress\Commerce\ConversionEvents;
use Numinix\SeekmodoWordPress\Commerce\Indexer as CommerceIndexer;
use Numinix\SeekmodoWordPress\Commerce\SearchRedirect as CommerceSearchRedirect;
use Numinix\SeekmodoWordPress\Commerce\SearchSwap as CommerceSearchSwap;
use Numinix\SeekmodoWordPress\Commerce\Typeahead as CommerceTypeahead;
use Numinix\SeekmodoWordPress\Config\Options;
use Numinix\SeekmodoWordPress\Content\Indexer as ContentIndexer;
use Numinix\SeekmodoWordPress\Content\PostTypeRegistry;
use Numinix\SeekmodoWordPress\Content\SearchSwap as ContentSearchSwap;
use Numinix\SeekmodoWordPress\Content\Typeahead as ContentTypeahead;
use Numinix\SeekmodoWordPress\Cron\ConfigPullCron;
use Numinix\SeekmodoWordPress\Cron\EventsFlushCron;
use Numinix\SeekmodoWordPress\Cron\IndexCron;
use Numinix\SeekmodoWordPress\Events\ClickBeacon;
use Numinix\SeekmodoWordPress\Events\EventsQueue;
use Numinix\SeekmodoWordPress\Events\SerpAttributes;
use Numinix\SeekmodoWordPress\Frontend\AskUI;
use Numinix\SeekmodoWordPress\Frontend\RecommendationsUI;
use Numinix\SeekmodoWordPress\Frontend\SuggestUI;
use Numinix\SeekmodoWordPress\Frontend\TypeaheadUI;
use Numinix\SeekmodoWordPress\Rest\BrowserTokenRoute;
use Numinix\SeekmodoWordPress\Rest\RecommendationsRoute;
use Numinix\SeekmodoWordPress\LocalIndex\Indexer as LocalIndexer;
use Numinix\SeekmodoWordPress\LocalIndex\IndexSettings;
use Numinix\SeekmodoWordPress\LocalIndex\LocalIndexCron;
use Numinix\SeekmodoWordPress\LocalIndex\RelevanssiCompat;
use Numinix\SeekmodoWordPress\LocalIndex\Schema as LocalIndexSchema;
use Numinix\SeekmodoWordPress\LocalIndex\VerticalConfig;
use Numinix\SeekmodoWordPress\Maintenance\Upgrade;
use Numinix\SeekmodoWordPress\Rest\Routes as RestRoutes;

/**
 * Single entry point. Called from `seekmodo-wordpress.php` on
 * `plugins_loaded` priority 5 so we boot before themes and most plugins,
 * but after WordPress core finishes loading.
 *
 * Each sub-module is wired here via `*::register()` — none of them
 * register their own `plugins_loaded` hooks, which keeps the bootstrap
 * surface a single grep target and gives operator-side troubleshooting
 * one place to look when something doesn't fire.
 */
final class Bootstrap
{
    /**
     * Run on plugin activation. Creates the events table, seeds default
     * options, and schedules the three wp_cron events.
     *
     * Idempotent — safe to re-run on every reactivation.
     */
    public static function activate(): void
    {
        Options::seedDefaults();
        VerticalConfig::registerDefaults();
        IndexSettings::registerDefaults();
        LocalIndexSchema::install();
        EventsQueue::installTable();
        IndexCron::schedule();
        LocalIndexCron::schedule();
        LocalIndexer::queueInitialFullBuild();
        ConfigPullCron::schedule();
        EventsFlushCron::schedule();
    }

    /**
     * Run on plugin deactivation. Unschedules cron events but leaves
     * options + events table intact so a reactivate is a no-op.
     * Hard cleanup lives in `uninstall.php`.
     */
    public static function deactivate(): void
    {
        IndexCron::unschedule();
        LocalIndexCron::unschedule();
        ConfigPullCron::unschedule();
        EventsFlushCron::unschedule();
    }

    /**
     * Boot the runtime. Order matters in two places:
     *   - `Options::register()` must run first so every consumer can
     *     read configuration via `Options::*`.
     *   - REST routes register on the `rest_api_init` hook regardless
     *     of when we call their wiring code, so the relative order
     *     between Rest and Events/Content/Commerce doesn't matter.
     */
    public static function boot(): void
    {
        load_plugin_textdomain('seekmodo', false, dirname(SEEKMODO_WP_PLUGIN_BASENAME) . '/languages');

        // Plumb remote breaker thresholds (set by RemoteConfig from
        // tenant.snapshot) into the seekmodo_breaker_thresholds filter
        // so the runtime breaker honors admin.seekmodo.com overrides
        // without a code change here.
        add_filter('seekmodo_breaker_thresholds', static function ($defaults) {
            $remote = get_option('seekmodo_wp_remote_breaker', null);
            if (is_array($remote)) {
                return array_merge(is_array($defaults) ? $defaults : [], $remote);
            }

            return $defaults;
        }, 10, 1);

        Options::register();

        if (LocalIndexSchema::needsInstall()) {
            LocalIndexSchema::install();
        }
        LocalIndexer::register();
        LocalIndexCron::register();
        RelevanssiCompat::register();

        // v0.5.1 — deploy-channel-agnostic version-bump detector.
        // Compares the on-disk SEEKMODO_WP_VERSION constant to the
        // last-seen value stored in wp_options on every request.
        // On a mismatch (rsync'd a new file, ran wp-cli plugin
        // update, or core's upgrader downloaded a fresh zip), it
        // fans out cache flushes to every page-cache / minify /
        // OPCache plugin we know about and invalidates the cached
        // browser-token transient. Keeps the connector self-healing
        // across all three deploy channels — only WordPress's own
        // upgrader fires `upgrader_process_complete`, so cache
        // plugins that hook there miss the rsync + wp-cli paths
        // POCO and Trek use. {@see \Numinix\SeekmodoWordPress\Maintenance\Upgrade}.
        Upgrade::register();

        // v0.3.0 multi-vertical gate — only register the vertical
        // sub-modules the gateway has enabled for this tenant.
        // {@see Options::enabledVerticals()} returns the all-enabled
        // set on first boot / stale snapshots, so a freshly-installed
        // plugin that hasn't completed its first 5-min config pull
        // behaves the same as before this gate landed. The first
        // tenant.snapshot pull then narrows the set, preventing a
        // commerce-only tenant from pushing post-shaped docs at
        // the gateway (which would route them at the wrong
        // Typesense collection and trigger schema-mismatch 4xx).
        $verticals = Options::enabledVerticals();

        if (isset($verticals['content'])) {
            ContentSearchSwap::register();
            ContentIndexer::register();
            PostTypeRegistry::register();
        }

        // Typeahead REST route lives in Content\Typeahead; commerce-only
        // tenants (e.g. Woo storefronts) still need the shared endpoint.
        if (isset($verticals['content']) || isset($verticals['commerce'])) {
            ContentTypeahead::register();
        }

        // Commerce vertical is dormant unless BOTH (a) WooCommerce is
        // active on this site AND (b) the gateway has commerce
        // enabled for this tenant. The WC probe alone isn't enough —
        // a content-only tenant whose host happens to also run Woo
        // (e.g. a publisher that sells a subscription side-product)
        // shouldn't push commerce docs at a content-only collection.
        if (self::wooCommerceActive() && isset($verticals['commerce'])) {
            CommerceSearchRedirect::register();
            CommerceSearchSwap::register();
            CommerceIndexer::register();
            CommerceTypeahead::register();
            ConversionEvents::register();
            RecommendationsUI::register();
        }

        ClickBeacon::register();
        SerpAttributes::register();
        EventsQueue::register();
        RestRoutes::register();
        RecommendationsRoute::register();

        // v0.4.0 — front-end typeahead dropdown UI. The class
        // self-gates on the per-site opt-in option
        // (`seekmodo_wp_typeahead_ui_enabled`, default 0) plus the
        // `seekmodo_enqueue_typeahead` filter, so this register()
        // call is safe to make unconditionally — sites that don't
        // want the connector-bundled UI (e.g. trek.ca, which has its
        // own theme integration) won't ship the assets.
        //
        // v0.6.0 (SM-606) — the legacy TypeaheadUI now sits behind the
        // `seekmodo_wp_suggest_legacy` opt-out flag. By default v0.6.0
        // sites enabling the master toggle get the universal
        // <seekmodo-suggest> web component instead (richer envelope,
        // shared UX with the other connector platforms). Both classes
        // are mutually exclusive at the gate (`isActive()` on each
        // checks the legacy flag) — registering both unconditionally
        // is the same one-shot wiring the rest of Bootstrap follows.
        TypeaheadUI::register();
        SuggestUI::register();
        AskUI::register();
        BrowserTokenRoute::register();

        // Sprint 11 PR 1 — Gutenberg block surface for the four
        // Seekmodo web components (search, recommend, chat, quiz).
        // Skips silently on WP < 5.8 where `register_block_type_from_metadata`
        // is unavailable, so plugin activation still succeeds.
        BlocksRegistry::register();

        // Sprint 11 PR 2 — Elementor widget surface, gated on
        // `did_action('elementor/loaded')` so the connector behaves
        // identically on sites without Elementor (the four widgets
        // simply don't appear in the panel). The shared
        // `Blocks\AttributeSchema` keeps the Elementor surface
        // attribute-compatible with the Gutenberg blocks.
        ElementorLoader::register();

        // Sprint 11 PR 3 — `[seekmodo_*]` shortcodes (always armed,
        // useful for classic-editor and headless template embeds)
        // plus WPBakery `vc_map()` registration (gated on
        // `class_exists('Vc_Manager')` so the elements appear in the
        // Visual Composer picker without breaking sites that don't
        // have WPBakery installed).
        WPBakeryLoader::register();

        IndexCron::register();
        ConfigPullCron::register();
        EventsFlushCron::register();

        // ConnectFlow registers two surfaces:
        //   - admin_post_seekmodo_wp_connect (admin-side: operator
        //     clicks "Connect to Seekmodo" -> 302 to seekmodo.com)
        //   - REST POST /wp-json/seekmodo/v1/connect/callback
        //     (server-to-server: seekmodo.com posts the signed
        //     pairing JWT back here)
        //
        // The REST callback is hit from seekmodo.com's
        // /api/connect/complete with NO admin cookies and the WP
        // request cycle DOES NOT pass `is_admin() === true` for it
        // (REST API requests are not "admin" in WP's classification).
        // Gating the registration behind is_admin() -- which is what
        // <=v0.3.1 did -- meant the POST silently 404'd because the
        // route was never registered on REST requests, even though
        // the admin-side button worked. v0.3.2 lifts the registration
        // out of the is_admin() block so the rest_api_init hook
        // always runs. The admin_post_* registration is harmless to
        // call on every request -- those hooks only fire on
        // admin-post.php hits regardless of where they're registered.
        ConnectFlow::register();

        if (is_admin()) {
            SettingsPage::register();
            LocalIndexAdmin::register();
            PromoteFlow::register();
            HealthWidget::register();
            ModeBadge::register();
            Privacy::register();
        }

        if (defined('WP_CLI') && WP_CLI) {
            Commands::register();
        }
    }

    /**
     * WooCommerce-active probe. Cheap and conservative: we look for the
     * `WC()` runtime function (registered on `plugins_loaded` priority
     * 0) and the `WooCommerce` class. Either alone is not enough — a
     * partial Woo activation (rare; usually a half-finished update)
     * shouldn't enable the commerce vertical.
     */
    public static function wooCommerceActive(): bool
    {
        return class_exists('WooCommerce', false) && function_exists('WC');
    }
}
