<?php
/**
 * FajaedeSEO Sitemap Generator
 *
 * Handles the generation of XML sitemaps, including sitemap indexes
 * and individual sitemaps for different content types.
 */

defined('ABSPATH') or exit;

class FajaedeSEO_Sitemap_Generator {

    const SITEMAP_MAX_URLS = 50000;
    const SITEMAP_INDEX_SLUG = 'fajaede-sitemap-index.xml';
    const SITEMAP_BASE_SLUG = 'fajaede-sitemap';

    private function get_max_urls_limit() {
        return (class_exists('\FajaedeSEO_Pro_License') && \FajaedeSEO_Pro_License::is_pro()) ? self::SITEMAP_MAX_URLS : 500;
    }

    public function __construct() {
    }

    public function register_hooks() {
        add_action('init', [$this, 'add_rewrite_rules']);
        add_filter('query_vars', [$this, 'register_query_vars']);
        add_action('template_redirect', [$this, 'maybe_render_sitemap'], 0);
        add_filter('redirect_canonical', [$this, 'disable_canonical_for_sitemaps'], 10, 2);
    }

    public function add_rewrite_rules() {
        add_rewrite_rule(
            '^' . preg_quote(self::SITEMAP_INDEX_SLUG, '/') . '$',
            'index.php?fajaede_sitemap=index',
            'top'
        );

        add_rewrite_rule(
            '^' . preg_quote(self::SITEMAP_BASE_SLUG, '/') . '-([a-z0-9_-]+)-([0-9]+)\.xml$',
            'index.php?fajaede_sitemap=$matches[1]&fajaede_sitemap_page=$matches[2]',
            'top'
        );

        add_rewrite_rule(
            '^' . preg_quote(self::SITEMAP_BASE_SLUG, '/') . '$',
            'index.php?fajaede_sitemap=index',
            'top'
        );
    }

    /**
     * Registers the query vars for the sitemaps.
     *
     * @param array $vars The current query variables.
     * @return array
     */
    public function register_query_vars($vars) {
        $vars[] = 'fajaede_sitemap';
        $vars[] = 'fajaede_sitemap_page';
        return $vars;
    }

    public function maybe_render_sitemap() {
        $sitemap = get_query_var('fajaede_sitemap');
        if (!$sitemap) return;

        if ($sitemap === 'index') {
            $content = $this->get_sitemap_index_cache();
            if (false === $content) {
                $content = $this->generate_sitemap_index();
                $this->set_sitemap_index_cache($content);
            }
            $this->output_xml($content, 200);
        }

        $page = max(1, (int) get_query_var('fajaede_sitemap_page'));
        $type = sanitize_key($sitemap);

        if ($type !== 'custom' && (!post_type_exists($type) || !is_post_type_viewable($type))) {
            status_header(404);
            $this->output_xml(
                '<?xml version="1.0" encoding="UTF-8"?><error>Post type not found</error>',
                404
            );
        }

        $content = $this->get_sitemap_cache($type, $page);
        if (false === $content) {
            $content = $this->generate_specific_sitemap($type, $page);
            $this->set_sitemap_cache($type, $page, $content);
        }
        $this->output_xml($content, 200);
    }

    /**
     * Disables canonical redirects for sitemaps.
     *
     * @param string $redirect_url  The redirect URL.
     * @param string $requested_url The requested URL.
     * @return string|false
     */
    public function disable_canonical_for_sitemaps($redirect_url, $requested_url) {
        if (get_query_var('fajaede_sitemap')) return false;
        return $redirect_url;
    }

    /**
     * Generates the sitemap index file.
     *
     * @return string The XML content of the sitemap index.
     */
    public function generate_sitemap_index() {
        ob_start();
        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

        // Alleen publiek zichtbare post types die niet 'attachment' zijn
        $post_types = get_post_types(['public' => true, 'exclude_from_search' => false], 'names');
        unset($post_types['attachment']); // Attachments are generally not included in sitemaps

        foreach ($post_types as $post_type) {
            $total_urls = $this->count_urls_for_type($post_type);
            $limit = $this->get_max_urls_limit();
            $total_sitemaps = ceil($total_urls / $limit);

            // Limit free users to 1 sitemap page (max URLs limit) per post type
            if (!class_exists('\FajaedeSEO_Pro_License') || !\FajaedeSEO_Pro_License::is_pro()) {
                $total_sitemaps = 1;
            }

            for ($i = 1; $i <= $total_sitemaps; $i++) {
                echo "\n\t<sitemap>";
                echo '<loc>' . esc_url(home_url('/' . self::SITEMAP_BASE_SLUG . '-' . $post_type . '-' . $i . '.xml')) . '</loc>';
                echo '<lastmod>' . esc_html(gmdate('c')) . '</lastmod>';
                echo "</sitemap>";
            }
        }

        // Add Custom External URLs (Premium/Agency feature)
        if (class_exists('\FajaedeSEO_Pro_License') && in_array(\FajaedeSEO_Pro_License::get_plan_level(), ['premium', 'agency'])) {
            $custom_urls_option = get_option('fajaedeseo_custom_sitemap_urls', '');
            if (!empty(trim($custom_urls_option))) {
                echo "\n\t<sitemap>";
                echo '<loc>' . esc_url(home_url('/' . self::SITEMAP_BASE_SLUG . '-custom-1.xml')) . '</loc>';
                echo '<lastmod>' . esc_html(gmdate('c')) . '</lastmod>';
                echo "</sitemap>";
            }
        }

        // Add taxonomies later if needed

        echo '</sitemapindex>';
        return ob_get_clean();
    }

    /**
     * Generates a specific sitemap file for a given type and page.
     *
     * @param string $type The post type or taxonomy slug.
     * @param int $page The sitemap page number.
     * @return string The XML content of the specific sitemap.
     */
    public function generate_specific_sitemap($type, $page) {
        $limit = $this->get_max_urls_limit();
        
        // Prevent free users from accessing page 2+
        if (!class_exists('\FajaedeSEO_Pro_License') || !\FajaedeSEO_Pro_License::is_pro()) {
            if ($page > 1) {
                $this->output_xml('<?xml version="1.0" encoding="UTF-8"?><error>Pro license required to index more than ' . $limit . ' URLs per post type.</error>', 403);
            }
        }

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

        if ($type === 'custom') {
            if (class_exists('\FajaedeSEO_Pro_License') && in_array(\FajaedeSEO_Pro_License::get_plan_level(), ['premium', 'agency'])) {
                $custom_urls_option = get_option('fajaedeseo_custom_sitemap_urls', '');
                if (!empty($custom_urls_option)) {
                    $urls = array_filter(array_map('trim', explode("\n", $custom_urls_option)));
                    $urls = array_slice($urls, ($page - 1) * $limit, $limit); // Paginatie voor custom URLs
                    foreach ($urls as $custom_url) {
                        if (filter_var($custom_url, FILTER_VALIDATE_URL)) {
                            echo "\n\t<url>";
                            echo '<loc>' . esc_url($custom_url) . '</loc>';
                            echo '<lastmod>' . esc_html(gmdate('c')) . '</lastmod>';
                            echo '<changefreq>monthly</changefreq>';
                            echo '<priority>0.8</priority>';
                            echo "</url>";
                        }
                    }
                }
            }
        } else {
            $args = [
                'post_type' => $type, // Use the type from the URL
                'post_status' => 'publish',
                'posts_per_page' => $limit,
                'paged' => $page,
                'has_password' => false, // Nooit met wachtwoord beveiligde content tonen
                'orderby' => 'modified',
                'order' => 'DESC',
                'no_found_rows' => true, // Optimization
                'update_post_meta_cache' => true,
                'update_post_term_cache' => false, // Optimization
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Noodzakelijk om noindex posts uit te sluiten. Resultaat wordt gecachet.
                'meta_query' => [
                    'relation' => 'OR',
                    ['key' => '_fajaedeseo_robots_index', 'compare' => 'NOT EXISTS'],
                    ['key' => '_fajaedeseo_robots_index', 'value' => 'noindex', 'compare' => '!=']
                ]
            ];

            $posts = new \WP_Query($args);

            if ($posts->have_posts()) {
                while ($posts->have_posts()) {
                    $posts->the_post();
                    echo "\n\t<url>";
                    echo '<loc>' . esc_url(get_permalink()) . '</loc>';
                    echo '<lastmod>' . esc_html(get_the_modified_date('c')) . '</lastmod>';
                    echo '<changefreq>weekly</changefreq>';
                    echo '<priority>0.8</priority>';
                    echo "</url>";
                }
                wp_reset_postdata();
            }
        }

        echo '</urlset>';
        return ob_get_clean();
    }

    /**
     * Counts the total number of URLs for a given post type.
     *
     * @param string $post_type The post type slug.
     * @return int The total number of URLs.
     */
    private function count_urls_for_type($post_type) {
        $count_posts = wp_count_posts($post_type);
        return $count_posts->publish;
    }

    /**
     * Outputs XML content with appropriate headers.
     *
     * @param string $content The XML content to output.
     */
    private function output_xml($content, $status_code = 200) {
        if ( empty( $content ) ) {
            status_header( 404 );
            exit;
        }
        if (!headers_sent()) {
            while (ob_get_level()) ob_end_clean(); // Garandeer GEEN witruimte voor <?xml
            status_header($status_code);
            header('Content-Type: application/xml; charset=' . get_option('blog_charset'));
            header('X-Robots-Tag: noindex, follow', true);
            header('Cache-Control: no-cache, must-revalidate, max-age=0'); // phpcs:ignore WordPress.VIP.Headers.CacheControlOverride.CacheControlOverride
        } 
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Content is already properly escaped XML.
        echo $content; 
        exit;
    }

    /**
     * Retrieves the sitemap index from cache.
     *
     * @return string|false Cached sitemap index or false if not found.
     */
    private function get_sitemap_index_cache() {
        return get_transient('fajaedeseo_sitemap_index_cache');
    }

    /**
     * Stores the sitemap index in cache.
     *
     * @param string $content The sitemap index XML content.
     */
    private function set_sitemap_index_cache($content) {
        set_transient('fajaedeseo_sitemap_index_cache', $content, DAY_IN_SECONDS);
        update_option('fajaedeseo_sitemap_last_updated', time());
    }

    /**
     * Retrieves a specific sitemap from cache.
     *
     * @param string $type The post type or taxonomy slug.
     * @param int $page The sitemap page number.
     * @return string|false Cached sitemap or false if not found.
     */
    private function get_sitemap_cache($type, $page) {
        return get_transient('fajaedeseo_sitemap_' . $type . '_' . $page . '_cache');
    }

    /**
     * Stores a specific sitemap in cache.
     *
     * @param string $type The post type or taxonomy slug.
     * @param int $page The sitemap page number.
     * @param string $content The sitemap XML content.
     */
    private function set_sitemap_cache($type, $page, $content) {
        set_transient('fajaedeseo_sitemap_' . $type . '_' . $page . '_cache', $content, DAY_IN_SECONDS);
    }

    /**
     * Clears all sitemap related caches.
     */
    public function clear_sitemap_caches() {
        // 1. Zorg ervoor dat de index wordt verwijderd (ook uit RAM/Redis!)
        delete_transient('fajaedeseo_sitemap_index_cache');
        
        // 2. Specifieke sitemaps wissen via WordPress cache engine (ondersteuning tot 1 Miljoen urls)
        $post_types = get_post_types(['public' => true], 'names');
        $post_types[] = 'custom'; // Zorg dat ook de custom sitemap uit de cache gegooid wordt
        foreach ($post_types as $post_type) {
            for ($i = 1; $i <= 20; $i++) {
                delete_transient('fajaedeseo_sitemap_' . $post_type . '_' . $i . '_cache');
            }
        }

        // 3. Fallback voor systemen zonder actieve Object Cache
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query(
            $wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", '_transient_fajaedeseo_sitemap_%', '_transient_timeout_fajaedeseo_sitemap_%')
        );

        delete_option('fajaedeseo_sitemap_last_updated');
    }

    /**
     * Regenerates the entire sitemap cache.
     *
     * @param bool $store_in_db Whether to store the sitemap in the database.
     * @return string The generated sitemap XML.
     */
    public function regenerate_sitemap_cache($store_in_db = true) {
        $this->clear_sitemap_caches(); // Zorg dat ook alle sub-sitemaps (posts/pages) gewist worden

        $sitemap_xml = $this->generate_sitemap_index();
        
        if ($store_in_db) {
            $this->set_sitemap_index_cache($sitemap_xml);
        }
        
        return $sitemap_xml;
    }
}