<?php
/**
 * DBA NewsBot Admin — Settings page, manual trigger, status display
 *
 * Adds a submenu page under the main Damalion Blog AI menu:
 *   Damalion Blog AI → PE News Bot
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  ZERO CONFLICT: Separate submenu page (dba-pe-newsbot)
 *  Core admin uses: dba-settings, dba-logs
 *  PE admin uses:   dba-pe-newsbot (own JS: pe-admin.js, own settings group)
 *
 *  PUBLISH TIME: 10:00 AM New York (America/New_York)
 *  Timezone is fixed — not configurable — per docx instructions.
 * ═══════════════════════════════════════════════════════════════════════
 *
 * Features:
 *   - Enable/disable PE module
 *   - Configure posting days, posts per day, post status, author
 *   - Manual trigger button (Run PE Batch Now)
 *   - Live progress bar (AJAX polling)
 *   - Last run status display
 *   - Next scheduled run display
 *   - Dedupe history viewer
 *   - Zero conflict policy display
 *
 * @since 2.6.0
 * @since 2.6.1 — Fixed publish time to 10:00 AM New York, added zero-conflict panel
 */

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

class DBA_NewsBot_Admin {

    private static $instance = null;

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

    private function __construct() {
        add_action( 'admin_menu', array( $this, 'add_submenu' ), 20 );
        add_action( 'admin_init', array( $this, 'register_settings' ) );
        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );

        // ═══ Direct AJAX endpoints in admin (bypasses OPcache issues with generator) ═══
        add_action( 'wp_ajax_dba_pe_diag',           array( $this, 'ajax_diag' ) );
        add_action( 'wp_ajax_dba_pe_run_one_post',   array( $this, 'ajax_run_one_post' ) );
        add_action( 'wp_ajax_dba_pe_gen_image',      array( $this, 'ajax_gen_image' ) );
        add_action( 'wp_ajax_dba_pe_step_research',  array( $this, 'ajax_step_research' ) );
        add_action( 'wp_ajax_dba_pe_step_write',     array( $this, 'ajax_step_write' ) );
        add_action( 'wp_ajax_dba_pe_step_write_poll', array( $this, 'ajax_step_write_poll' ) );
        add_action( 'wp_ajax_dba_pe_step_save',      array( $this, 'ajax_step_save' ) );
        // Background worker — nopriv because loopback doesn't carry session
        add_action( 'wp_ajax_dba_pe_bg_write',        array( $this, 'ajax_bg_write' ) );
        add_action( 'wp_ajax_nopriv_dba_pe_bg_write', array( $this, 'ajax_bg_write' ) );
        add_action( 'wp_ajax_dba_pe_step_article',   array( $this, 'ajax_step_article' ) );

        if ( isset( $_GET['clear_pe_error'] ) && wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'clear_pe_error' ) ) {
            delete_option( 'dba_pe_last_error' );
            delete_option( 'dba_pe_debug_response' );
            wp_safe_redirect( admin_url( 'admin.php?page=dba-pe-newsbot' ) );
            exit;
        }
    }

    /**
     * Diagnostic: test API key + OpenAI connectivity + model.
     * Returns full details so we can see exactly what's broken.
     */
    public function ajax_diag() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized' );

        $result = array();

        // 1. Check API key
        $settings = get_option( 'dba_settings', array() );
        $api_key  = $settings['openai_api_key'] ?? '';
        $model    = $settings['openai_model'] ?? 'gpt-4o-mini';

        $result['api_key_exists'] = ! empty( $api_key );
        $result['api_key_prefix'] = ! empty( $api_key ) ? substr( $api_key, 0, 8 ) . '...' : '(empty)';
        $result['model']          = $model;

        // 2. Check PE settings
        $pe = get_option( 'dba_pe_settings', array() );
        $result['pe_enabled']     = ! empty( $pe['enabled'] );
        $result['posts_per_day']  = $pe['posts_per_day'] ?? '(not set)';

        // 3. Check active geos
        if ( class_exists( 'DBA_NewsBot_Dedupe' ) ) {
            $result['active_geos'] = DBA_NewsBot_Dedupe::get_active_geo_names();
        } else {
            $result['active_geos'] = 'DBA_NewsBot_Dedupe class not found!';
        }

        // 4. Test OpenAI API with minimal call
        if ( ! empty( $api_key ) ) {
            $test_body = array(
                'model'        => $model,
                'input'        => 'Return exactly: {"status":"ok"}',
                'instructions' => 'Return only valid JSON.',
                'store'        => false,
            );

            $test_resp = wp_remote_post( 'https://api.openai.com/v1/responses', array(
                'headers' => array(
                    'Authorization' => 'Bearer ' . $api_key,
                    'Content-Type'  => 'application/json',
                ),
                'body'    => wp_json_encode( $test_body ),
                'timeout' => 30,
            ) );

            if ( is_wp_error( $test_resp ) ) {
                $result['api_test'] = 'WP_ERROR: ' . $test_resp->get_error_message();
            } else {
                $code = wp_remote_retrieve_response_code( $test_resp );
                $body = wp_remote_retrieve_body( $test_resp );
                $result['api_test_http']   = $code;
                $result['api_test_body']   = substr( $body, 0, 500 );
            }
        } else {
            $result['api_test'] = 'SKIPPED — no API key';
        }

        // 5. Check last error
        $result['last_error'] = get_option( 'dba_pe_last_error', '(none)' );

        // 6. Check debug response
        $result['debug_response'] = get_option( 'dba_pe_debug_response', '(none)' );

        wp_send_json_success( $result );
    }

    /**
     * Run one post via the generator — with full error wrapping.
     * This is called from admin JS and guarantees error visibility.
     */
    public function ajax_run_one_post() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized' );

        @set_time_limit( 90 );
        @ini_set( 'memory_limit', '512M' );

        // Force OPcache to re-read the generator file
        $gen_file = dirname( __FILE__ ) . '/class-newsbot-generator.php';
        if ( function_exists( 'opcache_invalidate' ) ) {
            opcache_invalidate( $gen_file, true );
        }

        $post_index = max( 1, intval( $_POST['post_index'] ?? 1 ) );

        // Register shutdown handler for fatal errors
        register_shutdown_function( function() use ( $post_index ) {
            $error = error_get_last();
            if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ), true ) ) {
                $msg = sprintf( 'PHP FATAL: %s in %s:%d', $error['message'], basename( $error['file'] ), $error['line'] );
                update_option( 'dba_pe_last_error', array(
                    'time' => current_time( 'mysql' ), 'code' => 'fatal', 'message' => $msg,
                ), false );
            }
        } );

        try {
            $generator = DBA_NewsBot_Generator::get_instance();
            $result    = $generator->create_single_post( $post_index );
        } catch ( \Throwable $e ) {
            $msg = sprintf( 'EXCEPTION: %s in %s:%d', $e->getMessage(), basename( $e->getFile() ), $e->getLine() );
            wp_send_json_success( array( 'status' => 'failed', 'error' => $msg, 'post_index' => $post_index ) );
            return;
        }

        if ( is_wp_error( $result ) ) {
            $code = $result->get_error_code();
            $msg  = $result->get_error_message();
            $data = $result->get_error_data();
            $full = "[{$code}] " . ( $msg ?: '(no msg)' ) . ( $data ? ' data=' . ( is_string( $data ) ? $data : wp_json_encode( $data ) ) : '' );
            wp_send_json_success( array( 'status' => 'failed', 'error' => $full, 'post_index' => $post_index ) );
            return;
        }

        wp_send_json_success( array_merge( $result, array( 'post_index' => $post_index ) ) );
    }

    /**
     * Generate DALL-E image for a post that was created without one.
     * Called automatically by the JS after each post is created.
     * Separate AJAX call = separate Cloudflare timeout budget.
     */
    public function ajax_gen_image() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized' );

        @set_time_limit( 90 );

        $post_id = intval( $_POST['post_id'] ?? 0 );
        if ( ! $post_id ) {
            wp_send_json_success( array( 'status' => 'skipped', 'reason' => 'No post_id' ) );
            return;
        }

        // Read stored image data from post meta
        $image_data = get_post_meta( $post_id, '_dba_pe_image_data', true );
        if ( empty( $image_data ) || empty( $image_data['prompt'] ) ) {
            wp_send_json_success( array( 'status' => 'skipped', 'reason' => 'No image prompt stored' ) );
            return;
        }

        // Already has a featured image?
        if ( has_post_thumbnail( $post_id ) ) {
            wp_send_json_success( array( 'status' => 'skipped', 'reason' => 'Already has thumbnail' ) );
            return;
        }

        try {
            $img_gen    = DBA_Image_Generator::get_instance();
            $img_result = $img_gen->generate_and_upload( $image_data, $post_id );
        } catch ( \Throwable $e ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => $e->getMessage() ) );
            return;
        }

        if ( is_wp_error( $img_result ) ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => $img_result->get_error_message() ) );
            return;
        }

        $media_id = $img_result;
        set_post_thumbnail( $post_id, $media_id );

        // Fix alt text & description — use article title, not DALL-E prompt
        $article_title = get_the_title( $post_id );
        update_post_meta( $media_id, '_wp_attachment_image_alt', sanitize_text_field( $article_title ) );
        wp_update_post( array( 'ID' => $media_id, 'post_content' => sanitize_text_field( $article_title ) ) );

        // Clear the pending flag
        delete_post_meta( $post_id, '_dba_pe_image_pending' );

        wp_send_json_success( array(
            'status'   => 'ok',
            'media_id' => $media_id,
            'post_id'  => $post_id,
        ) );
    }

    /**
     * STEP 1: Research candidate (OpenAI web search + dedupe).
     * ~25s. Stores candidate in transient for step 2.
     */
    public function ajax_step_research() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized' );

        @set_time_limit( 60 );

        $post_index = max( 1, intval( $_POST['post_index'] ?? 1 ) );

        try {
            $generator = DBA_NewsBot_Generator::get_instance();
            $candidate = $generator->find_news_candidate( $post_index );
        } catch ( \Throwable $e ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => 'EXCEPTION: ' . $e->getMessage() ) );
            return;
        }

        if ( is_wp_error( $candidate ) ) {
            $code = $candidate->get_error_code();
            $msg  = $candidate->get_error_message();
            wp_send_json_success( array( 'status' => 'failed', 'error' => "[{$code}] {$msg}" ) );
            return;
        }

        // Dedupe check
        $dedupe = DBA_NewsBot_Dedupe::validate_and_reserve( array(
            'date'         => current_time( 'Y-m-d' ),
            'geo'          => $candidate['geo'],
            'company_name' => $candidate['company_name'],
            'official_url' => $candidate['official_url'] ?? '',
        ) );

        if ( ! $dedupe['ok'] ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => 'Dedupe: ' . $dedupe['reason'] ) );
            return;
        }

        // Store candidate for step 2
        $transient_key = 'dba_pe_candidate_' . $post_index;
        set_transient( $transient_key, $candidate, 300 ); // 5 min TTL

        wp_send_json_success( array(
            'status'       => 'ok',
            'company_name' => $candidate['company_name'],
            'geo'          => $candidate['geo'],
            'headline'     => $candidate['headline'] ?? '',
            'post_index'   => $post_index,
        ) );
    }

    /**
     * STEP 2a: Start article generation in background.
     * Spawns a non-blocking loopback so Cloudflare timeout doesn't apply.
     * JS polls ajax_step_write_poll for result.
     */
    public function ajax_step_write() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized' );

        $post_index = max( 1, intval( $_POST['post_index'] ?? 1 ) );

        $candidate = get_transient( 'dba_pe_candidate_' . $post_index );
        if ( empty( $candidate ) || ! is_array( $candidate ) ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => 'No candidate — run step 1 first' ) );
            return;
        }

        // Create one-time auth token for the background worker
        $token = wp_generate_password( 32, false );
        set_transient( 'dba_pe_bg_token_' . $post_index, $token, 300 );

        // Mark as working
        update_option( 'dba_pe_write_status_' . $post_index, array(
            'status' => 'working', 'started' => time(),
        ), false );

        // Spawn non-blocking loopback to background worker
        wp_remote_post( admin_url( 'admin-ajax.php' ), array(
            'blocking'  => false,
            'timeout'   => 1,
            'sslverify' => false,
            'body'      => array(
                'action'     => 'dba_pe_bg_write',
                'post_index' => $post_index,
                'token'      => $token,
            ),
        ) );

        wp_send_json_success( array(
            'status'     => 'started',
            'post_index' => $post_index,
        ) );
    }

    /**
     * Background worker — runs the OpenAI article call server-side.
     * Triggered by non-blocking loopback, NOT through Cloudflare from browser.
     * Uses nopriv because the loopback doesn't carry admin session cookies.
     */
    public function ajax_bg_write() {
        $post_index = max( 1, intval( $_POST['post_index'] ?? 1 ) );
        $token      = sanitize_text_field( $_POST['token'] ?? '' );

        // Verify one-time token
        $expected = get_transient( 'dba_pe_bg_token_' . $post_index );
        if ( empty( $expected ) || ! hash_equals( $expected, $token ) ) {
            wp_send_json_error( 'Invalid token' );
            return;
        }
        delete_transient( 'dba_pe_bg_token_' . $post_index );

        @set_time_limit( 120 );
        @ini_set( 'memory_limit', '512M' );

        $candidate = get_transient( 'dba_pe_candidate_' . $post_index );
        if ( empty( $candidate ) || ! is_array( $candidate ) ) {
            update_option( 'dba_pe_write_status_' . $post_index, array(
                'status' => 'failed', 'error' => 'No candidate in transient',
            ), false );
            wp_die();
            return;
        }

        try {
            $generator = DBA_NewsBot_Generator::get_instance();
            $article   = $generator->call_openai_for_article( $candidate );
        } catch ( \Throwable $e ) {
            update_option( 'dba_pe_write_status_' . $post_index, array(
                'status' => 'failed', 'error' => 'EXCEPTION: ' . $e->getMessage(),
            ), false );
            wp_die();
            return;
        }

        if ( is_wp_error( $article ) ) {
            update_option( 'dba_pe_write_status_' . $post_index, array(
                'status' => 'failed',
                'error'  => '[' . $article->get_error_code() . '] ' . $article->get_error_message(),
            ), false );
            wp_die();
            return;
        }

        // Subtitle auto-fix (same as in create_single_post)
        $html = $article['content_html'] ?? '';
        $has_subtitle = ( stripos( $html, 'class="subtitle"' ) !== false )
            || ( stripos( $html, "class='subtitle'" ) !== false );
        if ( ! $has_subtitle && ! empty( $html ) ) {
            $sub_text = ! empty( $article['excerpt'] ) ? $article['excerpt'] : ( $article['title'] ?? '' );
            $sub_words = explode( ' ', strip_tags( $sub_text ) );
            if ( count( $sub_words ) > 25 ) {
                $sub_text = implode( ' ', array_slice( $sub_words, 0, 25 ) ) . '.';
            }
            $subtitle_tag = '<p class="subtitle">' . esc_html( trim( $sub_text ) ) . '</p>';
            if ( stripos( $html, '</h1>' ) !== false ) {
                $pos = stripos( $html, '</h1>' ) + 5;
                $article['content_html'] = substr( $html, 0, $pos ) . "\n" . $subtitle_tag . "\n" . substr( $html, $pos );
            } else {
                $article['content_html'] = $subtitle_tag . "\n" . $html;
            }
        }

        // Title truncation (geo-aware) — keep under 80 chars but never cut the geography
        $geo_name = $candidate['geo'] ?? '';
        if ( ! empty( $article['title'] ) && mb_strlen( $article['title'] ) > 80 ) {
            $old_title = $article['title'];
            $cut = mb_substr( $article['title'], 0, 80 );
            $last_space = mb_strrpos( $cut, ' ' );
            $article['title'] = ( $last_space > 40 ) ? mb_substr( $cut, 0, $last_space ) : $cut;
            if ( ! empty( $geo_name ) && stripos( $old_title, $geo_name ) !== false && stripos( $article['title'], $geo_name ) === false ) {
                $article['title'] = rtrim( $article['title'], ' –—-,' ) . ' – ' . $geo_name;
            }
            if ( ! empty( $article['content_html'] ) ) {
                $article['content_html'] = preg_replace( '/<h1[^>]*>.*?<\/h1>/is', '<h1>' . esc_html( $article['title'] ) . '</h1>', $article['content_html'], 1 );
            }
        }

        // Store article for step_save
        set_transient( 'dba_pe_article_' . $post_index, $article, 300 );

        update_option( 'dba_pe_write_status_' . $post_index, array(
            'status' => 'done',
            'title'  => $article['title'] ?? '',
        ), false );

        wp_die();
    }

    /**
     * Poll for background article result.
     */
    public function ajax_step_write_poll() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized' );

        $post_index = max( 1, intval( $_POST['post_index'] ?? 1 ) );
        $status = get_option( 'dba_pe_write_status_' . $post_index, array() );

        if ( empty( $status ) ) {
            wp_send_json_success( array( 'status' => 'unknown' ) );
            return;
        }

        // Check for stuck job (>120s)
        if ( ( $status['status'] ?? '' ) === 'working' && isset( $status['started'] ) ) {
            if ( ( time() - $status['started'] ) > 120 ) {
                $status['status'] = 'failed';
                $status['error']  = 'Background worker timed out after 120s';
            }
        }

        wp_send_json_success( $status );
    }

    /**
     * STEP 2b: Validate + save post (~10-15s).
     * Reads candidate + article from transients, calls create_single_post with both.
     */
    public function ajax_step_save() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized' );

        @set_time_limit( 60 );

        $post_index = max( 1, intval( $_POST['post_index'] ?? 1 ) );

        $candidate = get_transient( 'dba_pe_candidate_' . $post_index );
        $article   = get_transient( 'dba_pe_article_' . $post_index );

        if ( empty( $candidate ) || ! is_array( $candidate ) ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => 'No candidate in transient' ) );
            return;
        }
        if ( empty( $article ) || ! is_array( $article ) ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => 'No article in transient — run step 2a first' ) );
            return;
        }

        // Clean up transients
        delete_transient( 'dba_pe_candidate_' . $post_index );
        delete_transient( 'dba_pe_article_' . $post_index );

        try {
            $generator = DBA_NewsBot_Generator::get_instance();
            $result    = $generator->create_single_post( $post_index, $candidate, $article );
        } catch ( \Throwable $e ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => 'EXCEPTION: ' . $e->getMessage() ) );
            return;
        }

        if ( is_wp_error( $result ) ) {
            $code = $result->get_error_code();
            $msg  = $result->get_error_message();
            $data = $result->get_error_data();
            $full = "[{$code}] " . ( $msg ?: '(no msg)' ) . ( $data ? ' data=' . ( is_string( $data ) ? $data : wp_json_encode( $data ) ) : '' );
            wp_send_json_success( array( 'status' => 'failed', 'error' => $full, 'post_index' => $post_index ) );
            return;
        }

        wp_send_json_success( array_merge( $result, array( 'post_index' => $post_index ) ) );
    }

    /**
     * STEP 2 (legacy): Generate article + validate + save post in one call.
     * Kept for backward compatibility with cron calls.
     */
    public function ajax_step_article() {
        check_ajax_referer( 'dba_admin_nonce', 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Unauthorized' );

        @set_time_limit( 90 );

        $post_index = max( 1, intval( $_POST['post_index'] ?? 1 ) );

        // Read candidate from step 1
        $transient_key = 'dba_pe_candidate_' . $post_index;
        $candidate = get_transient( $transient_key );
        if ( empty( $candidate ) || ! is_array( $candidate ) ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => 'No candidate found — run step 1 first' ) );
            return;
        }
        delete_transient( $transient_key );

        try {
            $generator = DBA_NewsBot_Generator::get_instance();
            $result    = $generator->create_single_post( $post_index, $candidate );
        } catch ( \Throwable $e ) {
            wp_send_json_success( array( 'status' => 'failed', 'error' => 'EXCEPTION: ' . $e->getMessage() ) );
            return;
        }

        if ( is_wp_error( $result ) ) {
            $code = $result->get_error_code();
            $msg  = $result->get_error_message();
            $data = $result->get_error_data();
            $full = "[{$code}] " . ( $msg ?: '(no msg)' ) . ( $data ? ' data=' . ( is_string( $data ) ? $data : wp_json_encode( $data ) ) : '' );
            wp_send_json_success( array( 'status' => 'failed', 'error' => $full, 'post_index' => $post_index ) );
            return;
        }

        wp_send_json_success( array_merge( $result, array( 'post_index' => $post_index ) ) );
    }

    public function add_submenu() {
        add_submenu_page(
            'dba-settings',                        // parent slug
            'PE News Bot — U.S. Capital Markets',  // page title
            '🇺🇸 PE News Bot',                     // menu title
            'manage_options',
            'dba-pe-newsbot',
            array( $this, 'render_page' )
        );
    }

    public function register_settings() {
        register_setting( 'dba_pe_settings_group', 'dba_pe_settings', array( $this, 'sanitize' ) );
    }

    public function sanitize( $input ) {
        $clean = array();
        $clean['enabled']       = ! empty( $input['enabled'] ) ? true : false;
        $clean['posts_per_day'] = max( 1, min( 50, intval( $input['posts_per_day'] ?? 6 ) ) );
        $clean['post_status']   = in_array( $input['post_status'] ?? '', array( 'publish', 'draft', 'pending' ), true )
                                    ? $input['post_status'] : 'publish';
        $clean['author_id']     = max( 1, intval( $input['author_id'] ?? 1 ) );
        $clean['timezone']      = 'America/New_York'; // Fixed — not configurable

        // Posting days
        $valid_days = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' );
        $days = array();
        if ( ! empty( $input['posting_days'] ) && is_array( $input['posting_days'] ) ) {
            foreach ( $input['posting_days'] as $d ) {
                $d = strtolower( sanitize_text_field( $d ) );
                if ( in_array( $d, $valid_days, true ) ) {
                    $days[] = $d;
                }
            }
        }
        $clean['posting_days'] = ! empty( $days ) ? $days : array( 'monday', 'wednesday', 'friday' );

        // Active geographies (US states selected by operator)
        $all_states = DBA_NewsBot_Dedupe::get_all_us_states();
        $geos = array();
        if ( ! empty( $input['active_geographies'] ) && is_array( $input['active_geographies'] ) ) {
            foreach ( $input['active_geographies'] as $geo ) {
                $geo = sanitize_text_field( $geo );
                // Must be a valid state name
                if ( array_key_exists( $geo, $all_states ) ) {
                    $geos[] = $geo;
                }
            }
        }
        // Minimum 1 state required — fallback to default 7
        if ( empty( $geos ) ) {
            $geos = array( 'New York', 'Florida', 'Colorado', 'Texas', 'California', 'New Hampshire', 'Arizona' );
        }
        $clean['active_geographies'] = $geos;

        // Custom categories (name + keywords)
        $cats = array();
        if ( ! empty( $input['custom_cat_name'] ) && is_array( $input['custom_cat_name'] ) ) {
            foreach ( $input['custom_cat_name'] as $i => $name ) {
                $name     = sanitize_text_field( trim( $name ) );
                $keywords = sanitize_text_field( trim( $input['custom_cat_keywords'][ $i ] ?? '' ) );
                if ( $name !== '' ) {
                    $cats[] = array( 'name' => $name, 'keywords' => $keywords );
                }
            }
        }
        $clean['custom_categories'] = $cats;

        // Word count range
        $clean['word_count_min'] = max( 200, min( 2000, intval( $input['word_count_min'] ?? 500 ) ) );
        $clean['word_count_max'] = max( 300, min( 3000, intval( $input['word_count_max'] ?? 600 ) ) );
        if ( $clean['word_count_max'] <= $clean['word_count_min'] ) {
            $clean['word_count_max'] = $clean['word_count_min'] + 100;
        }

        // Article features
        $clean['require_subtitle']     = ! empty( $input['require_subtitle'] ) ? true : false;
        $clean['require_company_link'] = ! empty( $input['require_company_link'] ) ? true : false;
        $clean['require_gov_link']     = ! empty( $input['require_gov_link'] ) ? true : false;

        // CTA
        $clean['cta_enabled']   = ! empty( $input['cta_enabled'] ) ? true : false;
        $clean['cta_text']      = sanitize_textarea_field( $input['cta_text'] ?? 'Damalion helps international family offices, entrepreneurs, private equity and venture capital firms identify opportunities to achieve their goals.' );
        $clean['cta_link_text'] = sanitize_text_field( $input['cta_link_text'] ?? 'Contact your Damalion experts now.' );
        $clean['cta_link_url']  = esc_url_raw( $input['cta_link_url'] ?? 'https://www.damalion.com/lets-connect/' );

        // Recalculate posts_per_day max based on selected geos
        $max_posts = count( $geos );
        if ( $clean['posts_per_day'] > $max_posts ) {
            $clean['posts_per_day'] = $max_posts;
        }

        return $clean;
    }

    public function enqueue_scripts( $hook ) {
        if ( $hook !== 'damalion-blog-ai_page_dba-pe-newsbot' ) return;
        wp_enqueue_script( 'jquery' );
    }

    public function render_page() {
        $settings   = get_option( 'dba_pe_settings', array() );
        $last_run   = DBA_NewsBot_Scheduler::get_last_run_info();
        $next_run   = DBA_NewsBot_Scheduler::get_next_run_info();
        $post_days  = DBA_NewsBot_Scheduler::get_posting_days_string();
        $history    = DBA_NewsBot_Dedupe::get_history();
        $enabled    = ! empty( $settings['enabled'] );

        $authors = get_users( array( 'role__in' => array( 'administrator', 'editor', 'author' ), 'number' => 50 ) );
        ?>
        <div class="wrap">
            <h1>🇺🇸 PE / Capital Markets News Bot</h1>
            <p style="font-size:14px;color:#555;">
                Automated U.S. private equity and capital markets news publishing.
                <?php $active_geos = $settings['active_geographies'] ?? array( 'New York', 'Florida', 'Colorado', 'Texas', 'California', 'New Hampshire', 'Arizona' ); ?>
                Active geographies: <strong><?php echo esc_html( implode( ', ', $active_geos ) ); ?></strong>
                (<?php echo count( $active_geos ); ?> states — configure below).
            </p>

            <!-- STATUS BAR -->
            <div style="background:<?php echo $enabled ? '#d4edda' : '#fff3cd'; ?>;border:1px solid <?php echo $enabled ? '#c3e6cb' : '#ffc107'; ?>;padding:12px 20px;border-radius:6px;margin:15px 0;display:flex;justify-content:space-between;align-items:center;">
                <div>
                    <strong>Status:</strong> <?php echo $enabled ? '🟢 ENABLED' : '🟡 DISABLED'; ?>
                    &nbsp;|&nbsp;
                    <strong>Schedule:</strong> <?php echo esc_html( $post_days ); ?>
                    &nbsp;|&nbsp;
                    <strong>Publish time:</strong> 10:00 AM New York
                    &nbsp;|&nbsp;
                    <strong>Next run:</strong> <?php echo esc_html( $next_run ); ?>
                </div>
                <div>
                    <button id="dba-pe-run-batch" class="button button-primary" style="background:#28a745;border-color:#28a745;">
                        🚀 Run PE Batch Now
                    </button>
                    <button id="dba-pe-stop-batch" class="button" style="background:#dc3545;border-color:#dc3545;color:#fff;margin-left:8px;opacity:0.5;" disabled>
                        ⛔ Stop Batch
                    </button>
                    <button id="dba-pe-diag" class="button" style="margin-left:8px;">
                        🔍 Diagnose
                    </button>
                </div>
            </div>

            <!-- PROGRESS BAR (hidden until batch starts) -->
            <div id="dba-pe-progress" style="display:none;background:#f8f9fa;border:1px solid #ddd;padding:15px 20px;border-radius:6px;margin-bottom:20px;">
                <div style="display:flex;justify-content:space-between;margin-bottom:8px;">
                    <strong id="dba-pe-progress-label">Generating…</strong>
                    <span id="dba-pe-progress-detail" style="color:#666;"></span>
                </div>
                <div style="background:#e9ecef;border-radius:4px;height:24px;overflow:hidden;">
                    <div id="dba-pe-progress-bar" style="background:#28a745;height:100%;width:0%;transition:width 0.5s;border-radius:4px;"></div>
                </div>
                <div id="dba-pe-results" style="margin-top:12px;display:none;"></div>
            </div>

            <!-- LAST RUN -->
            <?php if ( ! empty( $last_run['time'] ) ) : ?>
            <div style="background:#f8f9fa;border:1px solid #ddd;padding:12px 20px;border-radius:6px;margin-bottom:20px;">
                <strong>Last run:</strong> <?php echo esc_html( $last_run['time'] ); ?>
                &nbsp;|&nbsp;
                <strong>Created:</strong> <?php echo intval( $last_run['results']['created'] ?? 0 ); ?>
                &nbsp;|&nbsp;
                <strong>Failed:</strong> <?php echo intval( $last_run['results']['failed'] ?? 0 ); ?>
                <?php if ( ! empty( $last_run['results']['roundup_id'] ) ) : ?>
                &nbsp;|&nbsp;
                <strong>Roundup:</strong> <a href="<?php echo get_edit_post_link( $last_run['results']['roundup_id'] ); ?>">#<?php echo intval( $last_run['results']['roundup_id'] ); ?></a>
                <?php endif; ?>
            </div>
            <?php endif; ?>

            <?php
            $pe_error = get_option( 'dba_pe_last_error', array() );
            $pe_debug = get_option( 'dba_pe_debug_response', array() );
            if ( ! empty( $pe_error['message'] ) || ! empty( $pe_debug ) ) :
            ?>
            <div style="background:#fff3cd;border:1px solid #ffc107;padding:12px 20px;border-radius:6px;margin-bottom:20px;">
                <?php if ( ! empty( $pe_error['message'] ) ) : ?>
                <p style="margin:0 0 6px;"><strong style="color:#856404;">Last Error [<?php echo esc_html( $pe_error['code'] ?? '' ); ?>] — <?php echo esc_html( $pe_error['time'] ?? '' ); ?></strong></p>
                <pre style="background:#fff;padding:8px;border:1px solid #eee;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow:auto;font-size:12px;"><?php echo esc_html( $pe_error['message'] ); ?></pre>
                <?php if ( ! empty( $pe_error['article_title'] ) ) : ?>
                <p style="margin:4px 0;font-size:12px;color:#666;">Article: <?php echo esc_html( $pe_error['article_title'] ); ?> | Geo: <?php echo esc_html( $pe_error['geo'] ?? '' ); ?></p>
                <?php endif; ?>
                <?php endif; ?>
                <?php if ( ! empty( $pe_debug['body_preview'] ) ) : ?>
                <p style="margin:8px 0 4px;"><strong style="color:#856404;">API Response — HTTP <?php echo esc_html( $pe_debug['http_status'] ?? '?' ); ?> | Status: <?php echo esc_html( $pe_debug['api_status'] ?? '?' ); ?> | Model: <?php echo esc_html( $pe_debug['model'] ?? '?' ); ?></strong></p>
                <pre style="background:#fff;padding:8px;border:1px solid #eee;white-space:pre-wrap;word-break:break-all;max-height:300px;overflow:auto;font-size:11px;"><?php echo esc_html( $pe_debug['body_preview'] ); ?></pre>
                <?php endif; ?>
                <p style="margin:8px 0 0;"><small><a href="<?php echo wp_nonce_url( admin_url( 'admin.php?page=dba-pe-newsbot&clear_pe_error=1' ), 'clear_pe_error' ); ?>">Dismiss</a></small></p>
            </div>
            <?php endif; ?>

            <!-- SETTINGS FORM -->
            <form method="post" action="options.php">
                <?php settings_fields( 'dba_pe_settings_group' ); ?>

                <table class="form-table">
                    <tr>
                        <th>Enable PE News Bot</th>
                        <td>
                            <label>
                                <input type="checkbox" name="dba_pe_settings[enabled]" value="1" <?php checked( $enabled ); ?> />
                                Activate automated PE/Capital Markets news publishing
                            </label>
                            <p class="description">When disabled, the cron job will skip execution. You can still run manually.</p>
                        </td>
                    </tr>
                    <tr>
                        <th>Publishing Days</th>
                        <td>
                            <?php
                            $all_days = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' );
                            $current_days = $settings['posting_days'] ?? array( 'monday', 'wednesday', 'friday' );
                            foreach ( $all_days as $day ) : ?>
                                <label style="margin-right:15px;">
                                    <input type="checkbox" name="dba_pe_settings[posting_days][]" value="<?php echo $day; ?>"
                                        <?php checked( in_array( $day, $current_days, true ) ); ?> />
                                    <?php echo ucfirst( $day ); ?>
                                </label>
                            <?php endforeach; ?>
                            <p class="description">Default: Monday, Wednesday, Friday. Mon/Wed: 5-7 posts, Friday: 4-6 posts.</p>
                        </td>
                    </tr>
                    <tr>
                        <th>Posts per Day</th>
                        <td>
                            <?php $max_geos = count( $settings['active_geographies'] ?? array( 'New York', 'Florida', 'Colorado', 'Texas', 'California', 'New Hampshire', 'Arizona' ) ); ?>
                            <input type="number" name="dba_pe_settings[posts_per_day]" value="<?php echo intval( $settings['posts_per_day'] ?? 6 ); ?>" min="1" max="<?php echo $max_geos; ?>" style="width:80px;" />
                            <p class="description">Max <?php echo $max_geos; ?> (one per active geography). Friday automatically reduces by 1.</p>
                        </td>
                    </tr>
                </table>

                <!-- ════════════════════════════════════════════════════════ -->
                <!--  ACTIVE GEOGRAPHIES — Select which US states to cover  -->
                <!-- ════════════════════════════════════════════════════════ -->
                <h2 style="margin-top:30px;">🗺️ Active Geographies (US States)</h2>
                <p class="description" style="margin-bottom:10px;">
                    Check the states you want the bot to cover. The bot will only search for and publish PE/Capital Markets news from these states.
                    Each selected state gets its own H2 section in the daily roundup. <strong>Max posts per day is capped at the number of states selected.</strong>
                </p>
                <div style="background:#f8f9fa;border:1px solid #ddd;padding:15px 20px;border-radius:6px;margin-bottom:20px;">
                    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
                        <strong><?php echo count( $settings['active_geographies'] ?? array( 'New York', 'Florida', 'Colorado', 'Texas', 'California', 'New Hampshire', 'Arizona' ) ); ?> states selected</strong>
                        <span>
                            <button type="button" onclick="document.querySelectorAll('.dba-geo-cb').forEach(c=>c.checked=true)" class="button button-small">Select All</button>
                            <button type="button" onclick="document.querySelectorAll('.dba-geo-cb').forEach(c=>c.checked=false)" class="button button-small">Deselect All</button>
                        </span>
                    </div>
                    <div style="column-count:4;column-gap:20px;">
                    <?php
                    $all_states   = DBA_NewsBot_Dedupe::get_all_us_states();
                    $active_geos_setting = $settings['active_geographies'] ?? array( 'New York', 'Florida', 'Colorado', 'Texas', 'California', 'New Hampshire', 'Arizona' );
                    foreach ( $all_states as $name => $slug ) : ?>
                        <label style="display:block;margin-bottom:4px;font-size:13px;white-space:nowrap;">
                            <input type="checkbox" class="dba-geo-cb" name="dba_pe_settings[active_geographies][]" value="<?php echo esc_attr( $name ); ?>"
                                <?php checked( in_array( $name, $active_geos_setting, true ) ); ?> />
                            <?php echo esc_html( $name ); ?>
                        </label>
                    <?php endforeach; ?>
                    </div>
                </div>

                <!-- ════════════════════════════════════════════════════════ -->
                <!--  CATEGORIES — Built-in + Custom                        -->
                <!-- ════════════════════════════════════════════════════════ -->
                <h2 style="margin-top:30px;">🏷️ Post Categories</h2>
                <p class="description" style="margin-bottom:10px;">
                    The bot assigns categories to articles using keyword matching. Built-in categories are always active.
                    You can add your own custom categories below with trigger keywords (comma-separated).
                </p>

                <!-- Built-in categories (read only) -->
                <div style="background:#f0f8ff;border:1px solid #b8d4e3;padding:15px 20px;border-radius:6px;margin-bottom:15px;">
                    <strong>Built-in categories (always active):</strong>
                    <table style="margin-top:8px;width:100%;border-collapse:collapse;">
                        <tr style="border-bottom:1px solid #ddd;">
                            <th style="text-align:left;padding:4px 8px;width:250px;">Category</th>
                            <th style="text-align:left;padding:4px 8px;">Trigger keywords</th>
                        </tr>
                        <tr><td style="padding:4px 8px;">Private equity</td><td style="padding:4px 8px;color:#666;">buyout, sponsor, leveraged, private equity, pe firm, pe-backed</td></tr>
                        <tr><td style="padding:4px 8px;">Capital markets</td><td style="padding:4px 8px;color:#666;">bond, pipe, placement, capital markets, debt offering</td></tr>
                        <tr><td style="padding:4px 8px;">Private debt / Private credit</td><td style="padding:4px 8px;color:#666;">direct lending, private credit, private debt, mezzanine</td></tr>
                        <tr><td style="padding:4px 8px;">Real estate</td><td style="padding:4px 8px;color:#666;">property, real estate, reit, commercial property</td></tr>
                        <tr><td style="padding:4px 8px;">IPO</td><td style="padding:4px 8px;color:#666;">ipo, listing, public offering, exit, spac</td></tr>
                        <tr><td style="padding:4px 8px;">Mergers & Acquisitions</td><td style="padding:4px 8px;color:#666;">platform acquisition, merger, acquisition, m&a, takeover</td></tr>
                    </table>
                </div>

                <!-- Custom categories (editable) -->
                <div style="background:#fff;border:1px solid #ddd;padding:15px 20px;border-radius:6px;margin-bottom:20px;">
                    <strong>Custom categories (add your own):</strong>
                    <p class="description" style="margin-bottom:10px;">
                        Add a category name and comma-separated keywords. When the bot finds these keywords in an article, it assigns the custom category.
                    </p>
                    <table id="dba-custom-cats" style="width:100%;border-collapse:collapse;">
                        <tr style="border-bottom:1px solid #ddd;">
                            <th style="text-align:left;padding:4px 8px;width:250px;">Category Name</th>
                            <th style="text-align:left;padding:4px 8px;">Trigger Keywords (comma-separated)</th>
                            <th style="width:50px;"></th>
                        </tr>
                        <?php
                        $custom_cats = $settings['custom_categories'] ?? array();
                        if ( ! empty( $custom_cats ) ) :
                            foreach ( $custom_cats as $i => $cat ) : ?>
                                <tr class="dba-cat-row">
                                    <td style="padding:4px 8px;">
                                        <input type="text" name="dba_pe_settings[custom_cat_name][]" value="<?php echo esc_attr( $cat['name'] ?? '' ); ?>" class="regular-text" style="width:100%;" placeholder="e.g. Venture Capital" />
                                    </td>
                                    <td style="padding:4px 8px;">
                                        <input type="text" name="dba_pe_settings[custom_cat_keywords][]" value="<?php echo esc_attr( $cat['keywords'] ?? '' ); ?>" class="regular-text" style="width:100%;" placeholder="e.g. venture, seed, series a, startup" />
                                    </td>
                                    <td style="padding:4px 8px;text-align:center;">
                                        <button type="button" onclick="this.closest('tr').remove()" class="button button-small" style="color:#dc3545;" title="Remove">✕</button>
                                    </td>
                                </tr>
                            <?php endforeach;
                        endif; ?>
                    </table>
                    <button type="button" id="dba-add-cat" class="button button-small" style="margin-top:8px;">+ Add Custom Category</button>
                    <script>
                    document.getElementById('dba-add-cat').addEventListener('click', function() {
                        var table = document.getElementById('dba-custom-cats');
                        var row = document.createElement('tr');
                        row.className = 'dba-cat-row';
                        row.innerHTML = '<td style="padding:4px 8px;"><input type="text" name="dba_pe_settings[custom_cat_name][]" value="" class="regular-text" style="width:100%;" placeholder="e.g. Venture Capital" /></td>'
                            + '<td style="padding:4px 8px;"><input type="text" name="dba_pe_settings[custom_cat_keywords][]" value="" class="regular-text" style="width:100%;" placeholder="e.g. venture, seed, series a, startup" /></td>'
                            + '<td style="padding:4px 8px;text-align:center;"><button type="button" onclick="this.parentNode.parentNode.remove()" class="button button-small" style="color:#dc3545;" title="Remove">✕</button></td>';
                        table.appendChild(row);
                    });
                    </script>
                </div>

                <table class="form-table">
                    <tr>
                        <th>Post Status</th>
                        <td>
                            <select name="dba_pe_settings[post_status]">
                                <option value="publish" <?php selected( $settings['post_status'] ?? 'publish', 'publish' ); ?>>Publish</option>
                                <option value="draft" <?php selected( $settings['post_status'] ?? '', 'draft' ); ?>>Draft</option>
                                <option value="pending" <?php selected( $settings['post_status'] ?? '', 'pending' ); ?>>Pending Review</option>
                            </select>
                        </td>
                    </tr>
                    <tr>
                        <th>Author</th>
                        <td>
                            <select name="dba_pe_settings[author_id]">
                                <?php foreach ( $authors as $author ) : ?>
                                <option value="<?php echo $author->ID; ?>" <?php selected( $settings['author_id'] ?? 1, $author->ID ); ?>>
                                    <?php echo esc_html( $author->display_name ); ?> (#<?php echo $author->ID; ?>)
                                </option>
                                <?php endforeach; ?>
                            </select>
                        </td>
                    </tr>
                    <tr>
                        <th>Publish Timezone</th>
                        <td>
                            <input type="text" value="America/New_York — 10:00 AM" disabled class="regular-text" style="background:#f0f0f1;" />
                            <input type="hidden" name="dba_pe_settings[timezone]" value="America/New_York" />
                            <p class="description">Fixed: All USA-related posts publish at <strong>10:00 AM New York time</strong>. Individual posts are staggered by 2 minutes each. Roundup publishes at 10:15 AM.</p>
                        </td>
                    </tr>
                </table>

                <!-- ════════════════════════════════════════════════════════ -->
                <!--  ARTICLE SETTINGS — Word count, subtitle, CTA, links   -->
                <!-- ════════════════════════════════════════════════════════ -->
                <h2 style="margin-top:30px;">📝 Article Settings</h2>

                <table class="form-table">
                    <tr>
                        <th>Word Count Range</th>
                        <td>
                            <?php $wc_min = intval( $settings['word_count_min'] ?? 500 ); $wc_max = intval( $settings['word_count_max'] ?? 600 ); ?>
                            <input type="number" name="dba_pe_settings[word_count_min]" value="<?php echo $wc_min; ?>" min="200" max="2000" style="width:90px;" />
                            &nbsp;to&nbsp;
                            <input type="number" name="dba_pe_settings[word_count_max]" value="<?php echo $wc_max; ?>" min="300" max="3000" style="width:90px;" />
                            &nbsp;words
                            <p class="description">Default: 500–600 (target ~550). The AI aims for this range; the validator enforces minimum. Max is a soft warning.</p>
                        </td>
                    </tr>
                    <tr>
                        <th>Require Subtitle</th>
                        <td>
                            <label>
                                <input type="checkbox" name="dba_pe_settings[require_subtitle]" value="1" <?php checked( ! empty( $settings['require_subtitle'] ?? true ) ); ?> />
                                Every article must include a subtitle (H3 or &lt;p class="subtitle"&gt;) below the H1 for readability &amp; SEO
                            </label>
                            <p class="description">Improves readability scores and provides a secondary hook for readers. Enabled by default.</p>
                        </td>
                    </tr>
                    <tr>
                        <th>Company Link Rule</th>
                        <td>
                            <label>
                                <input type="checkbox" name="dba_pe_settings[require_company_link]" value="1" <?php checked( ! empty( $settings['require_company_link'] ?? true ) ); ?> />
                                Company name must link to its official website
                            </label>
                            <p class="description">Already enforced — the &lt;a&gt; tag wraps the company name pointing to the company's official URL.</p>
                        </td>
                    </tr>
                    <tr>
                        <th>State/City .gov Link</th>
                        <td>
                            <label>
                                <input type="checkbox" name="dba_pe_settings[require_gov_link]" value="1" <?php checked( ! empty( $settings['require_gov_link'] ?? true ) ); ?> />
                                First mention of the state or city must link to its official .gov website
                            </label>
                            <p class="description">Example: "New York" → <code>&lt;a href="https://www.ny.gov"&gt;New York&lt;/a&gt;</code></p>
                        </td>
                    </tr>
                    <tr>
                        <th>Call to Action (CTA)</th>
                        <td>
                            <label>
                                <input type="checkbox" name="dba_pe_settings[cta_enabled]" value="1" <?php checked( ! empty( $settings['cta_enabled'] ?? true ) ); ?> />
                                Append CTA block at the end of every article
                            </label>
                            <br><br>
                            <label><strong>CTA text:</strong></label><br>
                            <textarea name="dba_pe_settings[cta_text]" rows="2" class="large-text" style="max-width:600px;"><?php echo esc_textarea( $settings['cta_text'] ?? 'Damalion helps international family offices, entrepreneurs, private equity and venture capital firms identify opportunities to achieve their goals.' ); ?></textarea>
                            <br><br>
                            <label><strong>CTA link text:</strong></label><br>
                            <input type="text" name="dba_pe_settings[cta_link_text]" value="<?php echo esc_attr( $settings['cta_link_text'] ?? 'Contact your Damalion experts now.' ); ?>" class="regular-text" style="max-width:600px;" />
                            <br><br>
                            <label><strong>CTA link URL:</strong></label><br>
                            <input type="url" name="dba_pe_settings[cta_link_url]" value="<?php echo esc_attr( $settings['cta_link_url'] ?? 'https://www.damalion.com/lets-connect/' ); ?>" class="regular-text" style="max-width:600px;" />
                            <br><br>
                            <div style="background:#f8f9fa;border:1px solid #ddd;padding:12px 16px;border-radius:4px;max-width:600px;">
                                <strong>Preview:</strong><br>
                                <p style="margin:8px 0 4px;font-style:italic;"><?php echo esc_html( $settings['cta_text'] ?? 'Damalion helps international family offices, entrepreneurs, private equity and venture capital firms identify opportunities to achieve their goals.' ); ?></p>
                                <p style="margin:0;"><a href="<?php echo esc_url( $settings['cta_link_url'] ?? 'https://www.damalion.com/lets-connect/' ); ?>"><?php echo esc_html( $settings['cta_link_text'] ?? 'Contact your Damalion experts now.' ); ?></a></p>
                            </div>
                        </td>
                    </tr>
                </table>

                <?php submit_button( 'Save PE Settings' ); ?>
            </form>

            <!-- ZERO CONFLICT POLICY INFO -->
            <h2>🛡️ Zero Conflict Policy</h2>
            <div style="background:#f0f8ff;border:1px solid #b8d4e3;padding:15px 20px;border-radius:6px;margin-bottom:20px;">
                <table style="width:100%;border-collapse:collapse;">
                    <tr>
                        <th style="text-align:left;padding:6px 12px;border-bottom:1px solid #ddd;width:200px;"></th>
                        <th style="text-align:left;padding:6px 12px;border-bottom:1px solid #ddd;">Core Damalion (EU)</th>
                        <th style="text-align:left;padding:6px 12px;border-bottom:1px solid #ddd;">PE NewsBot (US)</th>
                    </tr>
                    <tr><td style="padding:4px 12px;"><strong>Cron hook</strong></td><td style="padding:4px 12px;"><code>dba_daily_generation</code></td><td style="padding:4px 12px;"><code>dba_pe_daily_generation</code></td></tr>
                    <tr><td style="padding:4px 12px;"><strong>Run time</strong></td><td style="padding:4px 12px;">06:00 UTC (07:00 CET)</td><td style="padding:4px 12px;">15:00 UTC (10:00 AM NY)</td></tr>
                    <tr><td style="padding:4px 12px;"><strong>Timezone</strong></td><td style="padding:4px 12px;">Europe/Luxembourg</td><td style="padding:4px 12px;">America/New_York</td></tr>
                    <tr><td style="padding:4px 12px;"><strong>Default days</strong></td><td style="padding:4px 12px;">Mon / Wed / Sat</td><td style="padding:4px 12px;">Mon / Wed / Fri</td></tr>
                    <tr><td style="padding:4px 12px;"><strong>Post meta</strong></td><td style="padding:4px 12px;"><code>_dba_auto_created</code></td><td style="padding:4px 12px;"><code>_dba_pe_auto_created</code></td></tr>
                    <tr><td style="padding:4px 12px;"><strong>Progress key</strong></td><td style="padding:4px 12px;"><code>dba_generation_progress</code></td><td style="padding:4px 12px;"><code>dba_pe_generation_progress</code></td></tr>
                    <tr><td style="padding:4px 12px;"><strong>Gap</strong></td><td colspan="2" style="padding:4px 12px;">9 hours between core and PE — no overlap possible</td></tr>
                </table>
            </div>

            <!-- DEDUPE HISTORY -->
            <h2>📊 Dedupe History (Last 10 Days)</h2>
            <?php if ( empty( $history['days'] ) ) : ?>
                <p style="color:#999;">No history yet. Run a batch to populate.</p>
            <?php else : ?>
                <table class="widefat striped" style="max-width:900px;">
                    <thead>
                        <tr>
                            <th>Date</th>
                            <th>Geographies Used</th>
                            <th>Companies</th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php
                    $dates = array_keys( $history['days'] );
                    rsort( $dates );
                    foreach ( $dates as $d ) :
                        $day = $history['days'][ $d ];
                    ?>
                        <tr>
                            <td><strong><?php echo esc_html( $d ); ?></strong></td>
                            <td><?php echo esc_html( implode( ', ', $day['geos'] ?? array() ) ); ?></td>
                            <td><?php echo esc_html( implode( ', ', $day['companies'] ?? array() ) ); ?></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>

            <hr style="margin:30px 0;" />
            <p style="color:#999;font-size:12px;">
                PE News Bot v<?php echo DBA_VERSION; ?> — Cron: <code>dba_pe_daily_generation</code> at 15:00 UTC (10:00 AM New York)
                — Publish: <code>10:00 AM America/New_York</code>
                — Progress: <code>dba_pe_generation_progress</code>
                — Meta: <code>_dba_pe_auto_created</code>
            </p>
        </div>

        <script>
        (function($) {
            'use strict';

            var A = '<?php echo admin_url( "admin-ajax.php" ); ?>';
            var N = '<?php echo wp_create_nonce( "dba_admin_nonce" ); ?>';
            var polling, stopped, B = {created:0, failed:0, target:0};
            var $run = $('#dba-pe-run-batch'), $stop = $('#dba-pe-stop-batch'), $R = $('#dba-pe-results');

            /* DIAGNOSE */
            $('#dba-pe-diag').on('click', function(e) {
                e.preventDefault();
                var btn = $(this);
                btn.prop('disabled', true).text('Testing...');
                $R.empty().show();
                $('#dba-pe-progress').slideDown();
                $('#dba-pe-progress-label').text('Running diagnostics...');
                $('#dba-pe-progress-bar').css({'width':'50%','background':'#17a2b8'});

                $.post(A, {action:'dba_pe_diag', nonce:N}, function(resp) {
                    btn.prop('disabled', false).text('Diagnose');
                    $('#dba-pe-progress-label').text('Diagnostic results:');
                    $('#dba-pe-progress-bar').css('width','100%');

                    if (resp.success && resp.data) {
                        var d = resp.data;
                        add('info', '<b>API Key:</b> ' + (d.api_key_exists ? 'Found (' + d.api_key_prefix + ')' : 'MISSING'));
                        add('info', '<b>Model:</b> ' + (d.model || '(not set)'));
                        add('info', '<b>PE Enabled:</b> ' + (d.pe_enabled ? 'Yes' : 'No'));
                        add('info', '<b>Posts/day:</b> ' + d.posts_per_day);
                        add('info', '<b>Geos:</b> ' + (Array.isArray(d.active_geos) ? d.active_geos.join(', ') : d.active_geos));
                        if (d.api_test) {
                            add('fail', '<b>API Test:</b> ' + d.api_test);
                        } else {
                            var ok = d.api_test_http === 200;
                            add(ok ? 'ok' : 'fail', '<b>API Test HTTP:</b> ' + d.api_test_http);
                            add(ok ? 'ok' : 'fail', '<b>API Body:</b> ' + (d.api_test_body || '(empty)').substring(0, 300));
                        }
                        if (d.last_error && d.last_error !== '(none)') {
                            add('fail', '<b>Last Error:</b> ' + JSON.stringify(d.last_error));
                        }
                    } else {
                        add('fail', 'Failed: ' + JSON.stringify(resp));
                    }
                }).fail(function(xhr, st) {
                    btn.prop('disabled', false).text('Diagnose');
                    add('fail', 'Request failed: ' + st + ' ' + (xhr.responseText||'').substring(0,200));
                });
            });

            /* RUN BATCH */
            $run.on('click', function(e) {
                e.preventDefault();
                if (!confirm('Run PE Batch now?')) return;
                stopped = false;
                $run.prop('disabled', true).text('Starting...');
                $stop.prop('disabled', false).css('opacity','1');
                $('#dba-pe-progress').slideDown();
                pb(2, '#28a745'); lbl('Initializing...'); dtl('');
                $R.empty().show();
                B = {created:0, failed:0, target:0};

                $.post(A, {action:'dba_pe_init_batch', nonce:N}, function(resp) {
                    if (stopped) { stopUI(); return; }
                    if (!resp.success) { errUI(resp.data ? resp.data.message : JSON.stringify(resp)); return; }
                    B.target = resp.data.posts_target || 1;
                    add('info', 'v5.1 (4-step+dedupe14d) | Target: ' + B.target + ' posts (' + (resp.data.day||'?') + '), existing: ' + (resp.data.existing||0));
                    pb(5); gen(1);
                }).fail(function(xhr, st) { errUI('Init failed: ' + st + ' ' + (xhr.responseText||'').substring(0,200)); });
            });

            $stop.on('click', function(e) {
                e.preventDefault(); stopped = true;
                $(this).prop('disabled', true).text('Stopping...');
                lbl('Stopping...');
            });

            /* GENERATE ONE POST = background processing to avoid Cloudflare 100s timeout */
            function gen(idx) {
                if (stopped) { stopUI(); return; }
                if (idx > B.target) { doRoundup(); return; }
                pb(Math.round(5 + (idx/(B.target+1))*85));
                lbl('Post ' + idx + '/' + B.target + ' [1/4] researching...');

                /* STEP 1: Research (~20s) */
                $.ajax({
                    url: A, type: 'POST', timeout: 55000,
                    data: {action:'dba_pe_step_research', nonce:N, post_index:idx},
                    success: function(r1) {
                        if (!r1||!r1.success||!r1.data||r1.data.status==='failed') {
                            B.failed++;
                            add('fail','Post '+idx+' research: '+(r1&&r1.data?(r1.data.error||JSON.stringify(r1.data)):'empty'));
                            nxt(idx); return;
                        }
                        add('info','Post '+idx+': '+esc(r1.data.company_name||'')+' in '+esc(r1.data.geo||'')+' — '+esc(r1.data.headline||''));
                        doWrite(idx);
                    },
                    error: function(x,s) { B.failed++; add('fail','Post '+idx+' research CRASHED: '+s+' HTTP='+x.status); nxt(idx); }
                });
            }

            /* STEP 2: Spawn background article generation, then poll */
            function doWrite(idx) {
                lbl('Post '+idx+'/'+B.target+' [2/4] writing article...');
                $.ajax({
                    url: A, type: 'POST', timeout: 15000,
                    data: {action:'dba_pe_step_write', nonce:N, post_index:idx},
                    success: function(r2) {
                        if (!r2||!r2.success||!r2.data||r2.data.status==='failed') {
                            B.failed++;
                            add('fail','Post '+idx+' write: '+(r2&&r2.data?(r2.data.error||JSON.stringify(r2.data)):'empty'));
                            nxt(idx); return;
                        }
                        /* Poll for background result every 5s */
                        var pc = 0;
                        var pt = setInterval(function() {
                            pc++;
                            lbl('Post '+idx+'/'+B.target+' [2/4] writing article ('+pc*5+'s)...');
                            if (pc > 30) { clearInterval(pt); B.failed++; add('fail','Post '+idx+' write: timed out after 150s'); nxt(idx); return; }
                            $.post(A, {action:'dba_pe_step_write_poll', nonce:N, post_index:idx}, function(pr) {
                                if (!pr||!pr.success||!pr.data) return;
                                var st = pr.data.status;
                                if (st === 'working') return;
                                clearInterval(pt);
                                if (st === 'done') {
                                    add('info','Post '+idx+' article: '+esc(pr.data.title||'OK'));
                                    doSave(idx);
                                } else {
                                    B.failed++;
                                    add('fail','Post '+idx+' write: '+(pr.data.error||'unknown'));
                                    nxt(idx);
                                }
                            });
                        }, 5000);
                    },
                    error: function(x,s) { B.failed++; add('fail','Post '+idx+' write spawn failed: '+s); nxt(idx); }
                });
            }

            /* STEP 3: Validate + save post (~15s) */
            function doSave(idx) {
                lbl('Post '+idx+'/'+B.target+' [3/4] saving post...');
                $.ajax({
                    url: A, type: 'POST', timeout: 55000,
                    data: {action:'dba_pe_step_save', nonce:N, post_index:idx},
                    success: function(r3) {
                        if (r3&&r3.success&&r3.data&&r3.data.post_id&&r3.data.status!=='failed') {
                            B.created++;
                            var d=r3.data;
                            var lnk=d.permalink?'<a href="'+d.permalink+'" target="_blank" style="color:#0073aa;font-weight:bold;">'+esc(d.title||'View')+'</a>':esc(d.title||'OK');
                            add('ok','Post '+idx+': '+lnk+' <span style="color:#888;">('+esc(d.geo||'')+'/'+esc(d.company||'')+')</span>');
                            doImage(idx, d.post_id);
                        } else {
                            B.failed++;
                            add('fail','Post '+idx+' save: '+(r3&&r3.data?(r3.data.error||JSON.stringify(r3.data)):'empty'));
                            nxt(idx);
                        }
                    },
                    error: function(x,s) { B.failed++; add('fail','Post '+idx+' save CRASHED: '+s+' HTTP='+x.status); nxt(idx); }
                });
            }

            /* STEP 4: DALL-E image (~60s) */
            function doImage(idx, postId) {
                lbl('Post '+idx+'/'+B.target+' [4/4] generating image...');
                $.ajax({
                    url: A, type: 'POST', timeout: 90000,
                    data: {action:'dba_pe_gen_image', nonce:N, post_id:postId},
                    success: function(ir) {
                        if (ir&&ir.success&&ir.data&&ir.data.status==='ok') add('ok','Post '+idx+' image: #'+ir.data.media_id);
                        else add('info','Post '+idx+' image: '+(ir&&ir.data?(ir.data.reason||ir.data.error||'skipped'):'skip'));
                        nxt(idx);
                    },
                    error: function(x,s) { add('info','Post '+idx+' image timeout (post OK)'); nxt(idx); }
                });
            }

            function nxt(idx) { if(stopped){stopUI();return;} setTimeout(function(){gen(idx+1);},2000); }

            /* ROUNDUP */
            function doRoundup() {
                if (stopped) { stopUI(); return; }
                pb(92); lbl('Creating roundup...');
                if (B.created === 0) { doneUI('0 created, '+B.failed+' failed. No roundup.'); return; }
                $.post(A, {action:'dba_pe_finish_roundup', nonce:N}, function(resp) {
                    if (resp.success && resp.data && resp.data.roundup_id) {
                        add('ok', 'Roundup: <a href="/?p='+resp.data.roundup_id+'" target="_blank">Post #'+resp.data.roundup_id+'</a>');
                    }
                    doneUI('Created: '+B.created+' | Failed: '+B.failed);
                }).fail(function(x,s) { doneUI('Created: '+B.created+' | Failed: '+B.failed+' | Roundup: '+s); });
            }

            /* HELPERS */
            function add(t, h) {
                var c={ok:'#28a745',fail:'#dc3545',info:'#6c757d'}, i={ok:'OK',fail:'FAIL',info:'INFO'};
                $R.append('<div style="padding:6px 0;border-bottom:1px solid #eee;font-size:13px;line-height:1.6;word-break:break-all;"><span style="color:'+(c[t]||'#333')+';font-weight:bold;">['+( i[t]||'')+']</span> '+h+'</div>');
            }
            function pb(p,c) { var s={'width':p+'%'}; if(c)s.background=c; $('#dba-pe-progress-bar').css(s); }
            function lbl(t) { $('#dba-pe-progress-label').text(t); }
            function dtl(t) { $('#dba-pe-progress-detail').text(t); }
            function esc(s) { return $('<span>').text(s).html(); }
            function rstBtns() { $run.prop('disabled',false).text('Run PE Batch Now'); $stop.prop('disabled',true).css('opacity','0.5').text('Stop Batch'); }
            function stopUI() { if(polling)clearInterval(polling); rstBtns(); pb(100,'#f0ad4e'); lbl('Stopped'); dtl('Created:'+B.created+' Failed:'+B.failed); }
            function doneUI(s) { if(polling)clearInterval(polling); rstBtns(); pb(100); lbl('Batch complete!'); dtl(s); }
            function errUI(m) { if(polling)clearInterval(polling); rstBtns(); add('fail',m); lbl('Error'); pb(100,'#dc3545'); }

        })(jQuery);
        </script>
        <?php
    }
}
