<?php

/**
 * Category Sitemap Provider
 *
 * Generates sitemaps for categories
 *
 * @package ProRank\SEO\Modules\Indexing\Sitemaps
 * @since   0.1.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Indexing\Sitemaps;

use ProRank\SEO\Core\SettingsManager;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}


/**
 * Category sitemap provider class
 */


class CategorySitemapProvider extends BaseSitemapProvider {
    
    /**
     * Settings manager instance
     *
     * @var SettingsManager|null
     */
    protected ?SettingsManager $settings_manager = null;
    
    /**
     * Provider slug
     *
     * @var string
     */
    protected string $slug = 'category';
    
    /**
     * Provider name
     *
     * @var string
     */
    protected string $name = 'Categories';
    
    /**
     * Constructor
     */
    public function __construct() {
        parent::__construct();
        // Settings manager will be injected if needed
    }
    
    /**
     * Check if provider is enabled
     *
     * @return bool
     */
    public function is_enabled(): bool {
        if (!$this->settings_manager) {
            return true; // Default to enabled
        }
        
        $settings = $this->get_sitemap_settings();
        $enabled_taxonomies = $settings['include_taxonomies'] ?? ['category', 'post_tag'];
        
        return in_array($this->slug, $enabled_taxonomies, true);
    }
    
    /**
     * Get total number of pages
     *
     * @return int
     */
    public function get_total_pages(): int {
        $total_terms = $this->get_total_terms();
        return (int) ceil($total_terms / $this->max_entries);
    }
    
    /**
     * Get total number of terms
     *
     * @return int
     */
    protected function get_total_terms(): int {
        return count($this->get_filtered_terms());
    }
    
    /**
     * Generate sitemap XML content
     *
     * @param int $page Page number
     * @return string XML content
     */
    public function generate_xml(int $page = 1): string {
        $urls = $this->get_term_urls($page);
        return $this->generate_urlset_xml($urls);
    }
    
    /**
     * Get term URLs for sitemap
     *
     * @param int $page Page number
     * @return array
     */
    protected function get_term_urls(int $page): array {
        $offset = ($page - 1) * $this->max_entries;
        $terms = array_slice($this->get_filtered_terms(), $offset, $this->max_entries);
        $urls = [];
        
        if (!is_wp_error($terms) && !empty($terms)) {
            foreach ($terms as $term) {
                $url_data = [
                    'loc' => get_term_link($term),
                    'lastmod' => $this->get_term_last_modified($term->term_id),
                    'changefreq' => 'weekly',
                    'priority' => $this->get_term_priority($term)
                ];
                
                // Allow other modules to modify URL data
                $url_data = apply_filters('prorank_seo_sitemap_url_data', $url_data, $term, 'term');
                
                $urls[] = $url_data;
            }
        }
        
        return $urls;
    }
    
    /**
     * Check if empty terms should be excluded
     *
     * @return bool
     */
    protected function should_exclude_empty_terms(): bool {
        if (!$this->settings_manager) {
            return true; // Default to exclude empty
        }
        
        $settings = $this->get_sitemap_settings();
        return $settings['exclude_empty_terms'] ?? true;
    }

    /**
     * Check if noindex terms should be excluded from the sitemap.
     *
     * @return bool
     */
    protected function should_exclude_noindex_terms(): bool {
        $settings = $this->get_sitemap_settings();
        return $settings['exclude_noindex'] ?? true;
    }

    /**
     * Get all terms that should be exposed in the sitemap after applying
     * explicit and effective noindex rules.
     *
     * @return array<int,\WP_Term>
     */
    protected function get_filtered_terms(): array {
        if ($this->should_exclude_noindex_terms() && $this->is_taxonomy_globally_noindexed()) {
            return [];
        }

        $terms = get_terms($this->get_term_query_args());
        if (is_wp_error($terms) || empty($terms) || !is_array($terms)) {
            return [];
        }

        return array_values(
            array_filter(
                $terms,
                fn ($term) => $term instanceof \WP_Term && !$this->should_exclude_term_from_sitemap($term)
            )
        );
    }

    /**
     * Build the base term query arguments for the current taxonomy.
     *
     * @return array
     */
    protected function get_term_query_args(): array {
        $settings = $this->get_sitemap_settings();
        $exclude_terms = $settings['exclude_terms'] ?? '';

        $args = [
            'taxonomy' => $this->slug,
            'hide_empty' => $this->should_exclude_empty_terms(),
            'orderby' => 'count',
            'order' => 'DESC',
        ];

        if (!empty($exclude_terms)) {
            $excluded_ids = array_map('intval', array_filter(explode(',', $exclude_terms)));
            if (!empty($excluded_ids)) {
                $args['exclude'] = $excluded_ids;
            }
        }

        if ($this->should_exclude_noindex_terms()) {
            $args['meta_query'] = $this->get_noindex_exclusion_meta_query();
        }

        return $args;
    }

    /**
     * Get sitemap settings with a stable fallback.
     *
     * @return array
     */
    protected function get_sitemap_settings(): array {
        return $this->resolve_sitemap_settings($this->settings_manager);
    }

    /**
     * Get titles/meta taxonomy settings for the current sitemap taxonomy.
     *
     * @return array
     */
    protected function get_taxonomy_titles_meta_settings(): array {
        $legacy_titles_meta = get_option('prorank_seo_titles_meta', []);
        $legacy_taxonomy_settings = is_array($legacy_titles_meta)
            ? ($legacy_titles_meta['taxonomy_' . $this->slug] ?? [])
            : [];

        $titles_meta = $this->settings_manager ? $this->settings_manager->get_settings('titles_meta') : [];
        $taxonomy_settings = $titles_meta['taxonomy_' . $this->slug] ?? [];

        return array_merge(
            is_array($legacy_taxonomy_settings) ? $legacy_taxonomy_settings : [],
            is_array($taxonomy_settings) ? $taxonomy_settings : []
        );
    }

    /**
     * Get robots/indexing settings with a stable fallback.
     *
     * @return array
     */
    protected function get_robots_indexing_settings(): array {
        $legacy_settings = get_option('prorank_seo_robots_indexing_settings', []);
        $settings = $this->settings_manager ? $this->settings_manager->get_settings('robots_indexing') : [];

        return array_merge(
            is_array($legacy_settings) ? $legacy_settings : [],
            is_array($settings) ? $settings : []
        );
    }

    /**
     * Get the global robots/indexing noindex flag for this taxonomy, if one exists.
     *
     * @return string|null
     */
    protected function get_taxonomy_robots_indexing_flag(): ?string {
        return match ($this->slug) {
            'category' => 'noindex_category',
            'post_tag' => 'noindex_tag',
            default => null,
        };
    }

    /**
     * Check whether the entire taxonomy archive type is configured as noindex.
     *
     * @return bool
     */
    protected function is_taxonomy_globally_noindexed(): bool {
        $taxonomy_settings = $this->get_taxonomy_titles_meta_settings();
        $robots_indexing_flag = $this->get_taxonomy_robots_indexing_flag();
        $robots_indexing = $this->get_robots_indexing_settings();

        if (($taxonomy_settings['robots'] ?? 'default') === 'custom' && !empty($taxonomy_settings['noindex'])) {
            return true;
        }

        return $robots_indexing_flag !== null && !empty($robots_indexing[$robots_indexing_flag]);
    }

    /**
     * Check whether empty archive noindex rules should also exclude this term.
     *
     * @return bool
     */
    protected function should_exclude_effectively_empty_terms(): bool {
        if (!$this->should_exclude_noindex_terms()) {
            return false;
        }

        $titles_meta = $this->settings_manager ? $this->settings_manager->get_settings('titles_meta') : [];

        return $titles_meta['noindex_empty_archives'] ?? true;
    }

    /**
     * Check if a specific term should be excluded from the sitemap.
     *
     * @param \WP_Term $term Term object.
     * @return bool
     */
    protected function should_exclude_term_from_sitemap(\WP_Term $term): bool {
        if ($this->should_exclude_noindex_terms() && $this->is_term_explicitly_noindexed($term)) {
            return true;
        }

        if (!$this->should_exclude_effectively_empty_terms()) {
            return false;
        }

        return $this->get_published_post_count_for_term((int) $term->term_taxonomy_id) === 0;
    }

    /**
     * Count published, publicly queryable posts attached to a term archive.
     *
     * This mirrors frontend indexability more closely than WP's raw term count,
     * which may include relationships that still resolve to an empty archive.
     *
     * @param int $term_taxonomy_id Term taxonomy ID.
     * @return int
     */
    protected function get_published_post_count_for_term(int $term_taxonomy_id): int {
        global $wpdb;

        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT p.ID)
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
                INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
                WHERE tr.term_taxonomy_id = %d
                AND tt.taxonomy = %s
                AND p.post_status = 'publish'
                AND p.post_password = ''
                AND p.post_type NOT IN ('attachment', 'revision', 'nav_menu_item')",
                $term_taxonomy_id,
                $this->slug
            )
        );
    }

    /**
     * Check whether a term is explicitly marked noindex via term meta.
     *
     * Frontend output treats any non-empty truthy value as noindex, and older
     * imports can store values like "noindex" instead of the literal "1".
     *
     * @param \WP_Term $term Term object.
     * @return bool
     */
    protected function is_term_explicitly_noindexed(\WP_Term $term): bool {
        foreach (['_prorank_seo_noindex', '_prorank_seo_robots_noindex', '_prorank_robots_noindex'] as $meta_key) {
            $value = get_term_meta($term->term_id, $meta_key, true);
            if (in_array((string) $value, ['1', 'true', 'yes', 'on', 'noindex'], true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Build a meta query that excludes both current and legacy noindex term meta keys.
     *
     * @return array
     */
    protected function get_noindex_exclusion_meta_query(): array {
        return [
            'relation' => 'AND',
            [
                'relation' => 'OR',
                [
                    'key' => '_prorank_seo_noindex',
                    'value' => '1',
                    'compare' => '!=',
                ],
                [
                    'key' => '_prorank_seo_noindex',
                    'compare' => 'NOT EXISTS',
                ],
            ],
            [
                'relation' => 'OR',
                [
                    'key' => '_prorank_seo_robots_noindex',
                    'value' => '1',
                    'compare' => '!=',
                ],
                [
                    'key' => '_prorank_seo_robots_noindex',
                    'compare' => 'NOT EXISTS',
                ],
            ],
        ];
    }
    
    /**
     * Get term priority based on post count
     *
     * @param \WP_Term $term Term object
     * @return float
     */
    protected function get_term_priority(\WP_Term $term): float {
        // Categories with more posts are considered more important
        if ($term->count > 100) {
            return 0.8;
        } elseif ($term->count > 50) {
            return 0.7;
        } elseif ($term->count > 10) {
            return 0.6;
        } elseif ($term->count > 0) {
            return 0.5;
        }
        
        return 0.3;
    }
    
    /**
     * Get last modified date for a term
     *
     * @param int $term_id Term ID
     * @return string ISO 8601 date
     */
    protected function get_term_last_modified(int $term_id): string {
        global $wpdb;
        
        // Get the most recent post in this category
        $last_post_date = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT MAX(p.post_modified_gmt) 
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
                WHERE tr.term_taxonomy_id = %d
                AND p.post_status = 'publish'
                AND p.post_password = ''",
                $term_id
            )
        );
        
        return $last_post_date ? $this->format_date($last_post_date) : $this->format_date('now');
    }
    
    /**
     * Get last modified date for sitemap
     *
     * @param int $page Page number
     * @return string ISO 8601 date
     */
    protected function get_last_modified(int $page = 1): string {
        global $wpdb;
        
        // Get the most recent post modification across all categories
        $last_modified = $wpdb->get_var(
            "SELECT MAX(post_modified_gmt) 
            FROM {$wpdb->posts} 
            WHERE post_type = 'post' 
            AND post_status = 'publish' 
            AND post_password = ''"
        );
        
        return $last_modified ? $this->format_date($last_modified) : $this->format_date('now');
    }
}
