<?php

namespace WPSN\Modules\AIReadiness;

use WPSN\Core\AbstractModule;
use WPSN\Core\Branding;
use WPSN\Modules\AIReadiness\Admin\AiReadinessAdmin;

defined('ABSPATH') || exit;

/**
 * AI Search Readiness
 *
 * - Serves /llms.txt
 * - Adds optional AI crawler controls in robots.txt
 * - Stores an audit log of policy changes
 */
final class AiReadinessModule extends AbstractModule
{
    public function id(): string
    {
        return 'ai_readiness';
    }

    public function name(): string
    {
        return __('AI Search Readiness', 'wp-seo-next');
    }

    public function description(): string
    {
        return __('Serve llms.txt, control major AI crawlers in robots.txt, and keep a policy audit log.', 'wp-seo-next');
    }

    public function defaultEnabled(): bool
    {
        // Admin-focused, but endpoint/robots output is front-end.
        return true;
    }

    public function register(): void
    {
        // DB installer.
        add_action('init', [PolicyLogInstaller::class, 'maybeInstall'], 1);
        add_action('init', [BotBlockStatsInstaller::class, 'maybeInstall'], 1);

        // /llms.txt endpoint.
        add_action('init', [$this, 'addRewriteRules'], 1);
        add_filter('query_vars', [$this, 'registerQueryVars']);

        // Optional Bot Blocker (server-side) for disallowed AI bots.
        add_action('template_redirect', [$this, 'maybeBlockAiBots'], 0);

        add_action('template_redirect', [$this, 'maybeRenderLlmsTxt'], 0);

        // robots.txt AI crawler controls.
        add_filter('robots_txt', [$this, 'filterRobotsTxt'], 98, 2);

        // Bot blocker stats cleanup (daily).
        add_action('init', [$this, 'maybeScheduleBotBlockStatsPurge'], 5);
        add_action('wpsn_ai_botblock_stats_purge', [$this, 'purgeBotBlockStats']);

        // Admin UI.
        if (is_admin() && !(bool) $this->settings->get('privacy.hide_ai_ui', false)) {
            (new AiReadinessAdmin($this->container))->register();
        }
    }

    public function maybeScheduleBotBlockStatsPurge(): void
    {
        if (wp_next_scheduled('wpsn_ai_botblock_stats_purge')) {
            return;
        }
        wp_schedule_event(time() + 600, 'daily', 'wpsn_ai_botblock_stats_purge');
    }

    public function purgeBotBlockStats(): void
    {
        $retention = (int) $this->settings->get('ai_readiness.bots.blocker.stats_retention_days', 90);
        $retention = max(1, min(365, $retention));

        // Keep it best-effort; ignore failures.
        try {
            (new BotBlockStatsRepository())->purgeOlderThan($retention);
        } catch (\Throwable $e) {
            // no-op
        }
    }

    public function addRewriteRules(): void
    {
        // /llms.txt
        add_rewrite_rule('^llms\\.txt$', 'index.php?wpsn_llms_txt=1', 'top');
    }

    /**
     * @param array<int,string> $vars
     * @return array<int,string>
     */
    public function registerQueryVars(array $vars): array
    {
        $vars[] = 'wpsn_llms_txt';
        return $vars;
    }

    public function maybeRenderLlmsTxt(): void
    {
        $flag = get_query_var('wpsn_llms_txt');
        if ((string) $flag !== '1') {
            return;
        }

        // If module is disabled, serve 404.
        /** @var \WPSN\Core\ModuleRegistry $modules */
        $modules = $this->container->get('modules');
        if (!$modules->isEnabled($this->id())) {
            status_header(404);
            exit;
        }

        if (!$this->settings->get('ai_readiness.llms.enabled', true)) {
            status_header(404);
            exit;
        }

        $service = new LlmsTxtService($this->container);
        $txt = $service->build();

        status_header(200);
        header('Content-Type: text/plain; charset=utf-8');
        // Optional caching (CDN friendly).
        header('Cache-Control: public, max-age=3600');
        echo $txt;
        exit;
    }


    /**
     * Optional server-side bot blocker.
     *
     * Blocks requests from disallowed AI bots based on User-Agent token.
     * Exempts robots.txt and llms.txt so bots can still read your policy.
     */
    public function maybeBlockAiBots(): void
    {
        /** @var \WPSN\Core\ModuleRegistry $modules */
        $modules = $this->container->get('modules');
        if (!$modules->isEnabled($this->id())) {
            return;
        }

        $blockerEnabled = (bool) $this->settings->get('ai_readiness.bots.blocker.enabled', false);
        if (!$blockerEnabled) {
            return;
        }

        // Never block admin, AJAX, REST, cron.
        if (is_admin() || (function_exists('wp_doing_ajax') && wp_doing_ajax())) {
            return;
        }
        if (defined('REST_REQUEST') && REST_REQUEST) {
            return;
        }
        if (defined('DOING_CRON') && DOING_CRON) {
            return;
        }

        // Optional bypass for logged-in users (safe default).
        $allowLoggedIn = (bool) $this->settings->get('ai_readiness.bots.blocker.allow_logged_in', true);
        if ($allowLoggedIn && function_exists('is_user_logged_in') && is_user_logged_in()) {
            return;
        }

        $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
        $path = '';
        if ($uri !== '' && function_exists('wp_parse_url')) {
            $p = wp_parse_url($uri);
            if (is_array($p) && isset($p['path'])) {
                $path = (string) $p['path'];
            }
        }

        // Exempt policy endpoints so bots can read your rules.
        // Support subdirectory installs (e.g. /wp/robots.txt) by matching suffix.
        if ($path !== '' && preg_match('~(?:^|/)(robots\.txt|llms\.txt)$~i', $path)) {
            return;
        }

        $ua = isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '';
        if (trim($ua) === '') {
            return;
        }

        $allow = $this->settings->get('ai_readiness.bots.allow', []);
        if (!is_array($allow)) {
            $allow = [];
        }

        $matchedBot = '';
        $matchedBotKey = '';
        $matchedUaToken = '';

        foreach (AiBotsCatalog::catalog() as $botKey => $bot) {
            $token = isset($bot['ua']) ? (string) $bot['ua'] : '';
            if ($token === '') {
                continue;
            }

            $isAllowed = array_key_exists($botKey, $allow) ? (bool) $allow[$botKey] : (bool) ($bot['default_allow'] ?? true);
            if ($isAllowed) {
                continue;
            }

            if (stripos($ua, $token) !== false) {
                $matchedBotKey = (string) $botKey;
                $matchedBot = isset($bot['label']) ? (string) $bot['label'] : (string) $botKey;
                $matchedUaToken = $token;
                break;
            }
        }

        if ($matchedUaToken === '') {
            return;
        }

        // Optional aggregated stats.
        $statsEnabled = (bool) $this->settings->get('ai_readiness.bots.blocker.stats_enabled', false);
        if ($statsEnabled && $matchedBotKey !== '') {
            try {
                (new BotBlockStatsRepository())->recordHit($matchedBotKey, $matchedBot, $uri, $ua);
            } catch (\Throwable $e) {
                // no-op
            }
        }

        $code = (int) $this->settings->get('ai_readiness.bots.blocker.status_code', 403);
        if (!in_array($code, [403, 429], true)) {
            $code = 403;
        }

        // Be explicit for bots.
        nocache_headers();
        status_header($code);
        header('Content-Type: text/plain; charset=utf-8');
        header('X-Robots-Tag: noindex, nofollow', true);
        header('Vary: User-Agent', true);

        if ($code === 429) {
            $retryAfter = (int) $this->settings->get('ai_readiness.bots.blocker.retry_after', 60);
            $retryAfter = max(1, min(3600, $retryAfter));
            header('Retry-After: ' . (string) $retryAfter, true);
        }

        $msg = "Blocked by Bot Blocker";
        if ($matchedBot !== '') {
            $msg .= ' (' . $matchedBot . ')';
        }

        echo $msg;
        exit;
    }

    public function filterRobotsTxt(string $output, bool $public): string
    {
        if (!$public) {
            return $output;
        }

        // If module is disabled, do nothing.
        /** @var \WPSN\Core\ModuleRegistry $modules */
        $modules = $this->container->get('modules');
        if (!$modules->isEnabled($this->id())) {
            return $output;
        }

        if (!$this->settings->get('ai_readiness.bots.enabled', true)) {
            return $output;
        }

        $allow = $this->settings->get('ai_readiness.bots.allow', []);
        if (!is_array($allow)) {
            $allow = [];
        }

        $blocked = [];
        foreach (AiBotsCatalog::catalog() as $botKey => $bot) {
            $isAllowed = array_key_exists($botKey, $allow) ? (bool) $allow[$botKey] : (bool) ($bot['default_allow'] ?? true);
            if (!$isAllowed) {
                $blocked[$botKey] = $bot;
            }
        }

        if (empty($blocked)) {
            return $output;
        }

        $out = rtrim((string) $output);
        if ($out !== '') {
            $out .= "\n";
        }

        $brand = Branding::brand($this->settings);
        $brand = sanitize_text_field($brand);
        $brand = str_replace(['--', '>', '<'], '', $brand);
        $out .= "\n# " . ($brand !== '' ? ($brand . ' — ') : '') . "AI crawler controls\n";

        foreach ($blocked as $bot) {
            $ua = isset($bot['ua']) ? (string) $bot['ua'] : '';
            if ($ua === '') {
                continue;
            }
            $out .= "User-agent: {$ua}\n";
            $out .= "Disallow: /\n\n";
        }

        return rtrim($out) . "\n";
    }
}
