<?php
/**
 * Hreflang XML Sitemap — generates multilingual sitemap with xhtml:link entries
 */
if (!defined('ABSPATH')) exit;

class LF_Sitemap {
    private static $instance = null;
    public static function instance() { if (!self::$instance) self::$instance = new self(); return self::$instance; }

    // #15: paginate the hreflang sitemap. The old single file was hard-capped
    // at LIMIT 5000 — sites with more translated posts silently lost pages.
    // Now `/sitemap-hreflang.xml` is an index of `/sitemap-hreflang-N.xml`
    // pages of PAGE_SIZE entries each.
    const PAGE_SIZE = 1000;

    private function __construct() {
        // Hook into WordPress native sitemap to add hreflang
        add_filter('wp_sitemaps_posts_entry', [$this, 'add_hreflang_to_entry'], 10, 3);

        // Register our own sitemap endpoint
        add_action('init', [$this, 'register_rewrite_rules']);
        add_action('template_redirect', [$this, 'render_sitemap']);
    }

    /**
 * Add hreflang links to WordPress native sitemap entries
 */
    public function add_hreflang_to_entry($entry, $post, $post_type) {
        $translations = LF_Post_Translation::instance()->get_translations($post->ID);
        if (empty($translations)) return $entry;

        $languages = LF_Core::instance()->get_active_languages();
        $default_lang = LF_Core::instance()->get_default_language();

        foreach ($translations as $lang_code => $trans_id) {
            $trans_post = get_post($trans_id);
            if (!$trans_post || $trans_post->post_status !== 'publish') continue;
            $locale = isset($languages[$lang_code]['locale']) ? str_replace('_', '-', $languages[$lang_code]['locale']) : $lang_code;
            $entry['languages'][$locale] = get_permalink($trans_id);
        }

        // Add x-default
        if (isset($translations[$default_lang])) {
            $entry['languages']['x-default'] = get_permalink($translations[$default_lang]);
        }

        return $entry;
    }

    /**
 * Register rewrite rule for /sitemap-hreflang.xml
 */
    public function register_rewrite_rules() {
        // Index: /sitemap-hreflang.xml
        add_rewrite_rule('^sitemap-hreflang\.xml$', 'index.php?lf_hreflang_sitemap=1', 'top');
        // Page: /sitemap-hreflang-N.xml
        add_rewrite_rule('^sitemap-hreflang-([0-9]+)\.xml$', 'index.php?lf_hreflang_sitemap=1&lf_sitemap_page=$matches[1]', 'top');
        add_rewrite_tag('%lf_hreflang_sitemap%', '1');
        add_rewrite_tag('%lf_sitemap_page%', '([0-9]+)');

        // #15: the paged sitemap rule was added in a plugin update. WP doesn't
        // re-run activation hooks on update, so flush ONCE (gated by version) so
        // the new pretty URLs register without a manual Settings→Permalinks save.
        if (function_exists('get_option') && defined('LF_VERSION')
            && get_option('lf_rewrite_version') !== LF_VERSION) {
            flush_rewrite_rules(false);
            update_option('lf_rewrite_version', LF_VERSION, false);
        }
    }

    /** Pages needed for `$total` source posts at PAGE_SIZE each (min 1). */
    public static function total_pages(int $total, int $page_size): int {
        if ($page_size < 1) $page_size = self::PAGE_SIZE;
        return max(1, (int) ceil($total / $page_size));
    }

    /**
 * Render custom hreflang sitemap — index (no page) or a single page.
 */
    public function render_sitemap() {
        if (!get_query_var('lf_hreflang_sitemap')) return;

        header('Content-Type: application/xml; charset=UTF-8');
        header('X-Robots-Tag: noindex');

        $page = (int) get_query_var('lf_sitemap_page');
        if ($page > 0) {
            $this->render_sitemap_page($page);
        } else {
            $this->render_sitemap_index();
        }
        exit;
    }

    /** Count of default-language source posts (the sitemap's base set). */
    private function count_source_posts($default_lang) {
        global $wpdb;
        return (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- own table from $wpdb->prefix; one-time/admin read
            "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
            INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
            WHERE p.post_status = 'publish' AND p.post_type NOT IN ('attachment','revision','nav_menu_item')
            AND pm.meta_key = '_lf_language' AND pm.meta_value = %s",
            $default_lang
        ));
    }

    /** Sitemap INDEX listing the paginated hreflang sub-sitemaps. */
    private function render_sitemap_index() {
        $default_lang = LF_Core::instance()->get_default_language();
        $total = $this->count_source_posts($default_lang);
        $pages = self::total_pages($total, self::PAGE_SIZE);

        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
        for ($i = 1; $i <= $pages; $i++) {
            echo "  <sitemap>\n";
            echo '    <loc>' . esc_url(home_url('/sitemap-hreflang-' . $i . '.xml')) . "</loc>\n";
            echo "  </sitemap>\n";
        }
        echo "</sitemapindex>\n";
    }

    /** Render one page of hreflang <url> entries. */
    private function render_sitemap_page($page) {
        $languages    = LF_Core::instance()->get_active_languages();
        $default_lang = LF_Core::instance()->get_default_language();
        $offset       = ($page - 1) * self::PAGE_SIZE;

        global $wpdb;
        $posts = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- own table from $wpdb->prefix; one-time/admin read
            "SELECT DISTINCT p.ID FROM {$wpdb->posts} p
            INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
            WHERE p.post_status = 'publish' AND p.post_type NOT IN ('attachment','revision','nav_menu_item')
            AND pm.meta_key = '_lf_language' AND pm.meta_value = %s
            ORDER BY p.post_date DESC
            LIMIT %d OFFSET %d",
            $default_lang, self::PAGE_SIZE, $offset
        ));

        $post_ids = array_map(static function ($r) { return (int) $r->ID; }, $posts ?: []);

        // #15 N+1 fix: one batched query loads ALL translation groups for this
        // page, then we prime the post + meta caches for every source post AND
        // its translations so the get_post/get_permalink calls below are
        // cache hits instead of a query per translation.
        $pt = LF_Post_Translation::instance();
        $pt->preload_translations($post_ids);

        $maps    = [];
        $all_ids = $post_ids;
        foreach ($post_ids as $pid) {
            $t = $pt->get_translations($pid); // cache hit after preload
            $maps[$pid] = is_array($t) ? $t : [];
            foreach ($maps[$pid] as $tid) { $all_ids[] = (int) $tid; }
        }
        if (function_exists('_prime_post_caches') && !empty($all_ids)) {
            _prime_post_caches(array_values(array_unique($all_ids)), false, true);
        }

        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . "\n";
        echo '        xmlns:xhtml="http://www.w3.org/1999/xhtml">' . "\n";

        foreach ($post_ids as $post_id) {
            $translations = $maps[$post_id];

            // Only include posts that have at least one translation
            if (count($translations) < 2) continue;

            echo "  <url>\n";
            echo "    <loc>" . esc_url(get_permalink($post_id)) . "</loc>\n";
            echo "    <lastmod>" . esc_html(get_post_modified_time('c', true, $post_id)) . "</lastmod>\n";

            foreach ($translations as $lang_code => $trans_id) {
                // Skip non-published translations (drafts return query-based URLs)
                $trans_post = get_post($trans_id);
                if (!$trans_post || $trans_post->post_status !== 'publish') continue;

                $hreflang = $lang_code;
                if (isset($languages[$lang_code]['locale'])) {
                    $hreflang = strtolower(str_replace('_', '-', $languages[$lang_code]['locale']));
                }
                $trans_url = get_permalink($trans_id);
                echo '    <xhtml:link rel="alternate" hreflang="' . esc_attr($hreflang) . '" href="' . esc_url($trans_url) . '" />' . "\n";
            }

            // x-default
            if (isset($translations[$default_lang])) {
                $default_url = get_permalink($translations[$default_lang]);
                echo '    <xhtml:link rel="alternate" hreflang="x-default" href="' . esc_url($default_url) . '" />' . "\n";
            }

            echo "  </url>\n";
        }

        echo "</urlset>\n";
    }
}
