<?php

namespace WPSN\Modules\IndexNow\Admin;

use WPSN\Core\Http\HttpClient;
use WPSN\Core\Container;
use WPSN\Core\Capabilities;
use WPSN\Core\Branding;
use WPSN\Core\Logger;
use WPSN\Core\Queue\QueueInterface;
use WPSN\Modules\IndexNow\IndexNowJobs;
use WPSN\Modules\IndexNow\IndexNowLogRepository;
use WPSN\Modules\IndexNow\IndexNowSecretsRepository;

defined('ABSPATH') || exit;

final class IndexNowAdmin
{
    private const PAGE_SLUG = 'wpsn-indexnow';
    private const SELFTEST_TTL = 2 * HOUR_IN_SECONDS;

    private Container $container;
    private Logger $logger;

    private IndexNowSecretsRepository $secrets;
    private IndexNowLogRepository $log;

    public function __construct(Container $container, Logger $logger)
    {
        $this->container = $container;
        $this->logger = $logger;

        $this->secrets = new IndexNowSecretsRepository();
        $this->log = new IndexNowLogRepository();
    }

    public function register(): void
    {
        add_action('admin_menu', [$this, 'addMenu']);

        add_action('admin_post_wpsn_indexnow_generate_key', [$this, 'handleGenerateKey']);
        add_action('admin_post_wpsn_indexnow_submit_url', [$this, 'handleSubmitUrl']);
        add_action('admin_post_wpsn_indexnow_bulk_sitemap', [$this, 'handleBulkFromSitemap']);
        add_action('admin_post_wpsn_indexnow_bulk_recent', [$this, 'handleBulkRecent']);
        add_action('admin_post_wpsn_indexnow_ping_sitemap', [$this, 'handlePingSitemap']);

        // Self-test.
        add_action('admin_post_wpsn_indexnow_self_test', [$this, 'handleSelfTest']);
        add_action('admin_post_wpsn_indexnow_self_test_clear', [$this, 'handleSelfTestClear']);

        add_action('admin_notices', [$this, 'adminNotices']);
    }

    private function selfTestKey(): string
    {
        return 'wpsn_indexnow_selftest_' . (string) get_current_blog_id();
    }

    /**
     * @return array<string,mixed>|null
     */
    private function getSelfTest(): ?array
    {
        $v = get_transient($this->selfTestKey());
        return is_array($v) ? $v : null;
    }

    /**
     * @param array<string,mixed> $data
     */
    private function setSelfTest(array $data): void
    {
        set_transient($this->selfTestKey(), $data, self::SELFTEST_TTL);
    }

    private function clearSelfTest(): void
    {
        delete_transient($this->selfTestKey());
    }

    public function handleSelfTest(): void
    {
        if (!Capabilities::canManageIndexNow()) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_indexnow_self_test');

        $this->setSelfTest($this->runSelfTest());

        wp_safe_redirect(admin_url('tools.php?page=' . self::PAGE_SLUG . '&wpsn_notice=selftest_done'));
        exit;
    }

    public function handleSelfTestClear(): void
    {
        if (!Capabilities::canManageIndexNow()) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_indexnow_self_test_clear');

        $this->clearSelfTest();

        wp_safe_redirect(admin_url('tools.php?page=' . self::PAGE_SLUG . '&wpsn_notice=selftest_cleared'));
        exit;
    }

    /**
     * @return array<string,mixed>
     */
    private function runSelfTest(): array
    {
        $results = [];
        $key = $this->secrets->getKey();
        $keyUrl = $key !== '' ? home_url('/' . $key . '.txt') : '';

        /** @var \WPSN\Core\Settings $settings */
        $settings = $this->container->get('settings');
        $public = (int) get_option('blog_public', 1) === 1;
        $localOnly = (bool) $settings->get('privacy.local_only', false);

        $results[] = [
            'label' => __('Key exists', 'wp-seo-next'),
            'ok' => $key !== '',
            'details' => $key !== '' ? __('OK', 'wp-seo-next') : __('Missing (generate a key first).', 'wp-seo-next'),
        ];

        $results[] = [
            'label' => __('Site visibility (blog_public)', 'wp-seo-next'),
            'ok' => $public,
            'details' => $public ? __('Public', 'wp-seo-next') : __('Discourage search engines', 'wp-seo-next'),
        ];

        $results[] = [
            'label' => __('Privacy: Local-only mode', 'wp-seo-next'),
            'ok' => !$localOnly,
            'details' => $localOnly ? __('Enabled (external pings will be blocked).', 'wp-seo-next') : __('Disabled', 'wp-seo-next'),
        ];

        if ($keyUrl !== '') {
            $resp = HttpClient::get($keyUrl, [
                'timeout' => 6,
                'redirection' => 3,
                'headers' => [
                    'User-Agent' => 'WPSN IndexNow SelfTest',
                ],
            ], [
                'service' => 'loopback',
                'context' => 'indexnow_selftest',
                'retries' => 0,
            ]);

            if (is_wp_error($resp)) {
                $results[] = [
                    'label' => __('Key file fetch', 'wp-seo-next'),
                    'ok' => false,
                    'details' => $resp->get_error_message(),
                ];
            } else {
                $code = (int) wp_remote_retrieve_response_code($resp);
                $body = (string) wp_remote_retrieve_body($resp);
                $ok = ($code === 200) && (trim($body) === $key);
                $results[] = [
                    'label' => __('Key file fetch', 'wp-seo-next'),
                    'ok' => $ok,
                    'details' => $ok
                        ? sprintf(__('HTTP %d · Body matches key', 'wp-seo-next'), $code)
                        : sprintf(__('HTTP %d · Body mismatch', 'wp-seo-next'), $code),
                ];
            }
        }

        return [
            'ran_at' => gmdate('Y-m-d H:i:s') . ' UTC',
            'results' => $results,
        ];
    }

    public function addMenu(): void
    {
        add_management_page(
            __('IndexNow', 'wp-seo-next'),
            __('IndexNow', 'wp-seo-next'),
            Capabilities::CAP_INDEXNOW,
            self::PAGE_SLUG,
            [$this, 'renderPage']
        );
    }

    public function adminNotices(): void
    {
        if (empty($_GET['wpsn_notice'])) {
            return;
        }

        $notice = sanitize_key((string) wp_unslash($_GET['wpsn_notice']));
        $messages = [
            'indexnow_key_generated' => __('IndexNow key generated.', 'wp-seo-next'),
            'indexnow_submitted' => __('IndexNow submission queued.', 'wp-seo-next'),
            'indexnow_bulk_queued' => __('IndexNow bulk submission queued.', 'wp-seo-next'),
            'indexnow_ping_queued' => __('Sitemap ping queued.', 'wp-seo-next'),
            'selftest_done' => __('IndexNow self-test completed.', 'wp-seo-next'),
            'selftest_cleared' => __('IndexNow self-test results cleared.', 'wp-seo-next'),
        ];
        $errors = [
            'indexnow_error' => __('IndexNow action failed.', 'wp-seo-next'),
            'indexnow_privacy_blocked' => __('External services are disabled by Local-only mode.', 'wp-seo-next'),
            'indexnow_ping_disabled' => __('Sitemap pings are disabled in Settings → IndexNow. Enable them to use ping buttons.', 'wp-seo-next'),
            'indexnow_sitemap_disabled' => __('That sitemap is disabled in Settings → Sitemap.', 'wp-seo-next'),
        ];

        if ($notice === 'indexnow_bulk_queued' && isset($_GET['count'])) {
            $cnt = max(0, (int) $_GET['count']);
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html(sprintf(__('IndexNow bulk queued: %d URLs.', 'wp-seo-next'), $cnt)) . '</p></div>';
        } elseif ($notice === 'indexnow_ping_queued' && isset($_GET['count'])) {
            $cnt = max(0, (int) $_GET['count']);
            $kind = isset($_GET['kind']) ? sanitize_key((string) wp_unslash($_GET['kind'])) : '';
            $label = $kind === 'news'
                ? __('News sitemap', 'wp-seo-next')
                : ($kind === 'video'
                    ? __('Video sitemap', 'wp-seo-next')
                    : ($kind === 'index'
                        ? __('Sitemap index', 'wp-seo-next')
                        : __('Sitemap', 'wp-seo-next')));
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html(sprintf(__('Queued %1$d sitemap ping request(s) for: %2$s.', 'wp-seo-next'), $cnt, $label)) . '</p></div>';
        } elseif (isset($messages[$notice])) {
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($messages[$notice]) . '</p></div>';
        } elseif (isset($errors[$notice])) {
            echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($errors[$notice]) . '</p></div>';
        }
    }

    public function renderPage(): void
    {
        if (!current_user_can(Capabilities::CAP_INDEXNOW)) {
            wp_die(esc_html__('You do not have permission to access this page.', 'wp-seo-next'));
        }

        $settings = $this->container->get('settings');

        if ((bool) $settings->get('privacy.local_only', false)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_privacy_blocked']));
            exit;
        }
        $enabled = (bool) $settings->get('indexnow.enabled', false);
        $auto = (bool) $settings->get('indexnow.auto_submit', true);

        $key = $this->secrets->getKey();

        $keyLocation = '';
        if ($key !== '') {
            $keyLocation = home_url('/' . rawurlencode($key) . '.txt');
        }

        $endpoint = (string) $settings->get('indexnow.endpoint', 'https://api.indexnow.org/indexnow');

        $selfTest = $this->getSelfTest();
        $selfTestRunUrl = wp_nonce_url(admin_url('admin-post.php?action=wpsn_indexnow_self_test'), 'wpsn_indexnow_self_test');
        $selfTestClearUrl = wp_nonce_url(admin_url('admin-post.php?action=wpsn_indexnow_self_test_clear'), 'wpsn_indexnow_self_test_clear');

        echo '<div class="wrap">';
        echo '<h1>' . esc_html__('IndexNow Center', 'wp-seo-next') . '</h1>';

        echo '<p>' . esc_html__('IndexNow lets you instantly notify participating search engines about URL changes. This module can serve the verification key file and queue submissions when content changes.', 'wp-seo-next') . '</p>';

        echo '<hr />';
        echo '<h2>' . esc_html__('Status', 'wp-seo-next') . '</h2>';
        echo '<p>' . esc_html__('Enabled:', 'wp-seo-next') . ' <strong>' . esc_html($enabled ? 'Yes' : 'No') . '</strong></p>';
        echo '<p>' . esc_html__('Auto-submit:', 'wp-seo-next') . ' <strong>' . esc_html($auto ? 'Yes' : 'No') . '</strong></p>';
        echo '<p>' . esc_html__('Endpoint:', 'wp-seo-next') . ' <code>' . esc_html($endpoint) . '</code></p>';
        $settings = $this->container->get('settings');
        $settingsText = $settings ? Branding::settingsLinkText($settings) : __('Settings', 'wp-seo-next');
        echo '<p>' . esc_html__('Configure these in:', 'wp-seo-next') . ' <a href="' . esc_url(admin_url('admin.php?page=wp-seo-next')) . '">' . esc_html($settingsText) . '</a></p>';

        echo '<hr />';
        echo '<h2>' . esc_html__('Key file', 'wp-seo-next') . '</h2>';

        if ($key === '') {
            echo '<p>' . esc_html__('No key set yet. Generate one, then verify the key URL is publicly accessible.', 'wp-seo-next') . '</p>';
        } else {
            echo '<p>' . esc_html__('Current key:', 'wp-seo-next') . ' <code style="user-select:all;">' . esc_html($key) . '</code></p>';
            echo '<p>' . esc_html__('Key URL:', 'wp-seo-next') . ' <a href="' . esc_url($keyLocation) . '" target="_blank" rel="noopener noreferrer">' . esc_html($keyLocation) . '</a></p>';
        }

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_indexnow_generate_key" />';
        wp_nonce_field('wpsn_indexnow_generate_key');
        submit_button(__('Generate new key', 'wp-seo-next'), 'secondary', 'submit', false);
        echo '</form>';

        echo '<hr />';
        echo '<h2>' . esc_html__('Self-test', 'wp-seo-next') . '</h2>';
        echo '<p class="description">' . esc_html__('Checks that your IndexNow key file is publicly reachable and matches the stored key.', 'wp-seo-next') . '</p>';
        echo '<p style="margin-top:0;">';
        echo '<a class="button button-primary" href="' . esc_url($selfTestRunUrl) . '">' . esc_html__('Run self-test', 'wp-seo-next') . '</a> ';
        if ($selfTest) {
            echo '<a class="button button-secondary" href="' . esc_url($selfTestClearUrl) . '">' . esc_html__('Clear results', 'wp-seo-next') . '</a>';
        }
        echo '</p>';

        if ($selfTest && isset($selfTest['results']) && is_array($selfTest['results'])) {
            $ranAt = isset($selfTest['ran_at']) ? (string) $selfTest['ran_at'] : '';
            if ($ranAt !== '') {
                echo '<p class="description" style="margin-top:0;">' . esc_html(sprintf(__('Last run: %s', 'wp-seo-next'), $ranAt)) . '</p>';
            }

            echo '<table class="widefat striped" style="max-width:920px;">';
            echo '<thead><tr><th>' . esc_html__('Check', 'wp-seo-next') . '</th><th style="width:110px;">' . esc_html__('Result', 'wp-seo-next') . '</th><th>' . esc_html__('Details', 'wp-seo-next') . '</th></tr></thead><tbody>';
            foreach ($selfTest['results'] as $row) {
                if (!is_array($row)) {
                    continue;
                }
                $label = isset($row['label']) ? (string) $row['label'] : '';
                $ok = !empty($row['ok']);
                $details = isset($row['details']) ? (string) $row['details'] : '';
                echo '<tr>';
                echo '<td>' . esc_html($label) . '</td>';
                echo '<td>' . ($ok ? '<span style="color:green;">' . esc_html__('OK', 'wp-seo-next') . '</span>' : '<span style="color:#b32d2e;">' . esc_html__('Fail', 'wp-seo-next') . '</span>') . '</td>';
                echo '<td>' . esc_html($details) . '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        } else {
            echo '<p><em>' . esc_html__('No self-test results yet.', 'wp-seo-next') . '</em></p>';
        }

        echo '<hr />';
        echo '<h2>' . esc_html__('Manual submit', 'wp-seo-next') . '</h2>';
        echo '<p>' . esc_html__('Queue a single URL submission to IndexNow (useful for quick tests).', 'wp-seo-next') . '</p>';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_indexnow_submit_url" />';
        wp_nonce_field('wpsn_indexnow_submit_url');
        echo '<input type="url" name="url" style="min-width:420px;" placeholder="https://example.com/page/" required /> ';
        submit_button(__('Queue submit', 'wp-seo-next'), 'primary', 'submit', false);
        echo '</form>';


        echo '<hr />';
        echo '<h2>' . esc_html__('Bulk submit', 'wp-seo-next') . '</h2>';
        echo '<p>' . esc_html__('Queue many URL submissions at once. This is useful after large migrations, bulk edits, or when you want to (re)notify search engines quickly.', 'wp-seo-next') . '</p>';

        // Bulk: from sitemap.
        echo '<h3>' . esc_html__('From sitemap', 'wp-seo-next') . '</h3>';
        echo '<p>' . esc_html__('Fetch a sitemap (usually sitemap_index.xml) from this site and queue URLs found inside it.', 'wp-seo-next') . '</p>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_indexnow_bulk_sitemap" />';
        wp_nonce_field('wpsn_indexnow_bulk_sitemap');
        echo '<input type="url" name="sitemap_url" style="min-width:420px;" value="' . esc_attr(home_url('/sitemap_index.xml')) . '" required /> ';
        echo '<input type="number" name="limit" min="1" max="50000" value="2000" /> ';
        submit_button(__('Queue from sitemap', 'wp-seo-next'), 'secondary', 'submit', false);
        echo '</form>';


        echo '<hr />';
        echo '<h2>' . esc_html__('Manual sitemap ping', 'wp-seo-next') . '</h2>';
        echo '<p>' . esc_html__('Queue lightweight ping requests for sitemap URLs (useful when you want to notify search engines immediately). Providers are taken from Settings → IndexNow → Sitemap pings.', 'wp-seo-next') . '</p>';

        $sp = $settings->get('indexnow.sitemap_pings', []);
        $sp = is_array($sp) ? $sp : [];
        $pingEnabled = !empty($sp['enabled']);
        $prov = isset($sp['providers']) && is_array($sp['providers']) ? $sp['providers'] : [];
        $provList = [];
        foreach (['bing', 'google'] as $p) {
            if (!empty($prov[$p])) {
                $provList[] = $p;
            }
        }
        if ($provList === []) {
            $provList = ['bing'];
        }

        $newsEnabled = (bool) $settings->get('sitemap.news.enabled', false);
        $videoEnabled = (bool) $settings->get('sitemap.video.enabled', false);
        $indexEnabled = (bool) $settings->moduleEnabled('sitemap', true);

        echo '<p>' . esc_html__('Sitemap pings enabled:', 'wp-seo-next') . ' <strong>' . esc_html($pingEnabled ? 'Yes' : 'No') . '</strong></p>';
        echo '<p>' . esc_html__('Providers:', 'wp-seo-next') . ' <code>' . esc_html(implode(', ', $provList)) . '</code></p>';

        echo '<div style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">';

        $newsBtnAttrs = $newsEnabled ? [] : ['disabled' => 'disabled'];
        $videoBtnAttrs = $videoEnabled ? [] : ['disabled' => 'disabled'];
        $indexBtnAttrs = $indexEnabled ? [] : ['disabled' => 'disabled'];

        // Ping Sitemap index.
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
        echo '<input type="hidden" name="action" value="wpsn_indexnow_ping_sitemap" />';
        echo '<input type="hidden" name="kind" value="index" />';
        wp_nonce_field('wpsn_indexnow_ping_sitemap');
        submit_button(__('Ping Sitemap index', 'wp-seo-next'), 'secondary', 'submit', false, $indexBtnAttrs);
        echo '</form>';

        // Ping News sitemap.
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
        echo '<input type="hidden" name="action" value="wpsn_indexnow_ping_sitemap" />';
        echo '<input type="hidden" name="kind" value="news" />';
        wp_nonce_field('wpsn_indexnow_ping_sitemap');
        submit_button(__('Ping News sitemap', 'wp-seo-next'), 'secondary', 'submit', false, $newsBtnAttrs);
        echo '</form>';

        // Ping Video sitemap.
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
        echo '<input type="hidden" name="action" value="wpsn_indexnow_ping_sitemap" />';
        echo '<input type="hidden" name="kind" value="video" />';
        wp_nonce_field('wpsn_indexnow_ping_sitemap');
        submit_button(__('Ping Video sitemap', 'wp-seo-next'), 'secondary', 'submit', false, $videoBtnAttrs);
        echo '</form>';

        echo '</div>';

        $disabledNotes = [];
        if (!$indexEnabled) {
            $disabledNotes[] = __('The Sitemap module is disabled in Settings → Modules.', 'wp-seo-next');
        }
        if (!$newsEnabled || !$videoEnabled) {
            $disabledNotes[] = __('Some buttons are disabled because the corresponding sitemap is disabled in Settings → Sitemap.', 'wp-seo-next');
        }
        if (!empty($disabledNotes)) {
            echo '<p class="description" style="margin-top:8px;">' . esc_html(implode(' ', $disabledNotes)) . '</p>';
        }

        // Bulk: recently modified posts.
        echo '<h3 style="margin-top:18px;">' . esc_html__('Recently modified', 'wp-seo-next') . '</h3>';
        echo '<p>' . esc_html__('Queue submissions for recently modified published posts/pages (based on post_modified).', 'wp-seo-next') . '</p>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_indexnow_bulk_recent" />';
        wp_nonce_field('wpsn_indexnow_bulk_recent');
        echo '<label>' . esc_html__('Modified in last', 'wp-seo-next') . ' ';
        echo '<input type="number" name="days" min="1" max="365" value="7" style="width:90px;" /> ';
        echo esc_html__('days', 'wp-seo-next') . '</label> ';
        echo '<input type="number" name="limit" min="1" max="50000" value="2000" /> ';
        submit_button(__('Queue recent URLs', 'wp-seo-next'), 'secondary', 'submit', false);
        echo '</form>';

        echo '<hr />';
        echo '<h2>' . esc_html__('Recent IndexNow log', 'wp-seo-next') . '</h2>';

        $page = isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1;
        $perPage = 20;
        $res = $this->log->list($page, $perPage);
        $items = $res['items'] ?? [];
        $total = (int) ($res['total'] ?? 0);

        if (!empty($items)) {
            echo '<table class="widefat striped" style="max-width:1200px;">';
            echo '<thead><tr>';
            echo '<th>' . esc_html__('When', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Status', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('HTTP', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('URLs', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Error', 'wp-seo-next') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($items as $it) {
                echo '<tr>';
                echo '<td>' . esc_html((string) ($it['created_at'] ?? '')) . '</td>';
                echo '<td><code>' . esc_html((string) ($it['status'] ?? '')) . '</code></td>';
                echo '<td>' . esc_html((string) ($it['response_code'] ?? '')) . '</td>';
                echo '<td>' . esc_html((string) ($it['url_count'] ?? '')) . '</td>';
                echo '<td>' . esc_html((string) ($it['error'] ?? '')) . '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';

            $pages = (int) ceil(max(1, $total) / $perPage);
            if ($pages > 1) {
                echo '<p>';
                for ($i = 1; $i <= $pages; $i++) {
                    $u = add_query_arg(['page' => self::PAGE_SLUG, 'paged' => $i], admin_url('tools.php'));
                    if ($i === $page) {
                        echo '<strong style="margin-right:6px;">' . esc_html((string) $i) . '</strong>';
                    } else {
                        echo '<a style="margin-right:6px;" href="' . esc_url($u) . '">' . esc_html((string) $i) . '</a>';
                    }
                }
                echo '</p>';
            }
        } else {
            echo '<p>' . esc_html__('No IndexNow requests logged yet.', 'wp-seo-next') . '</p>';
        }

        echo '</div>';
    }

    public function handleGenerateKey(): void
    {
        if (!current_user_can(Capabilities::CAP_INDEXNOW)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_indexnow_generate_key');

        $this->secrets->generateKey();

        wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_key_generated']));
        exit;
    }

    public function handleSubmitUrl(): void
    {
        if (!current_user_can(Capabilities::CAP_INDEXNOW)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_indexnow_submit_url');

        $settings = $this->container->get('settings');
        if ((bool) $settings->get('privacy.local_only', false)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_privacy_blocked']));
            exit;
        }

        $url = isset($_POST['url']) ? esc_url_raw((string) wp_unslash($_POST['url'])) : '';
        if ($url === '') {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        /** @var QueueInterface $queue */
        $queue = $this->container->get('queue');
        $queue->enqueue(IndexNowJobs::JOB_SUBMIT, ['url_list' => [$url], 'reason' => 'manual']);

        /**
         * Fired when an IndexNow submit is queued (admin/tools).
         *
         * @param int   $count
         * @param string $reason
         * @param array<string,mixed> $meta
         */
        do_action('wpsn/indexnow/submit_queued', 1, 'manual', [
            'source' => 'tools',
            'url' => $url,
        ]);

        wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_submitted']));
        exit;
    }



    public function handleBulkFromSitemap(): void
    {
        if (!current_user_can(Capabilities::CAP_INDEXNOW)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_indexnow_bulk_sitemap');

        $settings = $this->container->get('settings');
        if ((bool) $settings->get('privacy.local_only', false)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_privacy_blocked']));
            exit;
        }
        if (!(bool) $settings->get('indexnow.enabled', false)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        if (get_option('blog_public') === '0') {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        if (!$this->secrets->hasKey()) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $sitemapUrl = isset($_POST['sitemap_url']) ? esc_url_raw((string) wp_unslash($_POST['sitemap_url'])) : '';
        if ($sitemapUrl === '') {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $limit = isset($_POST['limit']) ? (int) $_POST['limit'] : 2000;
        $limit = max(1, min(50000, $limit));

        // Basic SSRF guard: allow only same-host sitemap URLs.
        $homeHost = (string) wp_parse_url(home_url('/'), PHP_URL_HOST);
        $host = (string) wp_parse_url($sitemapUrl, PHP_URL_HOST);
        if ($homeHost !== '' && $host !== '' && strtolower($homeHost) !== strtolower($host)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $urls = $this->collectUrlsFromSitemap($sitemapUrl, $limit);
        if (empty($urls)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $count = $this->queueUrls($urls, 'bulk_sitemap');

        /**
         * Fired when an IndexNow bulk submit is queued from a sitemap.
         *
         * @param int   $count
         * @param string $reason
         * @param array<string,mixed> $meta
         */
        do_action('wpsn/indexnow/submit_queued', (int) $count, 'bulk_sitemap', [
            'source' => 'tools',
            'sitemap_url' => $sitemapUrl,
            'limit' => $limit,
        ]);

        wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_bulk_queued', 'count' => $count]));
        exit;
    }

    public function handleBulkRecent(): void
    {
        if (!current_user_can(Capabilities::CAP_INDEXNOW)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_indexnow_bulk_recent');

        $settings = $this->container->get('settings');
        if ((bool) $settings->get('privacy.local_only', false)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_privacy_blocked']));
            exit;
        }
        if (!(bool) $settings->get('indexnow.enabled', false)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        if (get_option('blog_public') === '0') {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        if (!$this->secrets->hasKey()) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $days = isset($_POST['days']) ? (int) $_POST['days'] : 7;
        $days = max(1, min(365, $days));

        $limit = isset($_POST['limit']) ? (int) $_POST['limit'] : 2000;
        $limit = max(1, min(50000, $limit));

        $postTypes = get_post_types(['public' => true], 'names');
        if (isset($postTypes['attachment'])) {
            unset($postTypes['attachment']);
        }

        $afterTs = time() - ($days * DAY_IN_SECONDS);
        $after = gmdate('Y-m-d H:i:s', $afterTs);

        $q = new \WP_Query([
            'post_type' => array_values($postTypes),
            'post_status' => 'publish',
            'posts_per_page' => $limit,
            'orderby' => 'modified',
            'order' => 'DESC',
            'fields' => 'ids',
            'no_found_rows' => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
            'date_query' => [
                [
                    'after' => $after,
                    'inclusive' => true,
                    'column' => 'post_modified_gmt',
                ],
            ],
        ]);

        $ids = is_array($q->posts) ? $q->posts : [];
        $urls = [];

        foreach ($ids as $postId) {
            $postId = (int) $postId;
            if ($postId <= 0) {
                continue;
            }
            $u = get_permalink($postId);
            if (!is_string($u) || $u === '') {
                continue;
            }
            $urls[] = $u;
        }

        if (empty($urls)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $count = $this->queueUrls($urls, 'bulk_recent');

        /**
         * Fired when an IndexNow bulk submit is queued for recently modified URLs.
         *
         * @param int   $count
         * @param string $reason
         * @param array<string,mixed> $meta
         */
        do_action('wpsn/indexnow/submit_queued', (int) $count, 'bulk_recent', [
            'source' => 'tools',
            'days' => $days,
            'limit' => $limit,
        ]);

        wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_bulk_queued', 'count' => $count]));
        exit;
    }

    public function handlePingSitemap(): void
    {
        if (!current_user_can(Capabilities::CAP_INDEXNOW)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_indexnow_ping_sitemap');

        $settings = $this->container->get('settings');

        // Feature toggle for pings.
        $sp = $settings->get('indexnow.sitemap_pings', []);
        $sp = is_array($sp) ? $sp : [];
        if (empty($sp['enabled'])) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_ping_disabled']));
            exit;
        }

        if (!(bool) $settings->get('indexnow.enabled', false)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        if (get_option('blog_public') === '0') {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $kind = isset($_POST['kind']) ? sanitize_key((string) wp_unslash($_POST['kind'])) : '';
        if ($kind !== 'news' && $kind !== 'video' && $kind !== 'index') {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $sitemapUrl = '';
        if ($kind === 'news') {
            if (!(bool) $settings->get('sitemap.news.enabled', false)) {
                wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_sitemap_disabled']));
                exit;
            }
            $sitemapUrl = home_url('/news-sitemap.xml');
        } elseif ($kind === 'video') {
            if (!(bool) $settings->get('sitemap.video.enabled', false)) {
                wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_sitemap_disabled']));
                exit;
            }
            $sitemapUrl = home_url('/video-sitemap.xml');
        } else {
            $sitemapUrl = home_url('/sitemap_index.xml');
        }

        if (!wp_http_validate_url($sitemapUrl)) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        $prov = isset($sp['providers']) && is_array($sp['providers']) ? $sp['providers'] : [];
        $provList = [];
        foreach (['bing', 'google'] as $p) {
            if (!empty($prov[$p])) {
                $provList[] = $p;
            }
        }
        if ($provList === []) {
            $provList = ['bing'];
        }

        $dedupe = (int) $settings->get('indexnow.dedupe_window', 300);
        $dedupe = max(0, $dedupe);

        /** @var QueueInterface $queue */
        $queue = $this->container->get('queue');

        $queued = 0;
        foreach ($provList as $provider) {
            $key = 'wpsn_sitemap_ping_' . md5($provider . '|' . $sitemapUrl);
            if ($dedupe > 0 && get_transient($key)) {
                continue;
            }
            if ($dedupe > 0) {
                set_transient($key, 1, $dedupe);
            }

            $queue->enqueue(IndexNowJobs::JOB_PING_SITEMAP, [
                'provider' => $provider,
                'sitemap_url' => $sitemapUrl,
                'reason' => 'manual_tools',
            ]);
            $queued++;
        }

        if ($queued < 1) {
            wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_error']));
            exit;
        }

        /**
         * Fired when sitemap ping requests are queued from Tools → IndexNow.
         *
         * @param int $count
         * @param string $kind
         * @param array<string,mixed> $meta
         */
        do_action('wpsn/indexnow/ping_queued', (int) $queued, (string) $kind, [
            'source' => 'tools',
            'providers' => $provList,
            'sitemap_url' => $sitemapUrl,
        ]);

        wp_safe_redirect($this->pageUrl(['wpsn_notice' => 'indexnow_ping_queued', 'kind' => $kind, 'count' => $queued]));
        exit;
    }

    /**
     * @param array<int,string> $urls
     */
    private function queueUrls(array $urls, string $reason): int
    {
        $clean = [];
        foreach ($urls as $u) {
            $u = is_string($u) ? trim($u) : '';
            if ($u === '') {
                continue;
            }

            // Convert relative to absolute.
            if (strlen($u) > 0 && $u[0] === '/') {
                $u = home_url($u);
            }

            $u = esc_url_raw($u);
            if ($u === '') {
                continue;
            }

            $clean[] = $u;
        }

        $clean = array_values(array_unique($clean));
        if (empty($clean)) {
            return 0;
        }

        $settings = $this->container->get('settings');
        $batchSize = (int) $settings->get('indexnow.batch_size', 100);
        $batchSize = max(1, min(10000, $batchSize));

        $dedupe = (int) $settings->get('indexnow.dedupe_window', 300);
        $dedupe = max(0, min(86400, $dedupe));

        // Best-effort dedupe window (avoid re-queue spam).
        if ($dedupe > 0) {
            $tmp = [];
            foreach ($clean as $u) {
                $k = 'wpsn_idx_seen_' . md5($u);
                if (get_transient($k)) {
                    continue;
                }
                set_transient($k, '1', $dedupe);
                $tmp[] = $u;
            }
            $clean = $tmp;
        }

        if (empty($clean)) {
            return 0;
        }

        /** @var QueueInterface $queue */
        $queue = $this->container->get('queue');

        $chunks = array_chunk($clean, $batchSize);
        foreach ($chunks as $chunk) {
            $queue->enqueue(IndexNowJobs::JOB_SUBMIT, [
                'url_list' => array_values($chunk),
                'reason' => $reason,
            ]);
        }

        return count($clean);
    }

    /**
     * @return array<int,string>
     */
    private function collectUrlsFromSitemap(string $sitemapUrl, int $limit): array
    {
        $xml = $this->fetchXml($sitemapUrl);
        if ($xml === '') {
            return [];
        }

        $parsed = $this->parseSitemapXml($xml);
        $type = $parsed['type'] ?? 'unknown';
        $locs = isset($parsed['locs']) && is_array($parsed['locs']) ? $parsed['locs'] : [];

        $urls = [];

        if ($type === 'index') {
            // Fetch children until limit reached.
            $maxChildren = 50;
            $homeHost = (string) wp_parse_url(home_url('/'), PHP_URL_HOST);

            $i = 0;
            foreach ($locs as $childUrl) {
                $i++;
                if ($i > $maxChildren) {
                    break;
                }
                if (count($urls) >= $limit) {
                    break;
                }
                $childUrl = is_string($childUrl) ? trim($childUrl) : '';
                if ($childUrl === '') {
                    continue;
                }

                // Same-host guard for child sitemaps as well.
                $host = (string) wp_parse_url($childUrl, PHP_URL_HOST);
                if ($homeHost !== '' && $host !== '' && strtolower($homeHost) !== strtolower($host)) {
                    continue;
                }

                $childXml = $this->fetchXml($childUrl);
                if ($childXml === '') {
                    continue;
                }
                $childParsed = $this->parseSitemapXml($childXml);
                $childLocs = isset($childParsed['locs']) && is_array($childParsed['locs']) ? $childParsed['locs'] : [];
                foreach ($childLocs as $u) {
                    if (count($urls) >= $limit) {
                        break;
                    }
                    if (is_string($u) && trim($u) !== '') {
                        $urls[] = trim($u);
                    }
                }
            }
        } elseif ($type === 'urlset') {
            $urls = $locs;
        }

        // Clean + limit.
        $clean = [];
        foreach ($urls as $u) {
            $u = is_string($u) ? trim($u) : '';
            if ($u === '') {
                continue;
            }
            $clean[] = $u;
            if (count($clean) >= $limit) {
                break;
            }
        }

        return array_values(array_unique($clean));
    }

    private function fetchXml(string $url): string
    {
        $resp = HttpClient::get($url, [
            'timeout' => 12,
            'redirection' => 3,
            'headers' => [
                'user-agent' => 'WPSN/IndexNow',
                'accept' => 'application/xml,text/xml,*/*;q=0.8',
            ],
        ], [
            'service' => 'loopback',
            'context' => 'indexnow_sitemap_fetch',
            'retries' => 1,
            'retry_statuses' => [500, 502, 503, 504],
        ]);

        if (is_wp_error($resp)) {
            return '';
        }

        $code = (int) wp_remote_retrieve_response_code($resp);
        if ($code < 200 || $code >= 300) {
            return '';
        }

        $body = wp_remote_retrieve_body($resp);
        return is_string($body) ? (string) $body : '';
    }

    /**
     * Parse sitemap XML (urlset or sitemapindex).
     *
     * @return array<string,mixed>
     */
    private function parseSitemapXml(string $xml): array
    {
        $xml = trim($xml);
        if ($xml === '') {
            return ['type' => 'unknown', 'locs' => []];
        }

        // Prefer SimpleXML when available.
        if (function_exists('simplexml_load_string')) {
            $prev = libxml_use_internal_errors(true);
            $sx = simplexml_load_string($xml);
            libxml_use_internal_errors($prev);

            if ($sx !== false) {
                $name = $sx->getName();
                $locs = [];

                if ($name === 'sitemapindex') {
                    foreach ($sx->sitemap as $s) {
                        $loc = isset($s->loc) ? (string) $s->loc : '';
                        if ($loc !== '') {
                            $locs[] = $loc;
                        }
                    }
                    return ['type' => 'index', 'locs' => $locs];
                }

                if ($name === 'urlset') {
                    foreach ($sx->url as $u) {
                        $loc = isset($u->loc) ? (string) $u->loc : '';
                        if ($loc !== '') {
                            $locs[] = $loc;
                        }
                    }
                    return ['type' => 'urlset', 'locs' => $locs];
                }
            }
        }

        // Regex fallback.
        $locs = [];
        if (preg_match_all('#<loc>([^<]+)</loc>#i', $xml, $m)) {
            foreach ($m[1] as $raw) {
                $v = html_entity_decode((string) $raw, ENT_QUOTES);
                $v = trim($v);
                if ($v !== '') {
                    $locs[] = $v;
                }
            }
        }

        $type = (stripos($xml, '<sitemapindex') !== false) ? 'index' : 'urlset';
        return ['type' => $type, 'locs' => $locs];
    }
    /**
     * @param array<string,mixed> $args
     */
    private function pageUrl(array $args = []): string
    {
        $url = admin_url('tools.php?page=' . self::PAGE_SLUG);
        if (!empty($args)) {
            $url = add_query_arg($args, $url);
        }
        return $url;
    }
}
