<?php
/**
 * Google Sitemap Extension
 *
 * Generates an XML sitemap at /sitemap-titancart.xml containing
 * active products, categories, and WordPress pages.
 *
 * Settings read from tc_settings section = 'google_sitemap':
 *   include_products, products_change, products_priority,
 *   include_categories, categories_change, categories_priority,
 *   include_pages, pages_change, pages_priority
 *
 * @package TitanCart\Extensions
 */

namespace TitanCart\Extensions;

use TitanCart\REST\Extensions\ExtensionsController;
use TitanCart\Services\SeoService;

defined( 'ABSPATH' ) || exit;

class SitemapExtension {

    public static function init(): void {
        if ( ! ExtensionsController::is_active( 'sitemap' ) ) {
            return;
        }

        // Register the rewrite rule for /sitemap-titancart.xml
        add_action( 'init', [ self::class, 'add_rewrite_rule' ] );
        add_filter( 'query_vars', [ self::class, 'add_query_var' ] );
        add_action( 'template_redirect', [ self::class, 'maybe_serve_sitemap' ] );

        // Advertise the store sitemap in robots.txt (KI-20) so crawlers
        // auto-discover the catalog everywhere the store runs. WP serves a
        // virtual robots.txt via do_robots() and applies the `robots_txt`
        // filter; we append a Sitemap: line for /sitemap-titancart.xml. Only
        // registered when the sitemap extension is active (this init() bails
        // above otherwise), so the line never advertises a 404 sitemap.
        add_filter( 'robots_txt', [ self::class, 'advertise_sitemap' ], 10, 2 );

        // Flush rewrite rules on activation (only once)
        add_action( 'titancart_activated', [ self::class, 'flush_rules' ] );

        // Auto-flush once when extension is first enabled (transient guard).
        add_action( 'init', [ self::class, 'maybe_flush_rules' ], 99 );
    }

    /* ── Rewrite: /sitemap-titancart.xml → ?tc_sitemap=1 ── */

    public static function add_rewrite_rule(): void {
        add_rewrite_rule(
            '^sitemap-titancart\.xml$',
            'index.php?tc_sitemap=1',
            'top'
        );
    }

    public static function add_query_var( array $vars ): array {
        $vars[] = 'tc_sitemap';
        return $vars;
    }

    public static function flush_rules(): void {
        self::add_rewrite_rule();
        flush_rewrite_rules();
    }

    /**
     * Auto-flush rewrite rules once when the sitemap extension is first enabled.
     * Uses a transient so it only runs once, not on every page load.
     */
    public static function maybe_flush_rules(): void {
        $key = 'tc_sitemap_rules_flushed';
        if ( get_transient( $key ) ) {
            return;
        }

        // Check if our rule is already registered in WP's rewrite rules.
        $rules = get_option( 'rewrite_rules', [] );
        if ( is_array( $rules ) && isset( $rules['^sitemap-titancart\.xml$'] ) ) {
            // Already present — set transient and skip.
            set_transient( $key, '1', DAY_IN_SECONDS * 30 );
            return;
        }

        // Rule missing — flush now.
        self::flush_rules();
        set_transient( $key, '1', DAY_IN_SECONDS * 30 );
    }

    /* ── robots.txt advertisement (KI-20) ── */

    /**
     * Append the store sitemap to the virtual robots.txt.
     *
     * Hooked to WP's `robots_txt` filter (fired by do_robots() when the site
     * serves the dynamic /robots.txt — i.e. no static robots.txt file on disk).
     * Idempotent and only adds the line on a public (search-visible) site;
     * when $public is false WP emits a Disallow-all robots.txt and advertising
     * a sitemap there would be contradictory.
     *
     * robots.txt permits multiple Sitemap: lines, so this coexists with WP
     * core's wp-sitemap.xml line and any theme/SEO-plugin sitemap line.
     *
     * @param string   $output  The robots.txt content assembled so far.
     * @param bool|int  $public  Whether the site is search-engine visible.
     * @return string
     */
    public static function advertise_sitemap( string $output, $public ): string {
        if ( ! $public ) {
            return $output;
        }
        $line = 'Sitemap: ' . esc_url_raw( home_url( '/sitemap-titancart.xml' ) );
        if ( strpos( $output, $line ) !== false ) {
            return $output; // Already advertised — don't duplicate.
        }
        return rtrim( $output, "\n" ) . "\n" . $line . "\n";
    }

    /* ── Serve the sitemap ── */

    public static function maybe_serve_sitemap(): void {
        if ( ! get_query_var( 'tc_sitemap' ) ) {
            return;
        }

        $db = titancart_db();

        // Read settings
        $s = self::settings( $db );

        header( 'Content-Type: application/xml; charset=UTF-8' );
        header( 'X-Robots-Tag: noindex' ); // Sitemap itself shouldn't be indexed

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

        // ── Homepage ──
        self::url_entry( home_url( '/' ), 'daily', '1.0' );

        // ── Products ──
        if ( $s['include_products'] ) {
            // roadmap #15: emit only the representative URL per single-display
            // group (don't list every member). Correlated NOT IN with no bound
            // params — safe to embed directly in the query string.
            $tc_single_excl = \TitanCart\Modules\Products\ProductRepository::single_group_exclusion_sql( 'p' );

            $products = $db->getArray(
                "SELECT p.slug, p.updated_at
                 FROM {$db->table('products')} p
                 WHERE p.status = 'active'
                   AND p.parent_id = 0
                   AND p.deleted_at IS NULL
                   AND {$tc_single_excl}
                 ORDER BY p.updated_at DESC"
            );

            foreach ( $products as $p ) {
                if ( ! empty( $p['slug'] ) ) {
                    self::url_entry(
                        SeoService::product_url( $p['slug'] ),
                        $s['products_change'],
                        $s['products_priority'],
                        $p['updated_at']
                    );
                }
            }
        }

        // ── Categories ──
        if ( $s['include_categories'] ) {
            $categories = $db->getArray(
                "SELECT c.slug, c.updated_at
                 FROM {$db->table('categories')} c
                 WHERE c.status = 'active'
                   AND c.deleted_at IS NULL
                 ORDER BY c.position ASC"
            );

            foreach ( $categories as $c ) {
                if ( ! empty( $c['slug'] ) ) {
                    self::url_entry(
                        SeoService::category_url( $c['slug'] ),
                        $s['categories_change'],
                        $s['categories_priority'],
                        $c['updated_at']
                    );
                }
            }
        }

        // ── WordPress pages ──
        if ( $s['include_pages'] ) {
            $pages = get_pages( [ 'post_status' => 'publish', 'sort_column' => 'menu_order' ] );

            foreach ( $pages as $page ) {
                // Skip the front page (already included as homepage)
                if ( (int) $page->ID === (int) get_option( 'page_on_front' ) ) {
                    continue;
                }

                self::url_entry(
                    get_permalink( $page ),
                    $s['pages_change'],
                    $s['pages_priority'],
                    $page->post_modified
                );
            }
        }

        // ── Shop page ──
        self::url_entry( SeoService::get_shop_url(), 'daily', '0.8' );

        /**
         * Extension hook: contribute additional sitemap URLs.
         *
         * Listeners receive an empty array and return an array of associative
         * arrays, one per URL to emit. Each entry must have a `loc` key (full
         * absolute URL); `changefreq`, `priority`, and `lastmod` are optional
         * and default to 'weekly', '0.5', and null respectively.
         *
         *   add_filter( 'titancart_sitemap_extra_urls', function( array $urls ): array {
         *       $urls[] = [
         *           'loc'        => 'https://example.com/some-page/',
         *           'changefreq' => 'weekly',
         *           'priority'   => '0.6',
         *           'lastmod'    => '2026-05-15 12:00:00', // any strtotime-parseable
         *       ];
         *       return $urls;
         *   } );
         *
         * Used by the marketplace extension at B8 close (S198) to inject vendor
         * storefront URLs (/vendors/{slug}/) for active vendors. Pattern #168
         * third-instance reinforcement (codified at S197 — extending TC core
         * via new optional filter hook with empty-array default).
         *
         * Listeners MUST return an array. Non-array returns are dropped with a
         * silent log; the sitemap still emits cleanly without the extras.
         *
         * @since S198 (Track B Module B8 close).
         *
         * @param array<int, array<string, mixed>> $extra_urls  Default empty.
         */
        $extra_urls = apply_filters( 'titancart_sitemap_extra_urls', [] );
        if ( is_array( $extra_urls ) ) {
            foreach ( $extra_urls as $entry ) {
                if ( ! is_array( $entry ) || empty( $entry['loc'] ) ) {
                    continue;
                }
                self::url_entry(
                    (string) $entry['loc'],
                    (string) ( $entry['changefreq'] ?? 'weekly' ),
                    (string) ( $entry['priority']   ?? '0.5' ),
                    isset( $entry['lastmod'] ) ? (string) $entry['lastmod'] : null
                );
            }
        }

        echo '</urlset>' . "\n";
        exit;
    }

    /* ── Helper: output a single <url> entry ── */

    private static function url_entry( string $loc, string $changefreq = 'weekly', string $priority = '0.5', ?string $lastmod = null ): void {
        echo "  <url>\n";
        echo "    <loc>" . esc_url( $loc ) . "</loc>\n";

        if ( $lastmod ) {
            $ts = strtotime( $lastmod );
            if ( $ts ) {
                echo "    <lastmod>" . esc_html( gmdate( 'Y-m-d', $ts ) ) . "</lastmod>\n";
            }
        }

        echo "    <changefreq>" . esc_html( $changefreq ) . "</changefreq>\n";
        echo "    <priority>" . esc_html( $priority ) . "</priority>\n";
        echo "  </url>\n";
    }

    /* ── Load settings with defaults ── */

    private static function settings( $db ): array {
        $rows = $db->getArray(
            "SELECT `key`, value FROM {$db->table('settings')} WHERE section = ?s",
            'google_sitemap'
        );

        $vals = [];
        foreach ( $rows as $r ) {
            $vals[ $r['key'] ] = $r['value'];
        }

        return [
            'include_products'    => ( $vals['include_products']    ?? '1' ) === '1',
            'products_change'     => $vals['products_change']      ?? 'daily',
            'products_priority'   => $vals['products_priority']    ?? '0.5',
            'include_categories'  => ( $vals['include_categories']  ?? '1' ) === '1',
            'categories_change'   => $vals['categories_change']    ?? 'daily',
            'categories_priority' => $vals['categories_priority']  ?? '0.5',
            'include_pages'       => ( $vals['include_pages']       ?? '1' ) === '1',
            'pages_change'        => $vals['pages_change']         ?? 'daily',
            'pages_priority'      => $vals['pages_priority']       ?? '0.5',
        ];
    }
}
