<?php
/**
 * DBA Validator - Enforces all content quality rules before publication
 * Implements: Damalion Validator Regex Pack Option 1
 *
 * FIXED v2: URL validation now checks ALL damalion.com URLs (not just /blog/)
 *           Uses get_full_registry() instead of get_registry()
 *           Adds allowlist for CTA, pillar, and known structural paths
 */
class DBA_Validator {

    private $warnings = array();

    private static $instance = null;

    // Default banned words (used if none saved in settings)
    private $default_banned_words = array(
        'unlocking','unveiling','navigating','harnessing','demystifying',
        'mastering','empowering','transformative','recoupment','paradigm',
        'ecosystem','synergies','leveraging','optimization','optimisation',
        'framework','utilize','utilise','facilitate','comprehensive',
        'robust','streamline','cutting-edge','landscape','holistic',
        'pivot','scalable','actionable','delve','game-changer',
        'innovative','seamless','deep dive','unleash','elevate',
        'spearhead','foster','bolster','underpin','underscores',
        'realm','beacon','tapestry','multifaceted','intricate',
        'nuanced','commencing','aforementioned','pertaining',
        'therein','thereby','henceforth','notwithstanding',
        'stakeholder','synergy','disruption','disruptive',
        'best-in-class','world-class','next-gen','bleeding-edge',
        'turnkey','end-to-end','one-stop','full-spectrum',
        'value-added','mission-critical','results-driven',
        'proactive','futureproof','future-proof',
        'core structuring tool','structuring vehicle',
        'key instrument','instrumental in',
        // AI-detectable title words — humans spot these instantly
        'regulatory','key considerations','key insights',
        'key benefits','key advantages','key takeaways',
        'essential guide','complete guide','ultimate guide',
        'definitive guide','step-by-step guide',
        'everything you need to know','what you need to know',
        'in-depth look','in-depth guide','in-depth analysis',
        'a closer look','an overview','key factors',
        'key strategies','key aspects','key features',
        'key elements','crucial role','plays a crucial',
        'vital role','pivotal role','key role',
        'strategic importance','strategic overview',
        'key challenges','key opportunities',
    );

    /**
     * URL path patterns that are always allowed (no registry check needed).
     * These are structural/navigation pages, not content pages.
     */
    private $allowed_url_patterns = array(
        '#^/lets-connect/?#i',
        '#^/connectons-nous/?#i',
        '#^/blog/?$#i',
        '#^/about-us/?#i',
        '#^/a-propos-de-nous/?#i',
        '#^/our-expertise/?#i',
        '#^/notre-expertise/?#i',
        '#^/your-career/?#i',
        '#^/votre-carriere/?#i',
        '#^/?$#',                       // Homepage
        '#^/\?lang=[a-z]{2,5}$#i',     // Homepage with lang
    );

    /**
     * Get the active banned words list (from DB or defaults)
     */
    public function get_banned_words() {
        $content_settings = get_option('dba_content_settings', array());
        if (isset($content_settings['banned_words']) && is_array($content_settings['banned_words']) && !empty($content_settings['banned_words'])) {
            return $content_settings['banned_words'];
        }
        return $this->default_banned_words;
    }

    public static function get_instance() {
        if (null === self::$instance) self::$instance = new self();
        return self::$instance;
    }

    private function __construct() {}

    /**
     * Get CTA HTML from settings
     */
    private function get_cta_html() {
        $settings = get_option('dba_content_settings', array());
        return isset($settings['cta_html']) ? $settings['cta_html'] : '';
    }

    /**
     * Get pillar URL from settings
     */
    private function get_pillar_url() {
        $settings = get_option('dba_content_settings', array());
        return isset($settings['pillar_url']) ? $settings['pillar_url'] : '';
    }

    /**
     * Full validation pipeline
     * Returns array of errors (empty = pass)
     * HARD FAILS (block post): JSON parse, missing required keys
     * SOFT WARNINGS (log but proceed): everything else
     */
    public function validate($json_string) {
        $errors = array();

        // 1. JSON-only check
        $raw = trim($json_string);
        if (strpos($raw, '```') !== false) {
            $raw = preg_replace('/^```(?:json)?\s*/m', '', $raw);
            $raw = preg_replace('/```\s*$/m', '', $raw);
            $raw = trim($raw);
        }

        // 2. JSON parse — HARD FAIL
        $data = json_decode($raw, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            $errors[] = 'JSON parse error: ' . json_last_error_msg();
            return $errors;
        }

        // 3. Required keys — HARD FAIL
        $required = array('title','slug','excerpt','content_html','featured_image');
        foreach ($required as $key) {
            if (!isset($data[$key]) || (is_string($data[$key]) && empty(trim($data[$key])))) {
                $errors[] = "Missing required key: {$key}";
            }
        }

        if (!empty($errors)) return $errors;

        $content = $data['content_html'];

        // 4. Banned words — SOFT (auto-cleaned in post-processing, never blocks)
        $banned_errors = $this->check_banned_words($content);
        if (!empty($banned_errors)) {
            // Soft warning — banned words will be auto-cleaned in post-processing
            foreach ($banned_errors as $be) {
                $this->warnings[] = $be . ' (will be auto-removed)';
            }
        }
        $banned_title = $this->check_banned_words($data['title']);
        if (!empty($banned_title)) {
            foreach ($banned_title as $bt) {
                $this->warnings[] = 'Title: ' . $bt . ' (will be auto-removed)';
            }
        }

        // 5. CTA must exist — HARD FAIL
        $cta_count = $this->count_cta($content);
        if ($cta_count === 0) {
            $errors[] = 'CTA block is missing from content_html';
        }

        // 6. Banned URLs — HARD FAIL (OpenAI invents these from training data)
        $banned_urls = array('core-structuring-tool', 'key-instrument-for', 'comprehensive-guide-to-structuring');
        foreach ($banned_urls as $banned_url) {
            if (stripos($content, $banned_url) !== false) {
                $errors[] = "Content contains banned URL fragment: '{$banned_url}' — this page does not exist";
            }
        }

        // ═══════════════════════════════════════════════════════════════
        // 7. CHECK ALL DAMALION.COM URLs ARE REAL — HARD FAIL
        //    FIX: Now catches ALL damalion.com URLs, not just /blog/
        //    Uses get_full_registry() for complete site inventory
        // ═══════════════════════════════════════════════════════════════
        $invented_urls = $this->check_all_internal_urls($content);
        if (!empty($invented_urls)) {
            foreach ($invented_urls as $fake_url) {
                $errors[] = "Invented URL detected: {$fake_url} — not in sitemap registry";
            }
        }

        // HARD FAILS stop here — if any errors, retry
        if (!empty($errors)) return $errors;

        // ═══════════════════════════════════════════════
        // EVERYTHING BELOW IS A WARNING — logged but post proceeds
        // Store warnings in a separate key so post-creator can log them
        // ═══════════════════════════════════════════════
        $this->warnings = array();

        if ($cta_count > 1) {
            $this->warnings[] = 'CTA appears ' . $cta_count . ' times (expected 1)';
        }

        if ($cta_count === 1 && !$this->cta_is_last($content)) {
            $this->warnings[] = 'CTA is not the last block';
        }

        // Pillar link
        $pillar_url = $this->get_pillar_url();
        if (!empty($pillar_url)) {
            $first_2000 = substr($content, 0, 2000);
            $url_no_slash = rtrim($pillar_url, '/');
            if (strpos($first_2000, $pillar_url) === false && strpos($first_2000, $url_no_slash) === false) {
                $this->warnings[] = 'Pillar link not found near top';
            }
        }

        // Internal links in body
        $internal_count = $this->count_internal_links($content);
        if ($internal_count < 2) {
            $this->warnings[] = 'Only ' . $internal_count . ' internal links found in body text (expected 3+)';
        }

        // Word count — soft warning (prompt still asks for 1200+, but don't block)
        $word_count = $this->count_words($content);
        if ($word_count < 1200) {
            $this->warnings[] = "Article is {$word_count} words (target: 1200+)";
        }

        // FAQ
        if (!isset($data['faqs']) || !is_array($data['faqs']) || count($data['faqs']) < 1) {
            $this->warnings[] = 'Missing or empty faqs array';
        }

        // FAQ accordion in HTML
        if (strpos($content, 'faq-accordion') === false) {
            $this->warnings[] = 'No FAQ accordion in content_html';
        }

        // Glossary
        if (!isset($data['glossary']) || !is_array($data['glossary']) || count($data['glossary']) < 1) {
            $this->warnings[] = 'Missing or empty glossary array';
        }

        // HowTo steps
        if (!isset($data['howto_steps']) || !is_array($data['howto_steps']) || count($data['howto_steps']) < 1) {
            $this->warnings[] = 'Missing howto_steps';
        }

        // Speakable
        if (empty($data['speakable_text'])) {
            $this->warnings[] = 'Missing speakable_text';
        }

        return $errors; // Empty = pass (warnings are separate)
    }

    /**
     * ═══════════════════════════════════════════════════════════════
     * CHECK ALL INTERNAL URLs — the core fix
     *
     * Extracts every damalion.com href from content_html,
     * checks each one against the FULL sitemap registry,
     * returns an array of URLs that don't exist.
     *
     * Allowlist: pillar URL, CTA URL, known structural paths.
     * ═══════════════════════════════════════════════════════════════
     */
    private function check_all_internal_urls($content) {
        $fake_urls = array();

        // Extract ALL damalion.com URLs from content
        if (!preg_match_all('/href=["\']((https?:\/\/(?:www\.)?damalion\.com)[^"\']*)["\']/', $content, $matches)) {
            return $fake_urls; // No internal links at all — nothing to check
        }

        $urls_to_check = array_unique($matches[1]);

        if (empty($urls_to_check)) {
            return $fake_urls;
        }

        // Build the allowlist (URLs that bypass registry check)
        $pillar_url = $this->get_pillar_url();
        $allowlist = array();
        if (!empty($pillar_url)) {
            $allowlist[] = strtolower(rtrim($pillar_url, '/'));
        }
        // CTA URL (lets-connect is already in allowed_url_patterns but be safe)
        $allowlist[] = 'https://www.damalion.com/lets-connect';
        $allowlist[] = 'https://www.damalion.com/connectons-nous';
        $allowlist[] = 'https://www.damalion.com';

        // Load FULL registry (not just blog — this was the bug)
        $sitemap = DBA_Sitemap::get_instance();
        $full_registry = $sitemap->get_full_registry();

        // Build a fast lookup set: normalized URL => true
        $registry_lookup = array();
        if (!empty($full_registry) && is_array($full_registry)) {
            foreach ($full_registry as $entry) {
                $url = isset($entry['url']) ? $entry['url'] : '';
                if (empty($url)) continue;
                // Store both with and without trailing slash
                $norm = strtolower(rtrim($url, '/'));
                $registry_lookup[$norm] = true;
                // Also store without ?lang= for comparison flexibility
                $no_query = strtolower(rtrim(preg_replace('/\?.*$/', '', $url), '/'));
                $registry_lookup[$no_query] = true;
            }
        }

        // If registry is empty (no scan yet), skip check entirely to avoid
        // false-positively blocking every post. Log a warning instead.
        if (empty($registry_lookup)) {
            error_log('DBA Validator: WARNING — Full registry is empty, skipping URL validation. Run a sitemap scan!');
            return $fake_urls;
        }

        // Check each URL
        foreach ($urls_to_check as $url) {
            $url_clean = strtolower(rtrim($url, '/'));

            // Skip external links (non-damalion)
            $host = wp_parse_url($url_clean, PHP_URL_HOST);
            if ($host && !in_array($host, array('damalion.com', 'www.damalion.com'))) {
                continue;
            }

            // Skip if in the explicit allowlist
            if (in_array($url_clean, $allowlist)) {
                continue;
            }

            // Skip if matches an allowed structural path pattern
            $path = wp_parse_url($url, PHP_URL_PATH);
            $query = wp_parse_url($url, PHP_URL_QUERY);
            $path_with_query = $path . (!empty($query) ? '?' . $query : '');
            if ($this->is_allowed_path($path_with_query)) {
                continue;
            }

            // Skip wp-content, wp-admin, etc. (not content URLs)
            if (preg_match('#/(wp-content|wp-admin|wp-json|wp-includes|xmlrpc|feed)/#i', $url)) {
                continue;
            }

            // === THE CHECK: Is this URL in the full registry? ===
            $found = false;

            // Direct match (with or without trailing slash)
            if (isset($registry_lookup[$url_clean])) {
                $found = true;
            }

            // Try without query string (?lang=fr etc.)
            if (!$found) {
                $url_no_query = strtolower(rtrim(preg_replace('/\?.*$/', '', $url), '/'));
                if (isset($registry_lookup[$url_no_query])) {
                    $found = true;
                }
            }

            // Try adding ?lang= variant if the URL has one
            if (!$found && !empty($query) && preg_match('/^lang=([a-z]{2,5}(-[a-z]{2,5})?)$/i', $query)) {
                // URL has ?lang=xx — we already checked both with and without it above
                // If still not found, it's truly missing
            }

            if (!$found) {
                $fake_urls[] = $url;
                error_log('DBA Validator: FAKE URL detected — "' . $url . '" not found in registry (' . count($registry_lookup) . ' entries)');
            }
        }

        return $fake_urls;
    }

    /**
     * Check if a URL path matches the allowed structural patterns
     */
    private function is_allowed_path($path) {
        if (empty($path)) return false;
        foreach ($this->allowed_url_patterns as $pattern) {
            if (preg_match($pattern, $path)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get warnings from last validation (soft issues, non-blocking)
     */
    public function get_warnings() {
        return isset($this->warnings) ? $this->warnings : array();
    }

    /**
     * Check for banned words
     */
    public function check_banned_words($text) {
        $errors = array();
        $banned_words = $this->get_banned_words();
        if (empty($banned_words)) return $errors;

        // Check multi-word phrases first (plain string match)
        foreach ($banned_words as $word) {
            if (strpos($word, ' ') !== false) {
                if (stripos($text, $word) !== false) {
                    $errors[] = "Banned phrase found: \"{$word}\"";
                }
            }
        }

        // Check single words with word boundary
        $single_words = array_filter($banned_words, function($w) { return strpos($w, ' ') === false; });
        if (!empty($single_words)) {
            $escaped = array_map(function($w) { return preg_quote($w, '/'); }, $single_words);
            $pattern = '/\b(' . implode('|', $escaped) . ')\b/i';
            if (preg_match_all($pattern, $text, $matches)) {
                foreach (array_unique($matches[1]) as $word) {
                    $errors[] = "Banned word found: \"{$word}\"";
                }
            }
        }

        return $errors;
    }

    /**
     * Count CTA occurrences — dynamic, matches whatever CTA is in settings
     */
    private function count_cta($content) {
        $cta = $this->get_cta_html();
        if (empty($cta)) return 0;

        // Build a flexible regex from the actual CTA HTML in settings
        // Normalize whitespace, escape for regex, then make whitespace flexible
        $pattern = $this->build_cta_pattern($cta);
        return preg_match_all($pattern, $content, $matches);
    }

    /**
     * CTA must be the last block
     */
    private function cta_is_last($content) {
        $cta = $this->get_cta_html();
        if (empty($cta)) return true;

        $pattern = $this->build_cta_pattern($cta, true); // true = anchor to end
        return (bool) preg_match($pattern, $content);
    }

    /**
     * Build a whitespace-tolerant regex from the CTA HTML in settings.
     * Allows GPT to add/remove minor whitespace, change quote styles, etc.
     */
    private function build_cta_pattern($cta_html, $anchor_end = false) {
        $cta = trim($cta_html);
        // Escape regex special chars
        $escaped = preg_quote($cta, '/');
        // Make all whitespace flexible (1+ whitespace chars)
        $escaped = preg_replace('/\s+/', '\s+', $escaped);
        // Allow single or double quotes interchangeably
        $escaped = str_replace(array("\\'", '\\"'), '["\']', $escaped);
        // Allow optional commas (GPT sometimes drops them)
        $escaped = str_replace(',', ',?', $escaped);

        $end = $anchor_end ? '\s*$' : '';
        return '/' . $escaped . $end . '/si';
    }

    /**
     * Remove CTA block from content for counting/analysis
     */
    private function strip_cta($content) {
        $cta = $this->get_cta_html();
        if (empty($cta)) return $content;
        $pattern = $this->build_cta_pattern($cta);
        return preg_replace($pattern, '', $content);
    }

    /**
     * Count damalion.com internal links in body paragraphs (excluding CTA)
     */
    private function count_internal_links($content) {
        // Remove CTA block (dynamic)
        $text = $this->strip_cta($content);
        // Count unique damalion.com hrefs
        preg_match_all('/href="(https?:\/\/(?:www\.)?damalion\.com[^"]*)"/', $text, $matches);
        return count(array_unique($matches[1] ?? array()));
    }

    /**
     * Count words excluding CTA, FAQ accordion, glossary, and Related section
     * Per spec: "faq excluded in the word count"
     */
    public function count_words($content) {
        // Remove CTA block (dynamic)
        $text = $this->strip_cta($content);

        // Remove FAQ accordion section(s)
        $text = preg_replace('/<div class="faq-accordion">[\s\S]*?<\/div>/si', '', $text);

        // Remove glossary section
        $text = preg_replace('/<div class="dba-glossary-section">[\s\S]*?<\/div>/si', '', $text);
        $text = preg_replace('/<h[23][^>]*>\s*Glossary\s*<\/h[23]>[\s\S]*?(?=<h[23]|<p>\s*Damalion supports|$)/si', '', $text);

        // Strip HTML tags
        $text = preg_replace('/<[^>]+>/', ' ', $text);

        // Normalize whitespace
        $text = preg_replace('/\s+/', ' ', trim($text));

        // Count word tokens
        preg_match_all('/[0-9A-Za-z\x{00C0}-\x{00D6}\x{00D8}-\x{00F6}\x{00F8}-\x{00FF}]+(?:[-\'][0-9A-Za-z\x{00C0}-\x{00D6}\x{00D8}-\x{00F6}\x{00F8}-\x{00FF}]+)*/u', $text, $words);

        return count($words[0]);
    }

    /**
     * Quick check: is content safe to publish?
     */
    public function is_valid($json_string) {
        $errors = $this->validate($json_string);
        return empty($errors);
    }
}
