<?php

namespace WPSN\Modules\IndexNow;

use WPSN\Core\AbstractModule;
use WPSN\Core\Container;
use WPSN\Core\Queue\QueueInterface;

defined('ABSPATH') || exit;

/**
 * IndexNow integration.
 *
 * - Serves /{key}.txt (virtual) for IndexNow key verification
 * - Optionally submits URLs on content/SEO changes via queue
 */
final class IndexNowModule extends AbstractModule
{
    private IndexNowSecretsRepository $secrets;

    public function __construct(Container $container)
    {
        parent::__construct($container);
        $this->secrets = new IndexNowSecretsRepository();
    }

    public function id(): string
    {
        return 'indexnow';
    }

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

    public function description(): string
    {
        return __('Instant URL submission to IndexNow (Bing and other engines).', 'wp-seo-next');
    }

    public function defaultEnabled(): bool
    {
        return false;
    }

    public function register(): void
    {
        add_action('init', [IndexNowInstaller::class, 'maybeInstall'], 1);

        // Serve /{key}.txt (virtual).
        add_action('template_redirect', [$this, 'maybeServeKeyFile'], 0);

        // Queue submission handlers.
        $jobs = new IndexNowJobs($this->settings, $this->logger);
        add_filter('wpsn_queue_handle_' . IndexNowJobs::JOB_SUBMIT, [$jobs, 'handleSubmit'], 10, 3);


        // Auto-submit hooks (opt-in via settings).
        add_action('save_post', [$this, 'maybeQueuePost'], 100, 2);
        add_action('transition_post_status', [$this, 'maybeQueueTransition'], 100, 3);

        add_action('edited_term', [$this, 'maybeQueueTerm'], 100, 3);
        add_action('created_term', [$this, 'maybeQueueTerm'], 100, 3);

        add_action('seo/meta/updated', [$this, 'handleMetaUpdated'], 10, 2);

        add_action('seo/redirect/created', [$this, 'handleRedirectChanged'], 10, 2);
        add_action('seo/redirect/updated', [$this, 'handleRedirectChanged'], 10, 2);

        add_action('wpsn/internal_links/applied', [$this, 'handleInternalLinksApplied'], 10, 2);

        if (is_admin()) {
            $admin = new Admin\IndexNowAdmin($this->container, $this->logger);
            $admin->register();
        }
    }

    public function maybeServeKeyFile(): void
    {
        $key = $this->secrets->getKey();
        if ($key === '') {
            return;
        }

        $req = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
        $path = wp_parse_url($req, PHP_URL_PATH);
        $path = is_string($path) ? $path : '';

        $basePath = wp_parse_url(home_url('/'), PHP_URL_PATH);
        $basePath = is_string($basePath) ? $basePath : '';
        $basePath = rtrim($basePath, '/');
        $expected = ($basePath !== '' ? $basePath : '') . '/' . $key . '.txt';
        if ($path !== $expected) {
            return;
        }

        // Output key.
        nocache_headers();
        header('Content-Type: text/plain; charset=utf-8');
        echo $key;
        exit;
    }

    /**
     * @param int $postId
     * @param \WP_Post $post
     */
    public function maybeQueuePost(int $postId, $post): void
    {
        if (!$this->shouldAutoSubmit()) {
            return;
        }
        if (wp_is_post_autosave($postId) || wp_is_post_revision($postId)) {
            return;
        }

        // Only published, public posts.
        if (!is_object($post) || empty($post->post_status) || (string) $post->post_status !== 'publish') {
            return;
        }

        $typeObj = get_post_type_object((string) $post->post_type);
        if (!$typeObj || empty($typeObj->public)) {
            return;
        }

        $url = get_permalink($postId);
        if (!$url) {
            return;
        }

        $this->queueUrls([$url], 'post_save');
    }

    /**
     * @param string $newStatus
     * @param string $oldStatus
     * @param \WP_Post $post
     */
    public function maybeQueueTransition(string $newStatus, string $oldStatus, $post): void
    {
        if (!$this->shouldAutoSubmit()) {
            return;
        }

        if (!is_object($post) || empty($post->ID)) {
            return;
        }

        // Publish or unpublish transitions.
        if ($newStatus === $oldStatus) {
            return;
        }

        if ($newStatus !== 'publish' && $oldStatus !== 'publish') {
            return;
        }

        $typeObj = get_post_type_object((string) $post->post_type);
        if (!$typeObj || empty($typeObj->public)) {
            return;
        }

        $url = get_permalink((int) $post->ID);
        if (!$url) {
            return;
        }

        $this->queueUrls([$url], 'post_transition');
    }

    /**
     * @param int $termId
     * @param int $ttId
     * @param string $taxonomy
     */
    public function maybeQueueTerm(int $termId, int $ttId, string $taxonomy): void
    {
        if (!$this->shouldAutoSubmit()) {
            return;
        }

        if (!$this->settings->get('indexnow.submit_terms', true)) {
            return;
        }

        $tax = get_taxonomy($taxonomy);
        if (!$tax || empty($tax->public)) {
            return;
        }

        $link = get_term_link($termId, $taxonomy);
        if (is_wp_error($link)) {
            return;
        }

        $this->queueUrls([(string) $link], 'term_update');
    }

    /**
     * @param int $objectId
     * @param string|null $objectType post|term
     */
    public function handleMetaUpdated(int $objectId, ?string $objectType = null): void
    {
        if (!$this->shouldAutoSubmit()) {
            return;
        }

        $type = $objectType ? sanitize_key($objectType) : '';

        if ($type === 'term') {
            if (!$this->settings->get('indexnow.submit_terms', true)) {
                return;
            }
            $this->queueTermById($objectId, 'seo_meta_updated');
            return;
        }

        // Default: post.
        $this->queuePostById($objectId, 'seo_meta_updated');
    }

    /**
     * @param int $redirectId
     * @param array<string,mixed> $data
     */
    public function handleRedirectChanged(int $redirectId, array $data): void
    {
        if (!$this->shouldAutoSubmit()) {
            return;
        }
        if (!$this->settings->get('indexnow.submit_redirects', true)) {
            return;
        }

        $from = isset($data['from_url']) ? (string) $data['from_url'] : '';
        $to = isset($data['to_url']) ? (string) $data['to_url'] : '';

        $urls = [];
        if ($from !== '') {
            $urls[] = home_url($from);
        }
        if ($to !== '') {
            $urls[] = $to;
        }

        if (!empty($urls)) {
            $this->queueUrls($urls, 'redirect_changed');
        }
    }

    public function handleInternalLinksApplied(int $sourcePostId, int $suggestionId): void
    {
        if (!$this->shouldAutoSubmit()) {
            return;
        }

        $this->queuePostById($sourcePostId, 'internal_links_applied');
    }

    private function shouldAutoSubmit(): bool
    {
        if (!$this->settings->get('indexnow.enabled', false)) {
            return false;
        }
        if (!$this->settings->get('indexnow.auto_submit', true)) {
            return false;
        }
        if (get_option('blog_public') === '0') {
            return false;
        }
        if (!$this->secrets->hasKey()) {
            return false;
        }
        return true;
    }

    private function queuePostById(int $postId, string $reason): void
    {
        $post = get_post($postId);
        if (!$post || $post->post_status !== 'publish') {
            return;
        }

        $typeObj = get_post_type_object((string) $post->post_type);
        if (!$typeObj || empty($typeObj->public)) {
            return;
        }

        $url = get_permalink($postId);
        if (!$url) {
            return;
        }

        $this->queueUrls([(string) $url], $reason);
    }

    private function queueTermById(int $termId, string $reason): void
    {
        $taxes = get_taxonomies(['public' => true], 'names');
        if (!is_array($taxes)) {
            $taxes = [];
        }

        foreach ($taxes as $tax) {
            $term = get_term($termId, $tax);
            if (!$term || is_wp_error($term)) {
                continue;
            }
            $link = get_term_link((int) $termId, (string) $tax);
            if (is_wp_error($link)) {
                continue;
            }
            $this->queueUrls([(string) $link], $reason);
            break;
        }
    }

    /**
     * @param array<int,string> $urls
     */
    private function queueUrls(array $urls, string $reason): void
    {
        /** @var QueueInterface $queue */
        $queue = $this->container->get('queue');

        $dedupeWindow = (int) $this->settings->get('indexnow.dedupe_window', 300);
        $dedupeWindow = max(0, min(3600, $dedupeWindow));

        $clean = [];
        foreach ($urls as $u) {
            $u = is_string($u) ? trim($u) : '';
            if ($u === '') {
                continue;
            }
            $u = esc_url_raw($u);
            if ($u === '') {
                continue;
            }

            // Dedupe within a small time window to avoid spamming.
            if ($dedupeWindow > 0) {
                $tk = 'wpsn_indexnow_q_' . md5($u);
                if (get_transient($tk)) {
                    continue;
                }
                set_transient($tk, 1, $dedupeWindow);
            }

            $clean[] = $u;
        }

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

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

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


    /**
     * Optionally queue sitemap ping jobs when sitemaps are updated.
     *
     * @param string $token Cache bust token.
     * @param array<int,mixed> $args Original hook args.
     */
    public function maybeQueueSitemapPings(string $token = '', array $args = []): void
    {
        // Global toggle for external requests.
        if (!(bool) $this->settings->get('indexnow.enabled', false)) {
            return;
        }

        if (!(bool) $this->settings->get('indexnow.sitemap_pings.enabled', false)) {
            return;
        }

        // Respect WordPress privacy setting.
        if (get_option('blog_public') === '0') {
            return;
        }

        $sp = $this->settings->get('indexnow.sitemap_pings', []);
        $sp = is_array($sp) ? $sp : [];

        $providers = isset($sp['providers']) && is_array($sp['providers']) ? $sp['providers'] : [];
        $provList = [];
        foreach (['bing', 'google'] as $p) {
            if (!empty($providers[$p])) {
                $provList[] = $p;
            }
        }
        if ($provList === []) {
            // Default to Bing if user unchecks everything.
            $provList = ['bing'];
        }

        $sitemapUrls = [];

        if (!empty($sp['include_index'])) {
            $sitemapUrls[] = home_url('/sitemap_index.xml');
        }
        if (!empty($sp['include_news']) && (bool) $this->settings->get('sitemap.news.enabled', false)) {
            $sitemapUrls[] = home_url('/news-sitemap.xml');
        }
        if (!empty($sp['include_video']) && (bool) $this->settings->get('sitemap.video.enabled', false)) {
            $sitemapUrls[] = home_url('/video-sitemap.xml');
        }

        if ($sitemapUrls === []) {
            return;
        }

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

        foreach ($sitemapUrls as $sUrl) {
            $sUrl = is_string($sUrl) ? trim($sUrl) : '';
            if ($sUrl === '' || !wp_http_validate_url($sUrl)) {
                continue;
            }

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

                $this->queue->enqueue(IndexNowJobs::JOB_PING_SITEMAP, [
                    'provider' => $provider,
                    'sitemap_url' => $sUrl,
                    'reason' => 'sitemap_bust',
                    'token' => $token,
                ]);
            }
        }
    }
}
