<?php

declare(strict_types=1);

namespace Numinix\SeekmodoWordPress\Admin;

use Numinix\SeekmodoWordPress\Config\DomainLock;
use Numinix\SeekmodoWordPress\Config\Options;
use Numinix\SeekmodoWordPress\Gateway\CircuitBreaker;
use Numinix\SeekmodoWordPress\Gateway\SignatureMismatchTracker;
use Numinix\SeekmodoWordPress\Mode\ModeFsm;

/**
 * `Settings → Seekmodo Search` admin page. Three regions:
 *
 *   1. Pair status. Tenant ID + masked shared secret + Connect button.
 *      "Connect to Seekmodo" is the only thing visible until paired.
 *
 *   2. Mode + health. Current mode, breaker state, last index. The
 *      mode is read-only on this page — operators flip from
 *      admin.seekmodo.com so the change survives plugin reinstalls.
 *
 *   3. Indexable post types. Multi-select; pre-checked from
 *      {@see \Numinix\SeekmodoWordPress\Content\PostTypeRegistry::indexable()}.
 *
 * Everything else (auto-promote thresholds, breaker thresholds,
 * gateway URL overrides) lives on admin.seekmodo.com to keep the WP
 * admin UI scoped to "the things an operator on this tenant might
 * actually need to touch".
 */
final class SettingsPage
{
    public const SLUG = 'seekmodo-search';

    public static function register(): void
    {
        if (!function_exists('add_action')) {
            return;
        }
        add_action('admin_menu', [self::class, 'addMenu']);
        add_action('admin_notices', [self::class, 'maybeShowFlashNotice']);
        // Audit P-G (2026-06-10) -- self-recovery notice. Surfaces on
        // EVERY admin page (not just /options-general.php?page=
        // seekmodo-search) so an operator notices their storefront
        // is out of sync without having to look for the settings tab.
        add_action('admin_notices', [self::class, 'maybeShowSignatureMismatchNotice']);
    }

    public static function addMenu(): void
    {
        if (!function_exists('add_options_page')) {
            return;
        }
        add_options_page(
            esc_html__('Seekmodo Search', 'seekmodo'),
            esc_html__('Seekmodo Search', 'seekmodo'),
            'manage_options',
            self::SLUG,
            [self::class, 'render']
        );
    }

    public static function maybeShowFlashNotice(): void
    {
        $status = isset($_GET['seekmodo_status']) ? sanitize_key((string) $_GET['seekmodo_status']) : '';
        $code   = isset($_GET['seekmodo_code']) ? sanitize_key((string) $_GET['seekmodo_code']) : '';
        if ($status === '' || $code === '') {
            return;
        }
        $page = isset($_GET['page']) ? sanitize_key((string) $_GET['page']) : '';
        if ($page !== self::SLUG) {
            return;
        }
        $messages = [
            'success' => [
                'paired'   => __('Connected to Seekmodo. You can now flip the mode from admin.seekmodo.com.', 'seekmodo'),
                'promoted' => __('Canonical storefront host promoted. The dev site will silently become a no-op clone within five minutes; pair the production site now to take over search.', 'seekmodo'),
            ],
            'error' => [
                'nonce_mismatch'      => __('The Connect-to-Seekmodo state token did not match. Try again.', 'seekmodo'),
                'verify_failed'       => __('Seekmodo could not verify the claim token. It may have expired.', 'seekmodo'),
                'missing_pair'        => __('Seekmodo did not return a tenant pair. Try again.', 'seekmodo'),
                'empty_pair'          => __('Seekmodo returned an empty tenant pair. Try again.', 'seekmodo'),
                'invalid_host'        => __('That doesn\'t look like a valid hostname. Use the form `www.example.com` (no scheme, no path).', 'seekmodo'),
                'host_looks_nonprod'  => __('That host looks like another non-production environment. Promote to a host whose leftmost label is NOT dev / staging / test / qa / preview / sandbox.', 'seekmodo'),
                'gateway_rejected'    => __('Seekmodo refused the canonical-host promotion. Open admin.seekmodo.com → Tenants → Settings → Storefront domain to investigate; common causes are rate limits or the host having never been observed for this tenant.', 'seekmodo'),
            ],
        ];
        $msg = $messages[$status][$code] ?? '';
        if ($msg === '') {
            return;
        }
        $klass = $status === 'success' ? 'notice-success' : 'notice-error';
        printf(
            '<div class="notice %s is-dismissible"><p>%s</p></div>',
            esc_attr($klass),
            esc_html($msg)
        );
    }

    /**
     * Audit P-G (2026-06-10) -- self-recovery for the
     * signature_mismatch failure mode that the credentials-rotate
     * grace window doesn't catch. When 3+ signature_mismatch 401s
     * arrive within a 5-minute window (see
     * {@see SignatureMismatchTracker} for the truth-table), surface a
     * yellow admin notice on every WP admin page pointing at the
     * Re-pair button. The notice clears itself the moment the next
     * gateway call returns 2xx.
     */
    public static function maybeShowSignatureMismatchNotice(): void
    {
        if (!function_exists('current_user_can') || !current_user_can('manage_options')) {
            return;
        }
        if (!SignatureMismatchTracker::isTripped()) {
            return;
        }
        $settings_url = function_exists('admin_url')
            ? admin_url('options-general.php?page=' . self::SLUG)
            : '#';
        $message = sprintf(
            /* translators: 1: opening anchor tag for settings page, 2: closing anchor tag. */
            esc_html__(
                'Seekmodo Search is out of sync with the storefront -- the gateway has refused the connector\'s shared secret %1$smultiple%2$s times. %3$sOpen Seekmodo Search%4$s and click Re-pair to restore search.',
                'seekmodo'
            ),
            '<strong>',
            '</strong>',
            '<a href="' . esc_url($settings_url) . '">',
            '</a>'
        );
        printf(
            '<div class="notice notice-warning"><p>%s</p></div>',
            wp_kses(
                $message,
                [
                    'strong' => [],
                    'a'      => ['href' => []],
                ]
            )
        );
    }

    public static function render(): void
    {
        if (!current_user_can('manage_options')) {
            return;
        }

        $paired      = Options::isPaired();
        $tenant_id   = Options::tenantId();
        $secret      = Options::sharedSecret();
        $mode        = ModeFsm::current();
        $breaker     = CircuitBreaker::state();
        $host_lock   = DomainLock::matches();
        $current_host = DomainLock::currentHost();

        $masked_secret = $secret === ''
            ? __('— not set —', 'seekmodo')
            : str_repeat('•', max(0, strlen($secret) - 4)) . substr($secret, -4);

        $connect_url = wp_nonce_url(
            admin_url('admin-post.php?action=seekmodo_wp_connect'),
            'seekmodo_wp_connect'
        );

        echo '<div class="wrap">';
        echo '<h1>' . esc_html__('Seekmodo Search', 'seekmodo') . '</h1>';

        // 1. Pair status.
        echo '<h2>' . esc_html__('Connection', 'seekmodo') . '</h2>';
        echo '<table class="form-table" role="presentation"><tbody>';
        echo '<tr><th>' . esc_html__('Status', 'seekmodo') . '</th><td>';
        if ($paired) {
            echo '<span style="color:#2271b1;">✓ ' . esc_html__('Paired', 'seekmodo') . '</span>';
        } else {
            echo '<span style="color:#d63638;">— ' . esc_html__('Not paired yet', 'seekmodo') . '</span>';
        }
        echo '</td></tr>';
        echo '<tr><th>' . esc_html__('Tenant ID', 'seekmodo') . '</th><td><code>' . esc_html($tenant_id !== '' ? $tenant_id : '—') . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Shared secret', 'seekmodo') . '</th><td><code>' . esc_html($masked_secret) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Storefront host', 'seekmodo') . '</th><td><code>' . esc_html($current_host) . '</code> ';
        if ($host_lock) {
            echo '<span style="color:#2271b1;">(' . esc_html__('locked-in match', 'seekmodo') . ')</span>';
        } else {
            echo '<span style="color:#d63638;">(' . esc_html__('not the canonical host — connector returns null on every gateway call', 'seekmodo') . ')</span>';
        }
        echo '</td></tr>';
        echo '</tbody></table>';

        echo '<p><a class="button button-primary" href="' . esc_url($connect_url) . '">';
        echo esc_html($paired ? __('Re-pair with Seekmodo', 'seekmodo') : __('Connect to Seekmodo', 'seekmodo'));
        echo '</a></p>';

        // 1b. Promote dev -> production. Only renders on a paired
        // dev/staging-style site that's currently the canonical
        // host. The button POSTs through admin-post.php to the
        // gateway's tenant.domain.promote MCP tool which atomically
        // flips locked_domain and emits a domain_lock.changed audit
        // event. Within 5 min every paired connector's
        // tenant.snapshot pull mirrors the new lock and dev sites
        // silently become no-op clones (DomainLock::matches()
        // returns false against the new canonical).
        if (PromoteFlow::shouldOffer()) {
            self::renderPromoteCard($tenant_id, $current_host);
        }

        // 2. Mode + health.
        echo '<hr><h2>' . esc_html__('Health', 'seekmodo') . '</h2>';
        echo '<table class="form-table" role="presentation"><tbody>';
        echo '<tr><th>' . esc_html__('Mode', 'seekmodo') . '</th><td><code>' . esc_html($mode) . '</code> ';
        echo '<a href="https://admin.seekmodo.com" target="_blank" rel="noopener noreferrer">' . esc_html__('change on admin.seekmodo.com →', 'seekmodo') . '</a></td></tr>';
        echo '<tr><th>' . esc_html__('Circuit breaker', 'seekmodo') . '</th><td><code>' . esc_html($breaker) . '</code></td></tr>';
        echo '</tbody></table>';

        echo '<form method="post">';
        LocalIndexAdmin::renderPanel();
        echo '</form>';

        // 3. Indexable post types.
        echo '<hr><h2>' . esc_html__('Indexable post types (content vertical)', 'seekmodo') . '</h2>';
        echo '<p>' . esc_html__('Defaults to Posts + Pages + every public Custom Post Type. Override here or via the `seekmodo_indexable_post_types` filter.', 'seekmodo') . '</p>';

        if (function_exists('get_post_types')) {
            $available = get_post_types(['public' => true], 'objects');
            if (is_array($available)) {
                $current = \Numinix\SeekmodoWordPress\Content\PostTypeRegistry::indexable();
                echo '<form method="post" action="' . esc_url(admin_url('options.php')) . '">';
                settings_fields('seekmodo_wp');
                echo '<fieldset>';
                foreach ($available as $slug => $obj) {
                    if (in_array($slug, ['product', 'product_variation'], true)) {
                        continue; // owned by commerce vertical.
                    }
                    printf(
                        '<label style="display:block;margin:0.3em 0;"><input type="checkbox" name="%s[]" value="%s" %s> <code>%s</code> — %s</label>',
                        esc_attr(Options::OPT_INDEXABLE_TYPES),
                        esc_attr($slug),
                        checked(in_array($slug, $current, true), true, false),
                        esc_html($slug),
                        esc_html(isset($obj->label) ? (string) $obj->label : $slug)
                    );
                }
                echo '</fieldset>';
                submit_button(__('Save post types', 'seekmodo'));
                echo '</form>';
            }
        }

        // 4. Editorial post-type priority (re-rank gateway results).
        echo '<hr><h2>' . esc_html__('Search ranking priority', 'seekmodo') . '</h2>';
        echo '<p>' . esc_html__(
            'Comma-separated list of post types in priority order. After the gateway scores results by textual relevance, hits whose post type appears earlier in this list are bucket-sorted to the top while the relevance order within each tier is preserved. Leave blank for pure gateway-relevance order. Example: page,post — lifts every page above every blog post.',
            'seekmodo'
        ) . '</p>';
        $priority_value = (string) (function_exists('get_option') ? get_option(Options::OPT_POST_TYPE_PRIORITY, '') : '');
        echo '<form method="post" action="' . esc_url(admin_url('options.php')) . '">';
        settings_fields('seekmodo_wp');
        echo '<table class="form-table" role="presentation"><tbody><tr><th scope="row"><label for="' . esc_attr(Options::OPT_POST_TYPE_PRIORITY) . '">' . esc_html__('Priority order', 'seekmodo') . '</label></th><td>';
        printf(
            '<input type="text" id="%1$s" name="%1$s" value="%2$s" class="regular-text" placeholder="page,post" />',
            esc_attr(Options::OPT_POST_TYPE_PRIORITY),
            esc_attr($priority_value)
        );
        echo '<p class="description">' . esc_html__(
            'Types not listed keep their gateway-relevance order and appear after every listed type.',
            'seekmodo'
        ) . '</p>';
        echo '</td></tr></tbody></table>';
        submit_button(__('Save priority', 'seekmodo'));
        echo '</form>';

        // 5. Typeahead dropdown UI (v0.4.0). The connector ships a
        // vanilla-JS typeahead that auto-binds to common search inputs
        // (input[type="search"], input[name="s"], etc.) and renders a
        // gateway-powered dropdown beneath them. Default-off so a fresh
        // upgrade on a site that already has its own theme-level
        // typeahead (e.g. trek.ca) doesn't double-render.
        $typeahead_enabled = (bool) (function_exists('get_option') ? get_option(Options::OPT_TYPEAHEAD_UI_ENABLED, '0') : false);
        $typeahead_enabled = filter_var($typeahead_enabled, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
        $branding_enabled  = Options::showBranding();
        echo '<hr><h2>' . esc_html__('Typeahead dropdown', 'seekmodo') . '</h2>';
        echo '<p>' . esc_html__(
            'When enabled, the connector auto-binds a Seekmodo-powered search-suggestions dropdown to any <input type="search"> or <input name="s"> on the storefront. Leave OFF if your theme already provides its own typeahead (e.g. a custom header search popover).',
            'seekmodo'
        ) . '</p>';
        echo '<form method="post" action="' . esc_url(admin_url('options.php')) . '">';
        settings_fields('seekmodo_wp');
        echo '<table class="form-table" role="presentation"><tbody>';
        echo '<tr><th scope="row">' . esc_html__('Enable typeahead', 'seekmodo') . '</th><td>';
        printf(
            '<label><input type="checkbox" name="%1$s" value="1" %2$s /> %3$s</label>',
            esc_attr(Options::OPT_TYPEAHEAD_UI_ENABLED),
            $typeahead_enabled ? 'checked' : '',
            esc_html__('Render the connector-bundled dropdown UI on the storefront.', 'seekmodo')
        );
        echo '<p class="description">' . esc_html__(
            'Themes can also force-off via the `seekmodo_enqueue_typeahead` filter regardless of this checkbox.',
            'seekmodo'
        ) . '</p>';
        echo '</td></tr>';
        echo '<tr><th scope="row">' . esc_html__('Show "Powered by Seekmodo"', 'seekmodo') . '</th><td>';
        printf(
            '<label><input type="checkbox" name="%1$s" value="1" %2$s /> %3$s</label>',
            esc_attr(Options::OPT_SHOW_BRANDING),
            $branding_enabled ? 'checked' : '',
            esc_html__('Render the small "Powered by Seekmodo" footer in the dropdown.', 'seekmodo')
        );
        echo '<p class="description">' . esc_html__(
            'Default is OFF so the dropdown is white-labelled. Partner sites flip this on; the gateway can also push it down via the tenant.snapshot config pull.',
            'seekmodo'
        ) . '</p>';
        echo '</td></tr>';
        echo '</tbody></table>';
        submit_button(__('Save typeahead settings', 'seekmodo'));
        echo '</form>';

        echo '</div>';
    }

    /**
     * Render the "Promote dev to production" card. Surfaced only
     * when {@see PromoteFlow::shouldOffer()} returns true. See
     * {@see PromoteFlow} for the flow / source-of-truth contract.
     */
    private static function renderPromoteCard(string $tenant_id, string $current_host): void
    {
        $candidates = PromoteFlow::productionCandidates();
        $admin_link = 'https://admin.seekmodo.com/settings/storefront-domain'
            . ($tenant_id !== '' ? '?tenant=' . rawurlencode($tenant_id) : '');

        echo '<hr>';
        echo '<div class="notice notice-warning" style="padding:1em 1.5em;margin:1.5em 0;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('Promote this site from dev to production', 'seekmodo') . '</h2>';
        echo '<p>'
            . sprintf(
                /* translators: %s: current dev/staging hostname. */
                esc_html__('Seekmodo currently treats %s as the canonical storefront for this tenant. When you\'re ready to launch, point the canonical at the production host below — every paired connector picks up the change within five minutes via the next tenant snapshot pull, and this dev site silently becomes a no-op clone.', 'seekmodo'),
                '<code>' . esc_html($current_host) . '</code>'
            )
            . '</p>';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:1em 0;">';
        echo '<input type="hidden" name="action" value="' . esc_attr(PromoteFlow::ACTION_INITIATE) . '">';
        wp_nonce_field(PromoteFlow::NONCE_NAME);

        echo '<label for="seekmodo-new-host" style="display:block;margin-bottom:0.4em;font-weight:600;">';
        echo esc_html__('Production storefront host', 'seekmodo');
        echo '</label>';

        if ($candidates !== []) {
            echo '<p style="margin:0.4em 0;">' . esc_html__('Hosts the gateway has observed for this tenant (filtered to non-dev candidates):', 'seekmodo') . '</p>';
            echo '<select name="new_host" id="seekmodo-new-host" style="min-width:24em;">';
            foreach ($candidates as $host) {
                printf('<option value="%1$s">%1$s</option>', esc_attr($host));
            }
            echo '</select>';
        } else {
            echo '<p style="margin:0.4em 0;">' . esc_html__('No production hosts have been observed yet — type the canonical hostname (without scheme or path):', 'seekmodo') . '</p>';
            echo '<input type="text" name="new_host" id="seekmodo-new-host" placeholder="www.example.com" style="min-width:24em;" required>';
        }

        echo '<p style="margin:1em 0;">';
        echo '<button type="submit" class="button button-primary">'
            . esc_html__('Promote canonical to production', 'seekmodo')
            . '</button>';
        echo ' <a href="' . esc_url($admin_link) . '" target="_blank" rel="noopener noreferrer">'
            . esc_html__('Or swap manually on admin.seekmodo.com →', 'seekmodo')
            . '</a>';
        echo '</p>';
        echo '</form>';

        echo '<p style="color:#646970;margin:0;font-size:0.92em;">'
            . esc_html__('After promotion: install this same plugin version on the production site, click Connect, and verify the dev site\'s Settings → Seekmodo Search shows "(not the canonical host)" — that confirms domain-lock has shut the dev feed off.', 'seekmodo')
            . '</p>';

        echo '</div>';
    }
}
