<?php

/**
 * Base Sitemap Provider
 *
 * Abstract base class for all sitemap providers
 *
 * @package ProRank\SEO\Modules\Indexing\Sitemaps
 * @since   0.1.0
 */

declare(strict_types=1);

namespace ProRank\SEO\Modules\Indexing\Sitemaps;

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


/**
 * Base class for sitemap providers
 */


abstract class BaseSitemapProvider {
    
    /**
     * Provider slug
     *
     * @var string
     */
    protected string $slug = '';
    
    /**
     * Provider name
     *
     * @var string
     */
    protected string $name = '';
    
    /**
     * Maximum entries per sitemap (Google limit: 50,000)
     *
     * @var int
     */
    protected int $max_entries = 1000;
    
    /**
     * Maximum sitemap size in bytes (Google limit: 50MB)
     *
     * @var int
     */
    protected int $max_size_bytes = 52428800; // 50MB
    
    /**
     * Constructor
     */
    public function __construct() {
        // Use default max entries for now
        // Settings will be injected by the module if needed
        $this->max_entries = 1000;
    }
    
    /**
     * Set max entries per sitemap
     *
     * @param int $max_entries Maximum entries per sitemap
     */
    public function set_max_entries(int $max_entries): void {
        $this->max_entries = $max_entries;
    }
    
    /**
     * Set settings manager
     *
     * @param \ProRank\SEO\Core\SettingsManager $settings_manager Settings manager instance
     */
    public function set_settings_manager(\ProRank\SEO\Core\SettingsManager $settings_manager): void {
        // Use reflection to set private property in child classes
        try {
            $reflection = new \ReflectionClass($this);
            if ($reflection->hasProperty('settings_manager')) {
                $property = $reflection->getProperty('settings_manager');
                $property->setAccessible(true);
                $property->setValue($this, $settings_manager);
            }
        } catch (\Exception $e) {
            // Ignore if property doesn't exist or can't be set
        }
    }

    /**
     * Resolve sitemap settings from the grouped settings store with a legacy
     * fallback for older installs that still use the standalone sitemap option.
     *
     * @param \ProRank\SEO\Core\SettingsManager|null $settings_manager Settings manager instance.
     * @return array
     */
    protected function resolve_sitemap_settings(?\ProRank\SEO\Core\SettingsManager $settings_manager): array {
        $flat_legacy = get_option('prorank_seo_sitemap', []);
        $legacy = get_option('prorank_seo_sitemap_settings', []);
        $group = $settings_manager ? (array) $settings_manager->get_settings('sitemaps') : [];

        return array_merge(
            is_array($flat_legacy) ? $flat_legacy : [],
            is_array($legacy) ? $legacy : [],
            $group
        );
    }

    /**
     * Normalize a sitemap priority value.
     *
     * @param mixed $value Raw priority value.
     * @return float|null
     */
    protected function normalize_sitemap_priority($value): ?float {
        if ($value === '' || $value === null) {
            return null;
        }

        if (!is_numeric($value)) {
            return null;
        }

        $priority = round((float) $value, 1);
        if ($priority < 0.0 || $priority > 1.0) {
            return null;
        }

        return $priority;
    }

    /**
     * Get an explicit per-post sitemap priority override.
     *
     * @param int $post_id Post ID.
     * @return float|null
     */
    protected function get_post_priority_override(int $post_id): ?float {
        return $this->normalize_sitemap_priority(
            get_post_meta($post_id, '_prorank_sitemap_priority_override', true)
        );
    }

    /**
     * Check whether a page matches the configured landing-page boost rules.
     *
     * @param \WP_Post $post     Post object.
     * @param array    $settings Sitemap settings.
     * @return bool
     */
    protected function matches_landing_page_priority_rule(\WP_Post $post, array $settings): bool {
        if ($post->post_type !== 'page' || empty($settings['landing_page_priority_enabled'])) {
            return false;
        }

        $raw_patterns = trim((string) ($settings['landing_page_patterns'] ?? ''));
        if ($raw_patterns === '') {
            return false;
        }

        $patterns = preg_split('/[\r\n,]+/', strtolower($raw_patterns)) ?: [];
        $patterns = array_values(array_filter(array_map('trim', $patterns)));

        if (empty($patterns)) {
            return false;
        }

        $permalink = get_permalink($post);
        $path = wp_parse_url(is_string($permalink) ? $permalink : '', PHP_URL_PATH);
        $haystacks = array_filter([
            strtolower((string) $post->post_name),
            strtolower((string) $post->post_title),
            strtolower((string) ($path ?: '')),
        ]);

        foreach ($patterns as $pattern) {
            if ($pattern === '') {
                continue;
            }

            if (ctype_digit($pattern) && (int) $pattern === (int) $post->ID) {
                return true;
            }

            foreach ($haystacks as $haystack) {
                if ($haystack !== '' && str_contains($haystack, $pattern)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Resolve the final priority for a post-based sitemap entry.
     *
     * Explicit per-page overrides win. Landing-page boost rules can then raise
     * the automatic priority for matching pages.
     *
     * @param \WP_Post $post             Post object.
     * @param float    $default_priority Automatic fallback priority.
     * @param array    $settings         Sitemap settings.
     * @return float
     */
    protected function resolve_post_priority(\WP_Post $post, float $default_priority, array $settings = []): float {
        $priority = $this->get_post_priority_override($post->ID) ?? $default_priority;

        if ($this->matches_landing_page_priority_rule($post, $settings)) {
            $landing_priority = $this->normalize_sitemap_priority($settings['landing_page_priority'] ?? null);
            if ($landing_priority !== null) {
                $priority = max($priority, $landing_priority);
            }
        }

        $filtered = apply_filters('prorank_seo_sitemap_priority', $priority, $post, $settings, $default_priority);

        return $this->normalize_sitemap_priority($filtered) ?? $priority;
    }

    /**
     * Check whether a post is explicitly marked noindex.
     *
     * This honors both the current `_prorank_meta_robots` array and the legacy
     * boolean meta keys still present on older installs.
     *
     * @param int|\WP_Post $post Post ID or object.
     * @return bool
     */
    protected function is_post_explicitly_noindexed($post): bool {
        $post = $post instanceof \WP_Post ? $post : get_post((int) $post);
        if (!$post instanceof \WP_Post) {
            return false;
        }

        foreach (['_prorank_seo_noindex', '_prorank_seo_robots_noindex', '_prorank_robots_noindex'] as $meta_key) {
            $value = get_post_meta($post->ID, $meta_key, true);
            if (in_array((string) $value, ['1', 'true', 'yes'], true)) {
                return true;
            }
        }

        $meta_robots = get_post_meta($post->ID, '_prorank_meta_robots', true);
        if (is_array($meta_robots)) {
            return in_array('noindex', array_map('strval', $meta_robots), true);
        }

        if (is_string($meta_robots) && $meta_robots !== '') {
            return str_contains(strtolower($meta_robots), 'noindex');
        }

        return false;
    }

    /**
     * Get ordered post IDs for a sitemap provider.
     *
     * @param string $post_type  Post type slug.
     * @param array  $settings   Sitemap settings.
     * @param array  $query_args Additional WP_Query args.
     * @return int[]
     */
    protected function get_sitemap_post_ids(string $post_type, array $settings = [], array $query_args = []): array {
        $args = array_merge(
            [
                'post_type' => $post_type,
                'post_status' => 'publish',
                'posts_per_page' => -1,
                'fields' => 'ids',
                'has_password' => false,
                'no_found_rows' => true,
                'update_post_meta_cache' => false,
                'update_post_term_cache' => false,
            ],
            $query_args
        );

        $exclude_posts = $settings['exclude_posts'] ?? '';
        if (!empty($exclude_posts)) {
            $excluded_ids = array_map('intval', array_filter(explode(',', $exclude_posts)));
            if (!empty($excluded_ids)) {
                $args['post__not_in'] = $excluded_ids;
            }
        }

        $query = new \WP_Query($args);
        $post_ids = array_map('intval', (array) $query->posts);

        if (!(bool) ($settings['exclude_noindex'] ?? true)) {
            return $post_ids;
        }

        return array_values(
            array_filter(
                $post_ids,
                fn (int $post_id): bool => !$this->is_post_explicitly_noindexed($post_id)
            )
        );
    }
    
    /**
     * Get provider slug
     *
     * @return string
     */
    public function get_slug(): string {
        return $this->slug;
    }
    
    /**
     * Get provider name
     *
     * @return string
     */
    public function get_name(): string {
        return $this->name;
    }
    
    /**
     * Get sitemap filename
     *
     * @param int $page Page number
     * @return string
     */
    public function get_filename(int $page = 1): string {
        $filename = $this->slug . '-sitemap.xml';
        
        if ($page > 1) {
            $filename = $this->slug . '-sitemap' . $page . '.xml';
        }
        
        return $filename;
    }
    
    /**
     * Get sitemap URL
     *
     * @param int $page Page number
     * @return string
     */
    public function get_url(int $page = 1): string {
        return home_url($this->get_filename($page));
    }
    
    /**
     * Check if provider is enabled
     *
     * @return bool
     */
    abstract public function is_enabled(): bool;
    
    /**
     * Get total number of pages
     *
     * @return int
     */
    abstract public function get_total_pages(): int;
    
    /**
     * Generate sitemap XML content
     *
     * @param int $page Page number
     * @return string XML content
     */
    abstract public function generate_xml(int $page = 1): string;
    
    /**
     * Generate sitemap (wrapper for backward compatibility)
     *
     * @param int $page Page number
     * @return string XML content
     */
    public function generate(int $page = 1): string {
        return $this->generate_xml($page);
    }

    /**
     * Generate sitemap with caching
     *
     * Uses transient caching to avoid regenerating sitemaps on every request.
     * Cache is automatically invalidated when content is updated.
     *
     * @param int  $page       Page number
     * @param bool $force_refresh Force regeneration (bypass cache)
     * @return string XML content
     */
    public function generate_cached(int $page = 1, bool $force_refresh = false): string {
        // Check if caching is enabled (default: true)
        $cache_enabled = apply_filters('prorank_seo_sitemap_cache_enabled', true, $this->slug);

        if (!$cache_enabled || $force_refresh) {
            return $this->generate_xml($page);
        }

        // Generate cache key
        $cache_key = $this->get_cache_key($page);

        // Try to get cached sitemap
        $cached = get_transient($cache_key);
        if ($cached !== false) {
            // Cache hit - return cached XML
            return $cached;
        }

        // Cache miss - generate fresh sitemap
        $xml = $this->generate_xml($page);

        // Get cache TTL (default: 1 hour, filterable)
        $cache_ttl = apply_filters('prorank_seo_sitemap_cache_ttl', 3600, $this->slug);

        // Store in cache
        set_transient($cache_key, $xml, $cache_ttl);

        return $xml;
    }

    /**
     * Get cache key for sitemap
     *
     * @param int $page Page number
     * @return string Cache key
     */
    protected function get_cache_key(int $page = 1): string {
        return sprintf('prorank_sitemap_%s_page_%d', $this->slug, $page);
    }

    /**
     * Invalidate sitemap cache
     *
     * Should be called when content changes that would affect the sitemap.
     *
     * @param int|null $page Specific page to invalidate, or null for all pages
     * @return void
     */
    public function invalidate_cache(?int $page = null): void {
        if ($page !== null) {
            // Invalidate specific page
            delete_transient($this->get_cache_key($page));
        } else {
            // Invalidate all pages
            $total_pages = $this->get_total_pages();
            for ($i = 1; $i <= $total_pages; $i++) {
                delete_transient($this->get_cache_key($i));
            }
        }
    }

    /**
     * Invalidate all sitemap caches (static method)
     *
     * Can be called from hooks without provider instance.
     *
     * @return void
     */
    public static function invalidate_all_caches(): void {
        global $wpdb;

        // Delete all ProRank sitemap transients
        $wpdb->query(
            "DELETE FROM {$wpdb->options}
             WHERE option_name LIKE '_transient_prorank_sitemap_%'
             OR option_name LIKE '_transient_timeout_prorank_sitemap_%'"
        );

        // Clear object cache if available
        if (function_exists('wp_cache_flush_group')) {
            wp_cache_flush_group('prorank_sitemaps');
        }
    }
    
    /**
     * Get sitemap entries for index
     *
     * @return array Array of sitemap entries for the index
     */
    public function get_index_entries(): array {
        if (!$this->is_enabled()) {
            return [];
        }
        
        $entries = [];
        $total_pages = $this->get_total_pages();
        
        for ($page = 1; $page <= $total_pages; $page++) {
            $entries[] = [
                'loc' => $this->get_url($page),
                'lastmod' => $this->get_last_modified($page),
            ];
        }
        
        return $entries;
    }
    
    /**
     * Get last modified date for sitemap
     *
     * @param int $page Page number
     * @return string ISO 8601 date
     */
    abstract protected function get_last_modified(int $page = 1): string;
    
    /**
     * Format date for sitemap
     *
     * @param string $date Date to format
     * @return string ISO 8601 formatted date
     */
    protected function format_date(string $date): string {
        $timestamp = strtotime($date);
        if ($timestamp === false) {
            $timestamp = time();
        }
        
        return gmdate('c', $timestamp);
    }
    
    /**
     * Escape URL for XML with validation
     *
     * @param string $url URL to escape
     * @return string Escaped URL
     */
    protected function escape_xml_url(string $url): string {
        // Validate URL format
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            prorank_log('[ProRank SEO] Invalid URL in sitemap: ' . $url);
            return '';
        }
        
        // First decode any already encoded entities
        $url = html_entity_decode($url, ENT_QUOTES | ENT_XML1, 'UTF-8');
        
        // Ensure URL is properly encoded
        $parsed = wp_parse_url($url);
        if ($parsed === false) {
            return '';
        }
        
        // Rebuild URL with proper encoding
        $scheme = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : '';
        $host = isset($parsed['host']) ? $parsed['host'] : '';
        $port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
        $path = isset($parsed['path']) ? $parsed['path'] : '';
        $query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
        $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
        
        // Encode path segments
        if ($path) {
            $path_parts = explode('/', $path);
            $path_parts = array_map('rawurlencode', $path_parts);
            $path = implode('/', $path_parts);
            // Fix double encoding
            $path = str_replace('%2F', '/', $path);
        }
        
        $url = $scheme . $host . $port . $path . $query . $fragment;
        
        // Then encode for XML
        $url = htmlspecialchars($url, ENT_QUOTES | ENT_XML1, 'UTF-8');
        
        return $url;
    }
    
    /**
     * Generate XML for a set of URLs with size validation
     *
     * @param array $urls Array of URL data
     * @return string XML content
     */
    protected function generate_urlset_xml(array $urls): string {
        // Validate URL count (Google limit: 50,000 URLs per sitemap)
        if (count($urls) > 50000) {
            $urls = array_slice($urls, 0, 50000);
            prorank_log('[ProRank SEO] Warning: Sitemap truncated to 50,000 URLs (Google limit)');
        }
        
        $xml = new \XMLWriter();
        $xml->openMemory();
        $xml->setIndent(true);
        
        // Start XML document
        $xml->startDocument('1.0', 'UTF-8');
        
        // Branding comment
        $xml->writeComment(' Generated by ProRank SEO - https://prorank.io ');
        
        // Start urlset
        $xml->startElement('urlset');
        $xml->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
        $xml->writeAttribute('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1');
        $xml->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
        
        foreach ($urls as $url_data) {
            $xml->startElement('url');
            
            // Location (required)
            $xml->writeElement('loc', $this->escape_xml_url($url_data['loc']));
            
            // Last modified (optional)
            if (!empty($url_data['lastmod'])) {
                $xml->writeElement('lastmod', $this->format_date($url_data['lastmod']));
            }
            
            // Change frequency (optional)
            if (!empty($url_data['changefreq'])) {
                $xml->writeElement('changefreq', $url_data['changefreq']);
            }
            
            // Priority (optional)
            if (isset($url_data['priority'])) {
                $xml->writeElement('priority', number_format((float) $url_data['priority'], 1));
            }
            
            // Images (optional) - Updated for 2025 Google requirements
            // Google no longer supports title/caption properties as of 2025
            if (!empty($url_data['images']) && is_array($url_data['images'])) {
                foreach ($url_data['images'] as $image) {
                    $xml->startElement('image:image');
                    $xml->writeElement('image:loc', $this->escape_xml_url($image['loc']));
                    // Removed deprecated properties: title and caption (Google 2025 update)
                    $xml->endElement(); // image:image
                }
            }
            
            // Alternate language versions (hreflang) (optional)
            if (!empty($url_data['alternates']) && is_array($url_data['alternates'])) {
                foreach ($url_data['alternates'] as $alternate) {
                    if (!empty($alternate['hreflang']) && !empty($alternate['href'])) {
                        $xml->startElement('xhtml:link');
                        $xml->writeAttribute('rel', 'alternate');
                        $xml->writeAttribute('hreflang', $alternate['hreflang']);
                        $xml->writeAttribute('href', $this->escape_xml_url($alternate['href']));
                        $xml->endElement(); // xhtml:link
                    }
                }
            }
            
            $xml->endElement(); // url
        }
        
        $xml->endElement(); // urlset
        $xml->endDocument();
        
        $output = $xml->outputMemory();
        
        // Validate sitemap size (Google limit: 50MB uncompressed)
        $size = strlen($output);
        if ($size > $this->max_size_bytes) {
            prorank_log(sprintf(
                '[ProRank SEO] Warning: Sitemap size (%s MB) exceeds Google limit (50MB). Consider reducing entries per sitemap.',
                round($size / 1048576, 2)
            ));
        }
        
        return $output;
    }
}
