<?php

declare(strict_types=1);

namespace Numinix\SeekmodoWordPress\Admin;

use Numinix\SeekmodoWordPress\Config\DomainLock;
use Numinix\SeekmodoWordPress\Config\Options;
use Numinix\SeekmodoWordPress\Config\RemoteConfig;
use Numinix\SeekmodoWordPress\Gateway\Client;

/**
 * Self-service "Promote dev to production" flow (v0.3.0).
 *
 * Sibling to {@see ConnectFlow}. Surfaces in the WP admin
 * Settings → Seekmodo Search page when:
 *
 *   1. The site is paired (operator has finished pairing).
 *   2. {@see DomainLock::looksLikeNonProd()} matches the current
 *      host (the leftmost label is `dev`/`staging`/`stage`/`test`/
 *      `qa`/`preview`/`beta`/`preprod`/`pre-prod`/`sandbox`).
 *   3. The locked canonical host equals the current host (i.e.
 *      this dev site IS the active canonical right now — meaning
 *      there's no production site live yet, so flipping is the
 *      promotion).
 *
 * Two paths:
 *
 *   - Click "Promote canonical to <prod-host>". POSTs to the
 *     gateway's `tenant.domain.promote` MCP tool through the
 *     existing pairing-credential channel. The gateway atomically
 *     updates `numinix_mcp_tenant_config.locked_domain`, emits a
 *     `domain_lock.changed` audit event, and the next 5-min
 *     `tenant.snapshot` pull on every paired connector picks up
 *     the new lock without a re-pair. The dev site's local
 *     `seekmodo_wp_storefront_host` flips on the next pull, and
 *     `DomainLock::matches()` returns false on dev — every
 *     gateway-talking entry point goes silent.
 *
 *   - Click "I will swap manually". Opens admin.seekmodo.com's
 *     tenant-settings page focused on the storefront-domain field
 *     so the operator can pick the production host from the
 *     observed-domains list there.
 *
 * Why not just write `seekmodo_wp_storefront_host` locally?
 * Because the gateway-side `tenant_config.locked_domain` is the
 * source of truth — every other paired site (the prod site, once
 * it's added) needs to see the same canonical value, and the
 * `tenant.snapshot` pull is what mirrors it back. Touching the
 * local option only flips the local site without propagating, and
 * the next snapshot pull would revert it back.
 */
final class PromoteFlow
{
    public const ACTION_INITIATE = 'seekmodo_wp_promote_domain';
    public const NONCE_NAME      = 'seekmodo_wp_promote_domain';

    public static function register(): void
    {
        if (!function_exists('add_action')) {
            return;
        }
        add_action('admin_post_' . self::ACTION_INITIATE, [self::class, 'handle']);
    }

    /**
     * Predicate driving whether the SettingsPage card renders.
     */
    public static function shouldOffer(): bool
    {
        if (!Options::isPaired()) {
            return false;
        }
        if (!DomainLock::looksLikeNonProd()) {
            return false;
        }
        $current = DomainLock::currentHost();
        $locked  = DomainLock::lockedHost();
        if ($current === '' || $locked === '') {
            return false;
        }

        // We only offer promotion if THIS site is currently the
        // canonical (otherwise the canonical is already prod, and
        // dev is a no-op clone; nothing to promote).
        return $current === $locked;
    }

    /**
     * Production-host candidates the gateway has observed for this
     * tenant, filtered down to ones that don't look like non-prod
     * subdomains. Reads the cached snapshot transient written by
     * {@see RemoteConfig::pull()}.
     *
     * @return array<int,string>
     */
    public static function productionCandidates(): array
    {
        $snap = RemoteConfig::cached();
        if (!is_array($snap) || empty($snap['observed_domains']) || !is_array($snap['observed_domains'])) {
            return [];
        }
        $candidates = [];
        foreach ($snap['observed_domains'] as $entry) {
            $host = '';
            if (is_string($entry)) {
                $host = $entry;
            } elseif (is_array($entry) && !empty($entry['host']) && is_string($entry['host'])) {
                $host = (string) $entry['host'];
            }
            $host = strtolower(trim($host));
            if ($host === '') {
                continue;
            }
            $left = explode('.', $host)[0] ?? '';
            $blockers = ['dev', 'staging', 'stage', 'test', 'testing', 'qa', 'preview', 'beta', 'preprod', 'pre-prod', 'sandbox'];
            if (in_array($left, $blockers, true)) {
                continue;
            }
            $candidates[$host] = true;
        }

        return array_keys($candidates);
    }

    public static function handle(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Insufficient permissions.', 'seekmodo'), '', ['response' => 403]);
        }
        if (!function_exists('check_admin_referer')) {
            return;
        }
        check_admin_referer(self::NONCE_NAME);

        $raw = isset($_POST['new_host']) ? (string) wp_unslash($_POST['new_host']) : '';
        $new_host = self::sanitizeHost($raw);
        if ($new_host === '') {
            self::redirectToSettings('error', 'invalid_host');
        }

        // Disallow promoting to a host that itself looks like a
        // non-prod subdomain — operator probably mistyped.
        $left = explode('.', $new_host)[0] ?? '';
        $blockers = ['dev', 'staging', 'stage', 'test', 'testing', 'qa', 'preview', 'beta', 'preprod', 'pre-prod', 'sandbox'];
        if (in_array($left, $blockers, true)) {
            self::redirectToSettings('error', 'host_looks_nonprod');
        }

        $resp = Client::promoteCanonicalDomain($new_host);
        if (!is_array($resp) || empty($resp['ok'])) {
            self::redirectToSettings('error', 'gateway_rejected');
        }

        // Mirror locally so the operator's next page-load on the
        // dev site shows the canonical pointing at prod (without
        // waiting up to 5 min for the snapshot cron). The next pull
        // will reaffirm the same value so this is a safe optimistic
        // write.
        update_option(Options::OPT_STOREFRONT_HOST, $new_host);

        self::redirectToSettings('success', 'promoted');
    }

    /**
     * Loose host sanitizer — accepts a hostname with optional port
     * trimmed off, lowercases, rejects anything obviously non-host
     * (slashes, schemes, query strings).
     */
    private static function sanitizeHost(string $raw): string
    {
        $raw = trim($raw);
        if ($raw === '') {
            return '';
        }
        // If the user pasted a URL, pull just the host component.
        if (preg_match('~^https?://~i', $raw)) {
            $parsed = wp_parse_url($raw, PHP_URL_HOST);
            $raw = is_string($parsed) ? $parsed : '';
        }
        $raw = strtolower($raw);
        // Strip a port if present.
        $raw = preg_replace('~:\d+$~', '', $raw) ?? $raw;
        // Reject anything that still has a slash or whitespace.
        if ($raw === '' || preg_match('~[\s/?#@]~', $raw)) {
            return '';
        }
        // Loose hostname shape: at least one dot, no leading dot,
        // labels are alnum/hyphen.
        if (!preg_match('~^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$~', $raw)) {
            return '';
        }

        return $raw;
    }

    private static function redirectToSettings(string $status, string $code): void
    {
        $url = add_query_arg(
            ['page' => SettingsPage::SLUG, 'seekmodo_status' => $status, 'seekmodo_code' => $code],
            admin_url('options-general.php')
        );
        wp_safe_redirect($url, 302);
        exit;
    }
}
