<?php
/**
 * DBA Kaizen — Continuous Improvement Engine
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  MASTER LEARNING SYSTEM — Forces all sub-engines to learn from
 *  editor corrections before generating new content.
 * ═══════════════════════════════════════════════════════════════════════
 *
 * How it works:
 *   1. At generation time, any engine calls DBA_Kaizen::get_learning_context()
 *   2. Kaizen queries ALL auto-generated posts from the last 5 publish days
 *   3. Compares original AI output vs. current edited content
 *   4. Extracts correction patterns (links, alt text, structure, categories, tone)
 *   5. Returns a prompt injection string the engine appends to its system prompt
 *
 * What it detects:
 *   - Inbound links the editor added (learns linking patterns)
 *   - Inbound links the editor removed (learns what NOT to link)
 *   - Alt text corrections on featured images
 *   - Category reassignments
 *   - H2 heading changes (structure corrections)
 *   - Content additions/removals (tone + style corrections)
 *   - Title rewording patterns
 *
 * FIXED v2:
 *   - No longer tells AI to INSERT inbound links (auto-linker handles that)
 *   - Adds explicit anti-hallucination instruction for URLs
 *   - Link tracking kept for debugging/analytics but NOT injected as "do this" instructions
 *   - Editor link corrections still logged for the auto-linker to learn from
 *
 * Storage:
 *   - Original AI content stored in post meta: _dba_original_content, _dba_original_title
 *   - Each generator must call DBA_Kaizen::store_original() after creating a post
 *   - This is a ONE-LINE integration per generator
 *
 * Integration (one line per generator):
 *   $kaizen_context = DBA_Kaizen::get_learning_context();
 *   $system_prompt .= $kaizen_context;
 *
 * @since 2.7.0
 * @modified 2.8.0 — Fixed link hallucination issue
 */

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

class DBA_Kaizen {

    // How many publish days to look back
    const LOOKBACK_DAYS = 5;

    // Max posts to analyze (performance guard)
    const MAX_POSTS = 30;

    // Max chars for the learning context injection (keep prompt manageable)
    const MAX_CONTEXT_CHARS = 4000;

    // Cache key to avoid re-computing on every post in a batch
    const CACHE_KEY = 'dba_kaizen_context';
    const CACHE_TTL = 1800; // 30 minutes

    // Meta keys for original AI output (set by store_original)
    const META_ORIGINAL_TITLE   = '_dba_original_title';
    const META_ORIGINAL_CONTENT = '_dba_original_content';
    const META_ORIGINAL_EXCERPT = '_dba_original_excerpt';
    const META_ORIGINAL_ALT     = '_dba_original_img_alt';

    // All auto-created meta keys across all engines
    private static $engine_meta_keys = array(
        '_dba_auto_created',                  // Core + GeoBot
        '_dba_pe_auto_created',               // PE NewsBot
        '_dba_luxstrat_auto_created',         // LuxStrat EN
        '_dba_luxstrat_fr_auto_created',      // LuxStrat FR
        '_dba_luxstrat_de_auto_created',      // LuxStrat DE
        '_dba_luxstrat_es_auto_created',      // LuxStrat ES
        '_dba_luxstrat_pt_auto_created',      // LuxStrat PT
        '_dba_luxstrat_it_auto_created',      // LuxStrat IT
    );

    /* =================================================================
     *  PUBLIC API — Called by every generator
     * ================================================================= */

    /**
     * Get the learning context string to inject into any system prompt.
     *
     * Returns empty string if no corrections detected or no history.
     * Cached for 30 minutes to avoid re-computing per post in a batch.
     *
     * @return string  Prompt injection text (or empty string).
     */
    public static function get_learning_context() {

        // Check cache first (same batch shouldn't re-compute)
        $cached = get_transient( self::CACHE_KEY );
        if ( $cached !== false ) {
            return $cached;
        }

        $corrections = self::analyze_corrections();

        if ( empty( $corrections ) ) {
            set_transient( self::CACHE_KEY, '', self::CACHE_TTL );
            return '';
        }

        $context = self::build_context_string( $corrections );

        set_transient( self::CACHE_KEY, $context, self::CACHE_TTL );

        error_log( 'DBA Kaizen: Learning context built — ' . strlen( $context ) . ' chars, '
            . count( $corrections['corrections'] ?? array() ) . ' corrections detected' );

        return $context;
    }

    /**
     * Store the original AI-generated content as post meta.
     *
     * MUST be called by every generator immediately after wp_insert_post().
     * This is the "before" snapshot that Kaizen compares against the "after" (edited version).
     *
     * @param int    $post_id      The WordPress post ID.
     * @param string $title        Original AI-generated title.
     * @param string $content_html Original AI-generated content HTML.
     * @param string $excerpt      Original AI-generated excerpt.
     * @param string $img_alt      Original AI-generated featured image alt text.
     */
    public static function store_original( int $post_id, string $title, string $content_html, string $excerpt = '', string $img_alt = '' ) {
        update_post_meta( $post_id, self::META_ORIGINAL_TITLE,   $title );
        update_post_meta( $post_id, self::META_ORIGINAL_CONTENT, $content_html );

        if ( $excerpt ) {
            update_post_meta( $post_id, self::META_ORIGINAL_EXCERPT, $excerpt );
        }
        if ( $img_alt ) {
            update_post_meta( $post_id, self::META_ORIGINAL_ALT, $img_alt );
        }
    }

    /**
     * Force-refresh the Kaizen cache (call after manual corrections).
     */
    public static function flush_cache() {
        delete_transient( self::CACHE_KEY );
    }

    /* =================================================================
     *  ANALYSIS ENGINE
     * ================================================================= */

    /**
     * Analyze all auto-generated posts from the last N publish days.
     * Compares original AI output vs. current edited content.
     *
     * @return array { corrections[], link_patterns[], alt_patterns[], category_patterns[], structure_patterns[] }
     */
    private static function analyze_corrections() {

        $posts = self::get_recent_ai_posts();

        if ( empty( $posts ) ) {
            return array();
        }

        $result = array(
            'corrections'        => array(),
            'links_added'        => array(),
            'links_removed'      => array(),
            'alt_corrections'    => array(),
            'category_changes'   => array(),
            'title_corrections'  => array(),
            'h2_corrections'     => array(),
            'content_additions'  => array(),
            'posts_analyzed'     => 0,
            'posts_modified'     => 0,
        );

        foreach ( $posts as $post ) {
            $original_title   = get_post_meta( $post->ID, self::META_ORIGINAL_TITLE, true );
            $original_content = get_post_meta( $post->ID, self::META_ORIGINAL_CONTENT, true );

            // Also try legacy meta key from core post creator
            if ( empty( $original_content ) ) {
                $json_data = get_post_meta( $post->ID, '_dba_json_data', true );
                if ( $json_data ) {
                    $parsed = is_string( $json_data ) ? json_decode( $json_data, true ) : $json_data;
                    if ( is_array( $parsed ) ) {
                        $original_title   = $original_title ?: ( $parsed['title'] ?? '' );
                        $original_content = $parsed['content_html'] ?? ( $parsed['content'] ?? '' );
                    }
                }
            }

            // Skip posts without original data (pre-Kaizen posts)
            if ( empty( $original_content ) ) {
                continue;
            }

            $result['posts_analyzed']++;

            $current_title   = $post->post_title;
            $current_content = $post->post_content;

            // Was this post modified by the editor?
            $title_changed   = ( $original_title && $original_title !== $current_title );
            $content_changed = ( trim( $original_content ) !== trim( $current_content ) );

            if ( ! $title_changed && ! $content_changed ) {
                continue; // No corrections made
            }

            $result['posts_modified']++;

            // --- TITLE CORRECTIONS ---
            if ( $title_changed ) {
                $result['title_corrections'][] = array(
                    'original' => $original_title,
                    'corrected' => $current_title,
                );
            }

            // --- LINK ANALYSIS ---
            $original_links = self::extract_links( $original_content );
            $current_links  = self::extract_links( $current_content );

            $added   = array_diff_key( $current_links, $original_links );
            $removed = array_diff_key( $original_links, $current_links );

            foreach ( $added as $url => $text ) {
                $result['links_added'][] = array( 'url' => $url, 'anchor' => $text, 'post_id' => $post->ID );
            }
            foreach ( $removed as $url => $text ) {
                $result['links_removed'][] = array( 'url' => $url, 'anchor' => $text, 'post_id' => $post->ID );
            }

            // --- H2 STRUCTURE ---
            $original_h2s = self::extract_h2s( $original_content );
            $current_h2s  = self::extract_h2s( $current_content );

            if ( $original_h2s !== $current_h2s ) {
                $result['h2_corrections'][] = array(
                    'original'  => $original_h2s,
                    'corrected' => $current_h2s,
                );
            }

            // --- ALT TEXT ---
            $original_alt = get_post_meta( $post->ID, self::META_ORIGINAL_ALT, true );
            $thumb_id     = get_post_thumbnail_id( $post->ID );
            if ( $thumb_id && $original_alt ) {
                $current_alt = get_post_meta( $thumb_id, '_wp_attachment_image_alt', true );
                if ( $current_alt && $original_alt !== $current_alt ) {
                    $result['alt_corrections'][] = array(
                        'original'  => $original_alt,
                        'corrected' => $current_alt,
                    );
                }
            }

            // --- CONTENT ADDITIONS (text the editor added that wasn't in the original) ---
            $orig_sentences  = self::extract_sentences( strip_tags( $original_content ) );
            $curr_sentences  = self::extract_sentences( strip_tags( $current_content ) );
            $added_sentences = array_diff( $curr_sentences, $orig_sentences );

            // Only track substantial additions (> 30 chars, skip tiny edits)
            foreach ( $added_sentences as $s ) {
                if ( strlen( $s ) > 30 ) {
                    $result['content_additions'][] = $s;
                    if ( count( $result['content_additions'] ) >= 10 ) break;
                }
            }

            // --- CATEGORY CHANGES ---
            $original_category = get_post_meta( $post->ID, '_dba_pe_category', true )
                ?: ( get_post_meta( $post->ID, '_dba_theme', true ) ?: '' );
            $current_categories = wp_get_post_categories( $post->ID, array( 'fields' => 'names' ) );
            $current_cat_str = is_array( $current_categories ) ? implode( ', ', $current_categories ) : '';

            if ( $original_category && $current_cat_str && stripos( $current_cat_str, $original_category ) === false ) {
                $result['category_changes'][] = array(
                    'original'  => $original_category,
                    'corrected' => $current_cat_str,
                );
            }

            // Store as individual correction summary
            $result['corrections'][] = array(
                'post_id'         => $post->ID,
                'title_changed'   => $title_changed,
                'content_changed' => $content_changed,
                'links_added'     => count( $added ),
                'links_removed'   => count( $removed ),
            );
        }

        return $result;
    }

    /* =================================================================
     *  CONTEXT BUILDER — Converts corrections into prompt text
     * ================================================================= */

    /**
     * Build a prompt-injectable string from analyzed corrections.
     *
     * FIXED v2: No longer tells AI to insert inbound links.
     * Internal linking is handled by DBA_Auto_Linker AFTER generation.
     * Kaizen now explicitly instructs the AI to NOT invent URLs.
     */
    private static function build_context_string( array $data ) {

        if ( $data['posts_modified'] === 0 ) {
            return '';
        }

        $lines = array();
        $lines[] = "\n\n═══ EDITOR CORRECTIONS — LEARN FROM THESE ═══";
        $lines[] = "The following patterns were extracted from editor corrections on the last "
            . $data['posts_analyzed'] . " AI-generated posts (" . $data['posts_modified'] . " were corrected).";
        $lines[] = "Apply these corrections to ALL future articles:\n";

        // ═══════════════════════════════════════════════════════════════
        // CRITICAL: ANTI-HALLUCINATION INSTRUCTION FOR LINKS
        // This is ALWAYS injected, regardless of what corrections exist.
        // It overrides any tendency for the AI to invent damalion.com URLs.
        // ═══════════════════════════════════════════════════════════════
        $lines[] = "⚠️  INTERNAL LINKS RULE (MANDATORY):";
        $lines[] = "Do NOT insert any inbound links to damalion.com pages in content_html.";
        $lines[] = "Do NOT invent or guess any damalion.com URLs — they will be REJECTED by the validator.";
        $lines[] = "The ONLY damalion.com links you may include are: (1) the pillar link provided in the prompt, and (2) the CTA link.";
        $lines[] = "All other internal links are injected AUTOMATICALLY by the auto-linker system after your content is generated.";
        $lines[] = "If you insert a damalion.com URL that does not exist in our sitemap, the entire post will be rejected and you will have to regenerate it.";
        $lines[] = "";

        // --- INBOUND LINKS (informational only — NOT an instruction to insert) ---
        if ( ! empty( $data['links_added'] ) || ! empty( $data['links_removed'] ) ) {
            $lines[] = "LINK PATTERNS (for reference only — the auto-linker handles injection):";
            if ( ! empty( $data['links_added'] ) ) {
                $lines[] = "  Links the editor added (auto-linker should match these patterns):";
                $seen_urls = array();
                foreach ( $data['links_added'] as $link ) {
                    $url = $link['url'];
                    if ( isset( $seen_urls[ $url ] ) ) continue;
                    if ( self::is_internal_link( $url ) ) {
                        $lines[] = "    + \"{$link['anchor']}\" → {$url}";
                        $seen_urls[ $url ] = true;
                    }
                    if ( count( $seen_urls ) >= 8 ) break;
                }
            }
            if ( ! empty( $data['links_removed'] ) ) {
                $lines[] = "  Links the editor removed (should NOT be linked):";
                $seen = array();
                foreach ( $data['links_removed'] as $link ) {
                    if ( isset( $seen[ $link['url'] ] ) ) continue;
                    $lines[] = "    - {$link['url']}";
                    $seen[ $link['url'] ] = true;
                    if ( count( $seen ) >= 5 ) break;
                }
            }
            $lines[] = "";
        }

        // --- TITLE CORRECTIONS ---
        if ( ! empty( $data['title_corrections'] ) ) {
            $lines[] = "TITLE STYLE — The editor corrected these titles. Learn the preferred format:";
            foreach ( array_slice( $data['title_corrections'], 0, 3 ) as $tc ) {
                $lines[] = "  AI wrote:      \"{$tc['original']}\"";
                $lines[] = "  Editor fixed:  \"{$tc['corrected']}\"";
            }
            $lines[] = "";
        }

        // --- H2 STRUCTURE ---
        if ( ! empty( $data['h2_corrections'] ) ) {
            $lines[] = "H2 STRUCTURE — The editor changed these section headings:";
            foreach ( array_slice( $data['h2_corrections'], 0, 3 ) as $h2c ) {
                $lines[] = "  AI H2s:     " . implode( ' | ', $h2c['original'] );
                $lines[] = "  Editor H2s: " . implode( ' | ', $h2c['corrected'] );
            }
            $lines[] = "";
        }

        // --- ALT TEXT ---
        if ( ! empty( $data['alt_corrections'] ) ) {
            $lines[] = "IMAGE ALT TEXT — The editor corrected these alt texts. Learn the SEO pattern:";
            foreach ( array_slice( $data['alt_corrections'], 0, 3 ) as $alt ) {
                $lines[] = "  AI wrote:     \"{$alt['original']}\"";
                $lines[] = "  Editor fixed: \"{$alt['corrected']}\"";
            }
            $lines[] = "";
        }

        // --- CATEGORY CHANGES ---
        if ( ! empty( $data['category_changes'] ) ) {
            $lines[] = "CATEGORIES — The editor reassigned these categories:";
            foreach ( array_slice( $data['category_changes'], 0, 3 ) as $cc ) {
                $lines[] = "  AI assigned:   \"{$cc['original']}\"";
                $lines[] = "  Editor changed: \"{$cc['corrected']}\"";
            }
            $lines[] = "";
        }

        // --- CONTENT PATTERNS ---
        if ( ! empty( $data['content_additions'] ) ) {
            $lines[] = "CONTENT PATTERNS — The editor added these sentences/paragraphs. Use similar language and depth:";
            foreach ( array_slice( $data['content_additions'], 0, 5 ) as $s ) {
                // Truncate to ~120 chars for prompt efficiency
                $lines[] = "  - \"" . mb_substr( $s, 0, 120 ) . ( mb_strlen( $s ) > 120 ? '…' : '' ) . "\"";
            }
            $lines[] = "";
        }

        $lines[] = "═══ END EDITOR CORRECTIONS ═══\n";

        $context = implode( "\n", $lines );

        // Enforce size limit
        if ( strlen( $context ) > self::MAX_CONTEXT_CHARS ) {
            $context = substr( $context, 0, self::MAX_CONTEXT_CHARS - 50 )
                . "\n\n[Truncated — more corrections available]\n═══ END EDITOR CORRECTIONS ═══\n";
        }

        return $context;
    }

    /* =================================================================
     *  DATA HELPERS
     * ================================================================= */

    /**
     * Get all auto-generated posts from the last N publish days across ALL engines.
     */
    private static function get_recent_ai_posts() {

        $date_cutoff = date( 'Y-m-d', strtotime( '-' . self::LOOKBACK_DAYS . ' days' ) );

        // Build meta query: any of the engine auto-created markers
        $meta_queries = array( 'relation' => 'OR' );
        foreach ( self::$engine_meta_keys as $mk ) {
            $meta_queries[] = array( 'key' => $mk, 'compare' => 'EXISTS' );
        }

        $posts = get_posts( array(
            'post_type'      => 'post',
            'post_status'    => array( 'publish', 'draft', 'pending', 'private' ),
            'date_query'     => array( array(
                'after'     => $date_cutoff,
                'inclusive' => true,
            ) ),
            'meta_query'     => $meta_queries,
            'posts_per_page' => self::MAX_POSTS,
            'orderby'        => 'date',
            'order'          => 'DESC',
        ) );

        return $posts;
    }

    /**
     * Extract all <a href="...">text</a> links from HTML.
     * Returns assoc array: url => anchor_text
     */
    private static function extract_links( string $html ) {
        $links = array();
        if ( preg_match_all( '/<a\s[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is', $html, $matches, PREG_SET_ORDER ) ) {
            foreach ( $matches as $m ) {
                $url  = trim( $m[1] );
                $text = trim( strip_tags( $m[2] ) );
                if ( $url && $text ) {
                    $links[ $url ] = $text;
                }
            }
        }
        return $links;
    }

    /**
     * Extract all H2 heading texts from HTML.
     */
    private static function extract_h2s( string $html ) {
        $h2s = array();
        if ( preg_match_all( '/<h2[^>]*>(.*?)<\/h2>/is', $html, $matches ) ) {
            foreach ( $matches[1] as $h2 ) {
                $h2s[] = trim( strip_tags( $h2 ) );
            }
        }
        return $h2s;
    }

    /**
     * Split text into sentences.
     */
    private static function extract_sentences( string $text ) {
        $text = preg_replace( '/\s+/', ' ', trim( $text ) );
        $sentences = preg_split( '/(?<=[.!?])\s+/', $text );
        return array_filter( array_map( 'trim', $sentences ), function( $s ) {
            return strlen( $s ) > 10;
        } );
    }

    /**
     * Check if a URL is internal (same site).
     */
    private static function is_internal_link( string $url ) {
        $site_host = wp_parse_url( home_url(), PHP_URL_HOST );
        $link_host = wp_parse_url( $url, PHP_URL_HOST );

        if ( ! $link_host ) return true; // relative URL
        return ( strtolower( $link_host ) === strtolower( $site_host ) );
    }
}
