<?php
/**
 * DBA Startup Hub Generator — U.S. Startup Article Pipeline
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  Completely isolated from core DBA_Post_Creator, GeoBot, and NewsBot.
 *  Uses its own validator, kaizen, dedupe, progress transient, and meta keys.
 *
 *  PIPELINE (14 steps per post):
 *    1. select_vertical       — Pick category + city (weighted, dedupe-aware)
 *    2. research_startup      — OpenAI web_search: find real startups, funding, founders
 *    3. generate_content      — OpenAI article generation from research
 *    4. validate_content      — Startup Hub validator (CTA, company links)
 *    5. check_plagiarism      — Internal + external plagiarism scan
 *    6. create_draft          — Insert WP post (draft)
 *    7. inject_cta            — Insert Damalion CTA block
 *    8. install_links         — Auto-linker: inject contextual internal links
 *    9. generate_image        — DALL-E 3 featured image
 *   10. upload_image          — Upload to Media Library + fill all meta fields
 *   11. set_featured_image    — Attach thumbnail to post
 *   12. fill_yoast_seo        — Focus keyphrase, SEO title, meta description
 *   13. anti_orphan           — Link from hub page + cross-link from related
 *   14. publish               — Set final status + schedule orphan verification
 *
 * ═══════════════════════════════════════════════════════════════════════
 *
 * @since 2.16.0
 */

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

class DBA_Startup_Hub_Generator {

    private static $instance = null;

    const PROGRESS_KEY  = 'dba_startup_hub_generation_progress';
    const META_KEY      = '_dba_startup_hub_auto_created';
    const META_CATEGORY = '_dba_startup_hub_category';
    const META_CITY     = '_dba_startup_hub_city';
    const META_STATE    = '_dba_startup_hub_state';
    const META_STARTUPS = '_dba_startup_hub_startups_mentioned';
    const LOG_PREFIX    = 'DBA StartupHub';
    const TOTAL_STEPS   = 14;

    private $max_retries = 3;

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

    private function __construct() {
        add_action( 'wp_ajax_dba_startup_hub_generate_single', array( $this, 'ajax_generate_single' ) );
        add_action( 'wp_ajax_dba_startup_hub_generate_batch',  array( $this, 'ajax_generate_batch' ) );
        add_action( 'wp_ajax_dba_startup_hub_get_progress',    array( $this, 'ajax_get_progress' ) );
        add_action( 'wp_ajax_dba_startup_hub_save_cta',        array( $this, 'ajax_save_cta' ) );
    }

    /* =================================================================
     *  PROGRESS HELPERS
     * ================================================================= */

    private function set_progress( array $data ) {
        set_transient( self::PROGRESS_KEY, $data, 3600 );
    }

    public function get_progress() {
        return get_transient( self::PROGRESS_KEY ) ?: array();
    }

    private function step( int $post_index, int $step, string $name, string $status, string $detail = '' ) {
        $progress = $this->get_progress();
        $progress['current_post']      = $post_index;
        $progress['current_step']      = $step;
        $progress['current_step_name'] = $name;
        $progress['current_status']    = $status;
        $progress['current_detail']    = $detail;
        $progress['updated']           = time();
        $progress['step_percent']      = round( ( $step / self::TOTAL_STEPS ) * 100 );

        $total = max( 1, intval( $progress['total_posts'] ?? 1 ) );
        $done  = intval( $progress['completed'] ?? 0 ) + intval( $progress['failed'] ?? 0 );
        $progress['overall_percent'] = round( ( ( $done + ( $step / self::TOTAL_STEPS ) ) / $total ) * 100 );

        if ( ! isset( $progress['steps_log'] ) ) $progress['steps_log'] = array();
        $progress['steps_log'][] = array(
            'post' => $post_index, 'step' => $step, 'name' => $name,
            'status' => $status, 'detail' => $detail, 'time' => current_time( 'H:i:s' ),
        );

        $this->set_progress( $progress );
        error_log( self::LOG_PREFIX . " [{$post_index}] Step {$step} ({$name}): {$status} — {$detail}" );
    }

    /* =================================================================
     *  FULL PIPELINE — Single Post
     * ================================================================= */

    public function create_single_post( int $post_index = 1 ) {

        $config   = DBA_Startup_Hub_Registry::get_config();
        $settings = get_option( $config['settings_option'], array() );
        $openai   = DBA_OpenAI::get_instance();
        $img_gen  = DBA_Image_Generator::get_instance();
        $logger   = DBA_Logger::get_instance();

        /* ── STEP 1 — Select vertical + city ────────────────────── */
        $this->step( $post_index, 1, 'select_vertical', 'running', 'Picking category and city…' );

        $used_cats   = DBA_Startup_Hub_Dedupe::get_today_categories();
        $used_cities = DBA_Startup_Hub_Dedupe::get_today_cities();

        $category = DBA_Startup_Hub_Registry::pick_category( $used_cats );
        $city     = DBA_Startup_Hub_Registry::pick_city( $used_cities );

        // Avoid recently used combos
        $retries = 0;
        while ( DBA_Startup_Hub_Dedupe::combo_recently_used( $category['slug'], $city['city'], 3 ) && $retries < 5 ) {
            $city = DBA_Startup_Hub_Registry::pick_city( $used_cities );
            $retries++;
        }

        $this->step( $post_index, 1, 'select_vertical', 'done',
            "Category: {$category['short']} — City: {$city['city']}, {$city['state']}" );

        /* ── Subject dedup guard (from damalion-blog-ai.php) ── */
        $subject_key = $category['short'] . ' in ' . $city['city'];
        if ( function_exists( 'dba_subject_already_posted_today' ) && dba_subject_already_posted_today( $subject_key ) ) {
            $this->step( $post_index, 1, 'select_vertical', 'failed',
                'DUPLICATE: "' . $subject_key . '" already posted today — skipping' );
            error_log( self::LOG_PREFIX . ' DEDUP: Subject "' . $subject_key . '" already posted today. Skipping.' );
            return new WP_Error( 'duplicate_subject', 'Subject "' . $subject_key . '" already posted today' );
        }

        /* ── STEP 2 — Research via OpenAI web_search ────────────── */
        $this->step( $post_index, 2, 'research_startup', 'running',
            "Researching {$category['short']} startups in {$city['city']}…" );

        $research_context = '';
        $recent_startups  = DBA_Startup_Hub_Dedupe::get_recent_startups();

        $research_prompt = $this->build_research_prompt( $category, $city, $recent_startups );
        $research_result = $openai->research_news( $research_prompt, '' );

        if ( is_wp_error( $research_result ) ) {
            $this->step( $post_index, 2, 'research_startup', 'warning',
                'Web research failed: ' . $research_result->get_error_message() . ' — proceeding with ecosystem focus' );
        } else {
            $research_context = $research_result;
            $this->step( $post_index, 2, 'research_startup', 'done',
                'Research complete ✓ (' . strlen( $research_context ) . ' chars)' );
        }

        /* ── STEP 3 — Generate article ──────────────────────────── */
        $this->step( $post_index, 3, 'generate_content', 'running',
            'Calling OpenAI (' . ( $settings['openai_model'] ?? 'gpt-4.1' ) . ')…' );

        // Kaizen context (own center)
        $kaizen_context = DBA_Startup_Hub_Kaizen::get_learning_context();

        // Satellite links from sitemap
        $sitemap = class_exists( 'DBA_Sitemap' ) ? DBA_Sitemap::get_instance() : null;
        $satellite_links = array();
        if ( $sitemap ) {
            $satellite_links = $sitemap->select_contextual_links( array(), $category['short'], $city['city'], 6, 'en' );
        }

        $pillar_url = 'https://www.damalion.com/blog/';
        $content_settings = get_option( 'dba_content_settings', array() );
        if ( ! empty( $content_settings['pillar_url'] ) ) {
            $pillar_url = $content_settings['pillar_url'];
        }

        // Partnership CTA — Admin override takes priority
        $cta_html = '';
        if ( ! empty( $settings['cta_html_override'] ) ) {
            $cta_html = $settings['cta_html_override'];
            error_log( self::LOG_PREFIX . ' Using custom CTA from admin settings' );
        } else {
            $cta_html = DBA_Startup_Hub_Registry::get_partnership_cta();
        }

        // Build the system + user prompts
        $system_prompt = $this->build_system_prompt( $category, $city, $satellite_links, $pillar_url, $cta_html, $kaizen_context );
        $user_prompt   = $this->build_user_prompt( $category, $city, $research_context, $satellite_links, $pillar_url, $cta_html );

        // ── TIS: inject strategic content gap priorities ──
        if ( class_exists( 'DBA_TIS_Recommender' ) ) {
            $tis_context = DBA_TIS_Recommender::get_generator_context( 'startup-hub' );
            if ( $tis_context ) {
                $system_prompt .= "\n\n" . $tis_context;
            }
        }

        // ── PIPELINE: inject keyword focus from validated brief ──
        if ( class_exists( 'DBA_TIS_Pipeline' ) ) {
            $hub_brief = DBA_TIS_Pipeline::consume_brief( 'startup-hub' );
            if ( $hub_brief ) {
                $user_prompt .= DBA_TIS_Pipeline::build_brief_context( $hub_brief );
                $this->_pipeline_brief = $hub_brief;
                error_log( self::LOG_PREFIX . ' 🔬 Pipeline keyword focus injected' );
            }
        }

        $data       = null;
        $raw_json   = '';
        $last_errors = array();

        for ( $attempt = 0; $attempt < $this->max_retries; $attempt++ ) {
            if ( $attempt > 0 ) {
                $this->step( $post_index, 3, 'generate_content', 'running', 'Retry #' . ( $attempt + 1 ) . '…' );
            }

            $api_settings = get_option( 'dba_settings', array() );
            $model = $api_settings['openai_model'] ?? 'gpt-4.1';
            $api_key = $api_settings['openai_api_key'] ?? '';

            if ( empty( $api_key ) ) {
                return new WP_Error( 'no_api_key', 'OpenAI API key not configured' );
            }

            $response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', array(
                'timeout' => 120,
                'headers' => array(
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type'  => 'application/json',
                ),
                'body' => json_encode( array(
                    'model'       => $model,
                    'messages'    => array(
                        array( 'role' => 'system', 'content' => $system_prompt ),
                        array( 'role' => 'user',   'content' => $user_prompt ),
                    ),
                    'temperature' => 0.7,
                    'max_tokens'  => 4000,
                ) ),
            ) );

            if ( is_wp_error( $response ) ) {
                $last_errors[] = $response->get_error_message();
                sleep( 3 );
                continue;
            }

            $body = json_decode( wp_remote_retrieve_body( $response ), true );
            $raw_json = $body['choices'][0]['message']['content'] ?? '';

            if ( empty( $raw_json ) ) {
                $last_errors[] = 'Empty response from OpenAI';
                sleep( 3 );
                continue;
            }

            // Clean markdown fences
            $clean = trim( $raw_json );
            if ( strpos( $clean, '```' ) !== false ) {
                $clean = preg_replace( '/^```(?:json)?\s*/m', '', $clean );
                $clean = preg_replace( '/```\s*$/m', '', $clean );
                $clean = trim( $clean );
            }

            /* ── STEP 4 — Validate ──────────────────────────────── */
            $this->step( $post_index, 4, 'validate_content', 'running', 'Running startup validator…' );

            $validator = DBA_Startup_Hub_Validator::get_instance();
            $errors = $validator->validate( $clean, $city['city'] );

            if ( empty( $errors ) ) {
                $data = json_decode( $clean, true );
                $warnings = $validator->get_warnings();
                if ( ! empty( $warnings ) ) {
                    $this->step( $post_index, 4, 'validate_content', 'warning',
                        count( $warnings ) . ' warnings: ' . implode( '; ', array_slice( $warnings, 0, 3 ) ) );
                } else {
                    $this->step( $post_index, 4, 'validate_content', 'done', 'All checks passed ✓' );
                }
                break;
            }

            $last_errors = $errors;
            $this->step( $post_index, 4, 'validate_content', 'running',
                'Failed: ' . implode( '; ', array_slice( $errors, 0, 2 ) ) . ' — retrying…' );
            sleep( 2 );
        }

        if ( $data === null ) {
            $this->step( $post_index, 4, 'validate_content', 'failed', implode( '; ', $last_errors ) );
            return new WP_Error( 'generation_failed', implode( '; ', $last_errors ) );
        }

        $this->step( $post_index, 3, 'generate_content', 'done',
            'Article: "' . wp_trim_words( $data['title'] ?? '', 8 ) . '"' );

        /* ── STEP 5 — Plagiarism check ──────────────────────────── */
        $this->step( $post_index, 5, 'check_plagiarism', 'running', 'Scanning…' );

        $plag_result = array( 'pass' => true, 'score' => 95, 'method' => 'skipped', 'details' => array() );
        if ( class_exists( 'DBA_Plagiarism' ) ) {
            $plagiarism = DBA_Plagiarism::get_instance();
            $plag_result = $plagiarism->check( $data['content_html'] );
            if ( ! $plag_result['pass'] ) {
                $this->step( $post_index, 5, 'check_plagiarism', 'warning',
                    'Score: ' . $plag_result['score'] . '% — proceeding (startup content uses real data)' );
            }
        }
        $this->step( $post_index, 5, 'check_plagiarism', 'done',
            'Uniqueness: ' . $plag_result['score'] . '% ✓' );

        /* ── STEP 6 — Create WP draft ───────────────────────────── */
        $this->step( $post_index, 6, 'create_draft', 'running', 'Inserting WordPress post…' );

        // Ensure "Startup Hub" category exists
        $cat_term = get_term_by( 'slug', 'smb-growth-hub', 'category' );
        if ( ! $cat_term ) {
            $result_term = wp_insert_term( 'SMB Growth Hub', 'category', array( 'slug' => 'smb-growth-hub' ) );
            $cat_id = is_wp_error( $result_term ) ? 1 : $result_term['term_id'];
        } else {
            $cat_id = $cat_term->term_id;
        }

        $content = $data['content_html'];

        // SANITIZE: Strip any damalion.com links GPT hallucinated
        // (non-English slugs, invented URLs, mangled paths)
        $content = $this->sanitize_gpt_links( $content, $satellite_links, $pillar_url );

        /* ── STEP 7 — Inject Damalion CTA ───────────────────────── */
        $this->step( $post_index, 7, 'inject_cta', 'running', 'Adding Damalion CTA…' );

        // Only inject if not already present
        if ( stripos( $content, 'dba-startup-hub-cta' ) === false ) {
            $content .= "\n\n" . $cta_html;
        }
        $this->step( $post_index, 7, 'inject_cta', 'done', 'Damalion CTA injected ✓' );

        /* ── STEP 8 — Auto-linker: inject internal links ─────────── */
        $this->step( $post_index, 8, 'install_links', 'running', 'Injecting contextual internal links…' );

        if ( class_exists( 'DBA_Auto_Linker' ) ) {
            try {
                $auto_linker = DBA_Auto_Linker::get_instance();
                $content = $auto_linker->inject_links(
                    $content,
                    $pillar_url,
                    $satellite_links,
                    $category['short'] . ' startups ' . $city['city'],
                    $category['short'],
                    4,
                    $data['title'] ?? '',
                    $category['name'] ?? ''
                );
                $content = $auto_linker->link_country_names( $content );
            } catch ( \Throwable $e ) {
                error_log( self::LOG_PREFIX . ' Auto-linker error: ' . $e->getMessage() );
            }
        }
        $this->step( $post_index, 8, 'install_links', 'done', 'Internal links injected ✓' );

        $data['content_html'] = $content;

        // Insert the post
        $post_args = array(
            'post_title'    => sanitize_text_field( $data['title'] ),
            'post_name'     => sanitize_title( $data['slug'] ?? '' ),
            'post_content'  => wp_kses_post( $data['content_html'] ),
            'post_excerpt'  => sanitize_text_field( $data['excerpt'] ?? '' ),
            'post_status'   => 'draft',
            'post_type'     => 'post',
            'post_author'   => intval( $settings['author_id'] ?? get_current_user_id() ?: 1 ),
            'post_category' => array( $cat_id ),
        );

        $post_id = wp_insert_post( $post_args );

        if ( ! $post_id || is_wp_error( $post_id ) ) {
            $this->step( $post_index, 6, 'create_draft', 'failed', 'wp_insert_post error' );
            return new WP_Error( 'insert_failed', 'Failed to create WordPress post' );
        }

        // Tags
        if ( ! empty( $data['tags'] ) && is_array( $data['tags'] ) ) {
            wp_set_post_tags( $post_id, array_map( 'sanitize_text_field', $data['tags'] ) );
        }

        // Meta — ORDER MATTERS: _dba_subject BEFORE _dba_auto_created
        // (global dedup hook checks _dba_auto_created first — if not set, it skips)
        update_post_meta( $post_id, '_dba_subject',      $subject_key );
        update_post_meta( $post_id, self::META_KEY,      current_time( 'mysql' ) );
        update_post_meta( $post_id, '_dba_auto_created',  current_time( 'mysql' ) );
        update_post_meta( $post_id, self::META_CATEGORY, $category['slug'] );
        update_post_meta( $post_id, self::META_CITY,     $city['city'] );
        update_post_meta( $post_id, self::META_STATE,    $city['state'] );
        update_post_meta( $post_id, '_dba_theme',        $category['short'] );
        update_post_meta( $post_id, '_dba_language',     'en' );

        if ( ! empty( $data['startups_mentioned'] ) && is_array( $data['startups_mentioned'] ) ) {
            update_post_meta( $post_id, self::META_STARTUPS, $data['startups_mentioned'] );
        }

        // Kaizen: store original
        DBA_Startup_Hub_Kaizen::store_original( $post_id, $data['title'] ?? '', $data['content_html'] ?? '', $data['excerpt'] ?? '' );

        // Disable Divi Builder
        update_post_meta( $post_id, '_et_pb_use_builder', 'off' );
        update_post_meta( $post_id, '_et_pb_old_content', $data['content_html'] );

        $this->step( $post_index, 6, 'create_draft', 'done', 'Post #' . $post_id . ' created as draft' );

        /* ── STEP 9 + 10 + 11 — Featured image pipeline ────────── */
        $image_data = $data['featured_image'] ?? array();

        // Fallback: build a default prompt if AI didn't provide one
        if ( empty( $image_data['prompt'] ) ) {
            $image_data = array(
                'prompt'      => "Documentary editorial photo of a professional entrepreneur in {$city['city']}, {$city['state']}. Modern office or cityscape background. Realistic, natural lighting, candid pose. NO text, NO logos, NO words on the image.",
                'filename'    => sanitize_title( $data['slug'] ?? 'startup-hub' ) . '-image.webp',
                'alt'         => "{$category['short']} startup ecosystem in {$city['city']}, {$city['state']}",
                'title'       => $data['title'] ?? "{$category['short']} in {$city['city']}",
                'caption'     => "{$category['short']} startup scene in {$city['city']}",
                'description' => "Featured image for Damalion article about the {$category['name']} ecosystem in {$city['city']}, {$city['state']}.",
            );
        }

        $this->step( $post_index, 9, 'generate_image', 'running', 'Generating image with DALL-E 3…' );
        $img_result = $img_gen->generate_and_upload( $image_data, $post_id );

        if ( ! is_wp_error( $img_result ) ) {
            $this->step( $post_index, 10, 'upload_image', 'done', 'Image #' . $img_result . ' uploaded ✓' );
            $this->step( $post_index, 11, 'set_featured_image', 'running', 'Setting featured image…' );
            set_post_thumbnail( $post_id, $img_result );
            $this->step( $post_index, 11, 'set_featured_image', 'done', 'Featured image set ✓' );
        } else {
            $this->step( $post_index, 9, 'generate_image', 'warning', 'DALL-E failed: ' . $img_result->get_error_message() );
        }

        /* ── STEP 12 — Yoast SEO ───────────────────────────────── */
        $this->step( $post_index, 12, 'fill_yoast_seo', 'running', 'Setting SEO meta…' );

        $yoast = $data['yoast_seo'] ?? array();
        if ( ! empty( $yoast['focus_keyphrase'] ) ) {
            update_post_meta( $post_id, '_yoast_wpseo_focuskw', sanitize_text_field( $yoast['focus_keyphrase'] ) );
        }
        if ( ! empty( $yoast['seo_title'] ) ) {
            update_post_meta( $post_id, '_yoast_wpseo_title', sanitize_text_field( mb_substr( $yoast['seo_title'], 0, 60 ) ) );
        }
        if ( ! empty( $yoast['meta_description'] ) ) {
            update_post_meta( $post_id, '_yoast_wpseo_metadesc', sanitize_text_field( mb_substr( $yoast['meta_description'], 0, 155 ) ) );
        }
        $this->step( $post_index, 12, 'fill_yoast_seo', 'done', 'Yoast SEO filled ✓' );

        /* ── STEP 13 — Anti-orphan protection ───────────────────── */
        $this->step( $post_index, 13, 'anti_orphan', 'running', 'Linking from hub page…' );

        $anti_orphan = DBA_Startup_Hub_Anti_Orphan::get_instance();
        $anti_orphan->link_from_hub_page( $post_id, $category['short'], $city['city'] );
        $anti_orphan->inject_backlinks_from_related( $post_id, $category['slug'], $city['city'] );

        $this->step( $post_index, 13, 'anti_orphan', 'done', 'Hub link + cross-links injected ✓' );

        /* ── STEP 14 — Publish ──────────────────────────────────── */
        $this->step( $post_index, 14, 'publish', 'running', 'Publishing…' );

        $target_status = $settings['post_status'] ?? 'publish';
        wp_update_post( array( 'ID' => $post_id, 'post_status' => $target_status ) );

        // Schedule orphan verification
        $anti_orphan->schedule_verification( $post_id );

        // Record in dedupe
        DBA_Startup_Hub_Dedupe::mark_category_used( $category['slug'] );
        DBA_Startup_Hub_Dedupe::mark_city_used( $city['city'] );
        DBA_Startup_Hub_Dedupe::record_post(
            $post_id,
            $category['slug'],
            $city['city'],
            $data['startups_mentioned'] ?? array()
        );

        // Mark subject as used today (global cross-module dedupe)
        if ( function_exists( 'dba_mark_subject_used_today' ) ) {
            dba_mark_subject_used_today( $subject_key );
        }

        // Log to shared generation log table
        if ( class_exists( 'DBA_Logger' ) ) {
            $media_id = has_post_thumbnail( $post_id ) ? get_post_thumbnail_id( $post_id ) : 0;
            DBA_Logger::get_instance()->log( array(
                'title'    => $data['title'],
                'post_id'  => $post_id,
                'media_id' => $media_id,
                'theme'    => 'startup_hub_' . $category['slug'],
                'language' => 'en',
                'status'   => 'success',
            ) );
        }

        // Update sitemap registry
        if ( class_exists( 'DBA_Sitemap' ) ) {
            $sitemap = DBA_Sitemap::get_instance();
            if ( method_exists( $sitemap, 'register_url' ) ) {
                $sitemap->register_url( get_permalink( $post_id ), $data['title'] );
            }
        }

        // Flush homepage filter cache
        DBA_Startup_Hub_Homepage_Filter::flush_cache();

        // Comprehensive cache flush (from damalion-blog-ai.php)
        wp_cache_flush();
        if ( function_exists( 'rocket_clean_domain' ) ) rocket_clean_domain();
        if ( function_exists( 'wp_cache_clear_cache' ) ) wp_cache_clear_cache();
        if ( function_exists( 'w3tc_flush_all' ) ) w3tc_flush_all();
        if ( has_action( 'litespeed_purge_all' ) ) do_action( 'litespeed_purge_all' );
        if ( function_exists( 'sg_cachepress_purge_cache' ) ) sg_cachepress_purge_cache();
        if ( function_exists( 'et_core_clear_wp_cache' ) ) et_core_clear_wp_cache();
        if ( function_exists( 'icl_cache_clear' ) ) icl_cache_clear();

        // Save last run info
        update_option( $config['last_run_option'], array(
            'post_id'   => $post_id,
            'title'     => $data['title'],
            'category'  => $category['short'],
            'city'      => $city['city'],
            'timestamp' => current_time( 'mysql' ),
        ), false );

        $this->step( $post_index, 14, 'publish', 'done',
            "Published: \"{$data['title']}\" (#{$post_id})" );

        // ── TIS: track directive progress ──
        if ( class_exists( 'DBA_TIS_Recommender' ) ) {
            DBA_TIS_Recommender::auto_track_progress( 'startup-hub', $data['title'] );
        }

        // ── PIPELINE: mark brief as published ──
        if ( ! empty( $this->_pipeline_brief ) ) {
            DBA_TIS_Pipeline::mark_published( $data['title'], $post_id );
            update_post_meta( $post_id, '_dba_pipeline_brief', wp_json_encode( $this->_pipeline_brief ) );
            $this->_pipeline_brief = null;
        }

        return array(
            'post_id'  => $post_id,
            'title'    => $data['title'],
            'category' => $category['short'],
            'city'     => $city['city'],
            'state'    => $city['state'],
        );
    }

    /* =================================================================
     *  BATCH RUNNER
     * ================================================================= */

    public function run_batch() {
        $config   = DBA_Startup_Hub_Registry::get_config();
        $settings = get_option( $config['settings_option'], array() );
        $target   = max( 1, intval( $settings['posts_per_run'] ?? $config['default_posts_per_run'] ) );

        /* ── Conflict Center: acquire global lock ── */
        if ( class_exists( 'DBA_Conflict_Center' ) ) {
            $lock = DBA_Conflict_Center::acquire( 'startup_hub', 'Startup Hub Generator' );
            if ( ! $lock ) {
                error_log( self::LOG_PREFIX . ' BLOCKED by Conflict Center — another module is generating. Will retry later.' );
                return array( 'created' => 0, 'failed' => 0, 'details' => array( 'Blocked by Conflict Center lock' ) );
            }
        }

        $this->set_progress( array(
            'total_posts' => $target, 'completed' => 0, 'failed' => 0,
            'current_post' => 0, 'started' => time(), 'steps_log' => array(),
        ) );

        $results = array( 'created' => 0, 'failed' => 0, 'details' => array() );
        $stagger_minutes = function_exists( 'dba_get_post_stagger_minutes' ) ? dba_get_post_stagger_minutes() : 20;

        for ( $i = 1; $i <= $target; $i++ ) {
            $result = $this->create_single_post( $i );

            $progress = $this->get_progress();

            if ( is_wp_error( $result ) ) {
                $results['failed']++;
                $results['details'][] = array( 'index' => $i, 'error' => $result->get_error_message() );
                $progress['failed'] = ( $progress['failed'] ?? 0 ) + 1;
            } else {
                $results['created']++;
                $results['details'][] = $result;
                $progress['completed'] = ( $progress['completed'] ?? 0 ) + 1;

                /* ── Post date stagger: space posts like a natural editorial cadence ── */
                if ( $i > 1 && ! empty( $result['post_id'] ) ) {
                    $offset_minutes = ( $i - 1 ) * $stagger_minutes;
                    $staggered_date = date( 'Y-m-d H:i:s', strtotime( "+{$offset_minutes} minutes" ) );
                    wp_update_post( array(
                        'ID'            => $result['post_id'],
                        'post_date'     => $staggered_date,
                        'post_date_gmt' => get_gmt_from_date( $staggered_date ),
                    ) );
                    error_log( self::LOG_PREFIX . " Staggered post #{$result['post_id']} → {$staggered_date} (+{$offset_minutes}min)" );
                }
            }

            $this->set_progress( $progress );

            // Pause between posts to avoid API rate limits
            if ( $i < $target ) sleep( 5 );
        }

        /* ── Conflict Center: release global lock ── */
        if ( class_exists( 'DBA_Conflict_Center' ) ) {
            DBA_Conflict_Center::release( 'startup_hub' );
        }

        return $results;
    }

    /* =================================================================
     *  GPT LINK SANITIZER — Remove fake/hallucinated damalion.com links
     *
     *  GPT sometimes invents internal URLs (wrong slugs, non-English slugs
     *  like /faire-des-affaires-en-france/, or random /blog/... paths).
     *  This method strips ANY damalion.com link that is not in the verified
     *  allowlist. The auto-linker will inject real links afterward.
     * ================================================================= */

    private function sanitize_gpt_links( string $html, array $satellite_links, string $pillar_url, string $cta_url = 'https://www.damalion.com/lets-connect/' ) : string {

        // Build allowlist of verified URLs (lowercase, no trailing slash)
        $allowed = array();
        $allowed[] = strtolower( rtrim( $pillar_url, '/' ) );
        $allowed[] = strtolower( rtrim( $cta_url, '/' ) );

        foreach ( $satellite_links as $link ) {
            $url = $link['url'] ?? ( $link[0] ?? '' );
            if ( ! empty( $url ) ) {
                $allowed[] = strtolower( rtrim( $url, '/' ) );
            }
        }
        $allowed = array_unique( $allowed );

        // Find all damalion.com <a> tags and strip any NOT in the allowlist
        $cleaned = preg_replace_callback(
            '/<a\s+[^>]*href=["\']https?:\/\/(?:www\.)?damalion\.com[^"\']*["\'][^>]*>(.*?)<\/a>/si',
            function ( $match ) use ( $allowed ) {
                // Extract the href
                if ( ! preg_match( '/href=["\']([^"\']+)["\']/i', $match[0], $href_match ) ) {
                    return $match[1]; // Can't parse? Remove the link, keep text
                }

                $href_clean = strtolower( rtrim( $href_match[1], '/' ) );

                // If the URL is in our verified allowlist, keep it
                if ( in_array( $href_clean, $allowed ) ) {
                    return $match[0];
                }

                // NOT in allowlist = GPT hallucinated it. Remove link, keep anchor text.
                $anchor_text = wp_strip_all_tags( $match[1] );
                error_log( self::LOG_PREFIX . ' SANITIZER: Removed hallucinated link "' . $href_match[1] . '" (anchor: "' . $anchor_text . '")' );
                return $anchor_text;
            },
            $html
        );

        return ( $cleaned !== null ) ? $cleaned : $html;
    }

    /* =================================================================
     *  PROMPT BUILDERS
     * ================================================================= */

    private function build_research_prompt( array $category, array $city, array $exclude_startups = array() ) {
        $prompt = "Find 2-3 real {$category['name']} startups based in or near {$city['city']}, {$city['state']}. "
            . "For each startup, find: company name, founder(s), official website, most recent funding round "
            . "(amount, series, lead investor), and what the company does. "
            . "Also find: the overall {$category['short']} ecosystem size in {$city['city']}, "
            . "total VC investment in the area, and any recent regulatory or market trends. "
            . "Use sources like TechCrunch, Crunchbase, PitchBook, SEC filings, and company press releases.";

        if ( ! empty( $exclude_startups ) ) {
            $prompt .= "\n\nDO NOT mention these companies (already covered recently): "
                . implode( ', ', array_slice( $exclude_startups, 0, 10 ) );
        }

        return $prompt;
    }

    private function build_system_prompt( array $category, array $city, array $satellite_links, string $pillar_url, string $cta_html, string $kaizen_context = '' ) {
        $link_list = '';
        if ( ! empty( $satellite_links ) ) {
            foreach ( $satellite_links as $link ) {
                $url = $link['url'] ?? ( $link[0] ?? '' );
                $anchor = $link['title'] ?? ( $link[1] ?? '' );
                if ( $url && $anchor ) $link_list .= "  - \"{$anchor}\" → {$url}\n";
            }
        }

        $system = "You are an expert business journalist writing for Damalion, an international consulting firm. "
            . "You write professional articles about U.S. startup ecosystems for an audience of international investors, "
            . "family offices, private equity firms, and entrepreneurs.\n\n"

            . "ARTICLE RULES:\n"
            . "- Write in English, professional tone, 1000-1800 words\n"
            . "- Every company name, funding amount, and founder name MUST come from research (NEVER invent)\n"
            . "- Do NOT mention Lili Bank or Mercury Bank anywhere in the article\n"
            . "- If research is insufficient, focus on ecosystem trends instead of specific startups\n"
            . "- Include 3-5 H2 section headings\n"
            . "- Include 3-5 FAQs in an accordion div at the end\n"
            . "- FEATURED IMAGE IS MANDATORY: Include a featured_image object with a detailed DALL-E prompt. The prompt must describe a documentary editorial photo — realistic, professional, NO text/logos/words on the image. Include filename, alt, title, caption, and description keys.\n"
            . "- The article MUST mention {$city['city']}, {$city['state']}\n"
            . "- Link to each startup's official website (external links)\n"
            . "- Include pillar link: {$pillar_url} (in the first 2 paragraphs)\n"
            . "- The Damalion CTA below MUST appear at the end, BEFORE the FAQ section:\n"
            . $cta_html . "\n\n"

            . "═══════════════════════════════════════════════════════════\n"
            . "INTERNAL LINKS — CRITICAL RULES (READ CAREFULLY)\n"
            . "═══════════════════════════════════════════════════════════\n\n"

            . "You are ONLY allowed to use these EXACT damalion.com URLs:\n"
            . "  - Pillar: {$pillar_url}\n"
            . "  - CTA: https://www.damalion.com/lets-connect/\n"
            . ( $link_list ? "  - Satellite links (use 2-3 naturally in body text):\n{$link_list}\n" : '' )
            . "\n"

            . "EVERYTHING ELSE IS FORBIDDEN. Specifically:\n\n"

            . "❌ NEVER invent damalion.com URLs. If a URL is not listed above, it DOES NOT EXIST.\n"
            . "❌ NEVER link to https://www.damalion.com/blog/ — this is a hub page, not an article.\n"
            . "❌ NEVER translate URLs into French, German, Spanish, or any other language.\n"
            . "   BAD: /faire-des-affaires-en-france/ — DOES NOT EXIST\n"
            . "   BAD: /unternehmen-in-deutschland/ — DOES NOT EXIST\n"
            . "   BAD: /hacer-negocios-en-mexico/ — DOES NOT EXIST\n"
            . "❌ NEVER translate anchor text. All anchor text MUST be in English.\n"
            . "   BAD: <a href=\"...\">faire des affaires en France</a>\n"
            . "   BAD: <a href=\"...\">Geschäft in Deutschland</a>\n"
            . "❌ NEVER guess or construct damalion.com URLs from patterns you think might work.\n"
            . "   BAD: /startup-funding-guide/ — you made this up\n"
            . "   BAD: /company-registration-france/ — you made this up\n"
            . "   BAD: /doing-business-in-germany/ — even if this LOOKS right, do NOT use it unless it is listed above\n\n"

            . "✅ DO use the pillar URL in the first 2 paragraphs.\n"
            . "✅ DO use 2-3 satellite links (listed above) naturally in the body.\n"
            . "✅ DO use the CTA link in the CTA section.\n"
            . "✅ DO link to external startup websites (crunchbase, company sites, etc.).\n"
            . "✅ DO write all anchor text in English.\n\n"

            . "WHY: An automated system will inject additional verified internal links AFTER your article is generated.\n"
            . "You do NOT need to add extra internal links. The system handles it.\n"
            . "Any fake link you create will be STRIPPED and damages SEO.\n"
            . "═══════════════════════════════════════════════════════════\n\n"

            . "RESPOND WITH VALID JSON ONLY (no markdown fences, no commentary). Schema:\n"
            . '{"title":"...", "slug":"...", "excerpt":"...", "content_html":"...", '
            . '"featured_image":{"prompt":"DALL-E prompt: editorial documentary photo, professional, realistic, NO text or logos or words on the image", "filename":"slug-name-image.webp", "alt":"SEO alt text", "title":"image title", "caption":"short caption", "description":"detailed image description"}, '
            . '"tags":["tag1","tag2"], "startups_mentioned":["Company A","Company B"], '
            . '"yoast_seo":{"focus_keyphrase":"...", "seo_title":"... | Damalion", "meta_description":"..."}, '
            . '"faqs":[{"question":"...","answer":"..."}]}'
            . "\n\n" . $kaizen_context;

        return $system;
    }

    private function build_user_prompt( array $category, array $city, string $research_context, array $satellite_links, string $pillar_url, string $cta_html ) {
        $prompt = "Write a {$category['name']} startup ecosystem article for {$city['city']}, {$city['state']}.\n\n"
            . "CATEGORY FOCUS: {$category['name']}\n"
            . "KEYWORDS: " . implode( ', ', $category['keywords'] ) . "\n"
            . "CITY: {$city['city']}, {$city['state']}\n"
            . "ECOSYSTEM: {$city['ecosystem']}\n"
            . "DAMALION ANGLE: {$category['damalion_angle']}\n\n";

        if ( ! empty( $research_context ) ) {
            $prompt .= "RESEARCH DATA (use this as your primary source — every fact must come from here):\n"
                . "═══════════════════════════════════════\n"
                . mb_substr( $research_context, 0, 6000 )
                . "\n═══════════════════════════════════════\n\n";
        } else {
            $prompt .= "NOTE: Web research returned no results. Write about the {$category['short']} ECOSYSTEM TRENDS "
                . "in {$city['city']} generally — do NOT invent specific startup names or funding amounts.\n\n";
        }

        $prompt .= "Remember: Include the Damalion CTA div (dba-startup-hub-cta) at the end before FAQs. "
            . "Pillar link ({$pillar_url}) must appear in the first 2 paragraphs. "
            . "IMPORTANT: You MUST include a featured_image object with a detailed DALL-E prompt (documentary photo, NO text on image), filename, alt, title, caption, and description. "
            . "INTERNAL LINKS REMINDER: Do NOT create ANY damalion.com links beyond the pillar, CTA, and satellite links provided in the system prompt. "
            . "Do NOT link to /blog/. Do NOT translate any URL or anchor text into French, German, or any language. "
            . "Write ALL text in English. Additional internal links are injected automatically after generation. "
            . "Respond with valid JSON only.";

        return $prompt;
    }

    /* =================================================================
     *  AJAX HANDLERS
     * ================================================================= */

    public function ajax_generate_single() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Permission denied' );

        @set_time_limit( 300 );

        /* ── Conflict Center: acquire global lock ── */
        if ( class_exists( 'DBA_Conflict_Center' ) ) {
            $lock = DBA_Conflict_Center::acquire( 'startup_hub', 'Startup Hub (manual)' );
            if ( ! $lock ) {
                wp_send_json_error( array( 'message' => 'Another module is generating. Please wait a few minutes and try again.' ) );
            }
        }

        $this->set_progress( array(
            'total_posts' => 1, 'completed' => 0, 'failed' => 0,
            'current_post' => 1, 'current_step' => 0, 'started' => time(), 'steps_log' => array(),
        ) );

        $result = $this->create_single_post( 1 );

        /* ── Conflict Center: release global lock ── */
        if ( class_exists( 'DBA_Conflict_Center' ) ) {
            DBA_Conflict_Center::release( 'startup_hub' );
        }

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ) );
        }

        wp_send_json_success( array( 'message' => 'Post created: ' . $result['title'], 'data' => $result ) );
    }

    public function ajax_generate_batch() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Permission denied' );

        @set_time_limit( 900 );

        $results = $this->run_batch();

        wp_send_json_success( array(
            'message' => sprintf( 'Batch complete! Created: %d, Failed: %d', $results['created'], $results['failed'] ),
            'data'    => $results,
        ) );
    }

    public function ajax_get_progress() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Permission denied' );
        wp_send_json_success( $this->get_progress() );
    }

    /* =================================================================
     *  AJAX: Save custom CTA from Startup Hub admin panel
     * ================================================================= */

    public function ajax_save_cta() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Permission denied' );

        $cta_html = wp_kses_post( $_POST['cta_html'] ?? '' );

        $config   = DBA_Startup_Hub_Registry::get_config();
        $settings = get_option( $config['settings_option'], array() );
        $settings['cta_html_override'] = $cta_html;
        update_option( $config['settings_option'], $settings );

        error_log( self::LOG_PREFIX . ' Custom CTA saved (' . strlen( $cta_html ) . ' chars)' );

        wp_send_json_success( array(
            'message' => empty( $cta_html )
                ? 'Custom CTA cleared — will use default from registry.'
                : 'Custom CTA saved ✓ (' . strlen( $cta_html ) . ' chars)',
        ) );
    }

    /* =================================================================
     *  PUBLIC HELPER: Get current CTA (for admin preview)
     * ================================================================= */

    public function get_current_cta() {
        $config   = DBA_Startup_Hub_Registry::get_config();
        $settings = get_option( $config['settings_option'], array() );

        if ( ! empty( $settings['cta_html_override'] ) ) {
            return array(
                'source' => 'custom',
                'html'   => $settings['cta_html_override'],
            );
        }

        return array(
            'source' => 'default',
            'html'   => DBA_Startup_Hub_Registry::get_partnership_cta(),
        );
    }
}
