<?php
/**
 * LuxStrat AI Content Generator
 *
 * Generates long-form, SEO-optimised Luxembourg structuring articles
 * using the OpenAI API directly (wp_remote_post).
 *
 * Produces 1 post every Sunday, rotating through active themes.
 *
 * @package DamalionBlogAI
 * @subpackage LuxStrat
 */

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

class DBA_LuxStrat_Generator {

    private static $instance = null;

    /** Meta prefix for all LuxStrat post meta */
    const META_PREFIX = '_dba_luxstrat_';

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

    /**
     * Main entry point — called by the scheduler every Sunday.
     * Generates exactly 1 post for the next theme in rotation.
     *
     * @param  bool $force  Skip daily/weekly dedup guards (for manual runs).
     * @return array|WP_Error Result summary or error.
     */
    public function run( $force = false ) {
        try {
            $settings = get_option( 'dba_luxstrat_settings', array() );

            if ( empty( $settings['enabled'] ) ) {
                error_log( 'LuxStrat: Module disabled. Skipping.' );
                return new WP_Error( 'disabled', 'LuxStrat module is disabled.' );
            }

            // ── Guard: only 1 post per day (skipped on manual force) ──
            if ( ! $force && DBA_LuxStrat_Dedupe::posted_today() ) {
                error_log( 'LuxStrat: Already posted today. Skipping.' );
                return new WP_Error( 'already_posted', 'LuxStrat already posted today.' );
            }

            // ── Pick next theme in rotation ──
            $theme_slug = $this->get_next_theme( $force );
            if ( ! $theme_slug ) {
                error_log( 'LuxStrat: No available theme for this week.' );
                return new WP_Error( 'no_theme', 'No theme available for generation.' );
            }

            $theme_config = DBA_LuxStrat_Registry::get_theme( $theme_slug );
            if ( ! $theme_config ) {
                error_log( 'LuxStrat: Invalid theme slug: ' . $theme_slug );
                return new WP_Error( 'invalid_theme', 'Theme not found in registry.' );
            }

            error_log( 'LuxStrat: Generating post for theme "' . $theme_slug . '" (' . $theme_config['short_label'] . ')' );

            // ── Generate content via OpenAI ──
            $result = $this->generate_article( $theme_slug, $theme_config, $settings );
            if ( is_wp_error( $result ) ) {
                error_log( 'LuxStrat: Generation failed — ' . $result->get_error_message() );
                return $result;
            }

            // ── Validate ──
            $validator  = DBA_LuxStrat_Validator::get_instance();
            $validation = $validator->validate( $result );

            if ( ! $validation['valid'] ) {
                $error_msg = 'LuxStrat validation failed: ' . implode( ' | ', $validation['errors'] );
                error_log( $error_msg );

                // Retry once with feedback
                $result = $this->generate_article( $theme_slug, $theme_config, $settings, $validation['errors'] );
                if ( is_wp_error( $result ) ) {
                    return $result;
                }
                $validation = $validator->validate( $result );
                if ( ! $validation['valid'] ) {
                    error_log( 'LuxStrat: Retry also failed validation: ' . implode( ' | ', $validation['errors'] ) );
                    return new WP_Error( 'validation_failed', implode( ' | ', $validation['errors'] ) );
                }
            }

            if ( ! empty( $validation['warnings'] ) ) {
                error_log( 'LuxStrat warnings: ' . implode( ' | ', $validation['warnings'] ) );
            }

            // ── Writing style validation (passive voice, sentence length, transitions) ──
            $style_errors = $this->validate_writing_style( $result );
            if ( ! empty( $style_errors ) ) {
                error_log( 'LuxStrat: Style check failed — ' . implode( ' | ', $style_errors ) );

                // Retry once with style feedback
                $result = $this->generate_article( $theme_slug, $theme_config, $settings, $style_errors );
                if ( is_wp_error( $result ) ) {
                    return $result;
                }
                // Re-run main validator on retry
                $validation = $validator->validate( $result );
                if ( ! $validation['valid'] ) {
                    error_log( 'LuxStrat: Style retry failed main validation: ' . implode( ' | ', $validation['errors'] ) );
                    return new WP_Error( 'validation_failed', implode( ' | ', $validation['errors'] ) );
                }
                // Log style metrics on retry (non-blocking — accept even if still imperfect)
                $retry_style = $this->validate_writing_style( $result );
                if ( ! empty( $retry_style ) ) {
                    error_log( 'LuxStrat: Style retry still has issues (accepted): ' . implode( ' | ', $retry_style ) );
                }
            }

            // ── Create WordPress post ──
            $post_id = $this->create_post( $result, $theme_slug, $theme_config, $settings );
            if ( is_wp_error( $post_id ) ) {
                return $post_id;
            }

            // ── Generate featured image ──
            $this->generate_featured_image( $post_id, $result['title'], $theme_config, $settings );

            // ── KAIZEN: store original content for diff tracking ──
            if ( class_exists( 'DBA_LuxStrat_Kaizen' ) ) {
                DBA_LuxStrat_Kaizen::store_original( $post_id, $result['title'], $result['content'], $result['excerpt'] ?? '' );
            }

            // ── ANTI-ORPHAN: authority links + related block + backlinks ──
            $ao_result = array();
            if ( class_exists( 'DBA_LuxStrat_Anti_Orphan' ) ) {
                $ao_result = DBA_LuxStrat_Anti_Orphan::on_post_created( $post_id, $theme_slug );
                error_log( 'LuxStrat Anti-Orphan: auth=' . ($ao_result['auth_links']??0) . ' related=' . ($ao_result['related']??0) . ' backlinks=' . ($ao_result['backlinks']??0) );
            }

            // ── Mark as used ──
            DBA_LuxStrat_Dedupe::mark_used_today( $theme_slug );

            // ── Update rotation pointer ──
            $this->advance_rotation( $theme_slug );

            error_log( 'LuxStrat: Successfully created post #' . $post_id . ' — "' . $result['title'] . '"' );

            // ── TIS: track directive progress ──
            if ( class_exists( 'DBA_TIS_Recommender' ) ) {
                DBA_TIS_Recommender::auto_track_progress( 'luxstrat', $result['title'] );
            }

            return array(
                'post_id'    => $post_id,
                'title'      => $result['title'],
                'theme'      => $theme_slug,
                'words'      => str_word_count( strip_tags( $result['content'] ) ),
                'kaizen'     => class_exists( 'DBA_LuxStrat_Kaizen' ) ? 'stored' : 'off',
                'anti_orphan'=> $ao_result,
            );

        } catch ( \Throwable $e ) {
            $msg = 'LuxStrat fatal error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
            error_log( $msg );
            return new WP_Error( 'fatal_error', $msg );
        }
    }

    /**
     * Generate article content via OpenAI API (direct call).
     *
     * @param  string     $theme_slug   Theme key.
     * @param  array      $theme_config Theme configuration from registry.
     * @param  array      $settings     Plugin settings.
     * @param  array      $feedback     Optional validation feedback for retry.
     * @return array|WP_Error           { title, content, excerpt, meta_description, focus_keyword }
     */
    private function generate_article( $theme_slug, $theme_config, $settings, $feedback = array() ) {

        $dba_settings = get_option( 'dba_settings', array() );
        $api_key      = ! empty( $dba_settings['openai_api_key'] ) ? $dba_settings['openai_api_key'] : '';

        if ( empty( $api_key ) ) {
            return new WP_Error( 'no_api_key', 'OpenAI API key not configured. Go to Damalion Blog AI → Settings to add it.' );
        }

        $model       = ! empty( $dba_settings['openai_model'] ) ? $dba_settings['openai_model'] : 'gpt-4o-mini';
        $word_min    = ! empty( $settings['word_count_min'] ) ? (int) $settings['word_count_min'] : 1000;
        $word_max    = ! empty( $settings['word_count_max'] ) ? (int) $settings['word_count_max'] : 1800;
        $cta_text    = ! empty( $settings['cta_text'] ) ? $settings['cta_text'] : '';
        $cta_link    = ! empty( $settings['cta_link_url'] ) ? $settings['cta_link_url'] : 'https://www.damalion.com/lets-connect/';
        $cta_anchor  = ! empty( $settings['cta_link_text'] ) ? $settings['cta_link_text'] : 'Contact your Damalion experts now.';

        // Get recent titles to avoid repetition
        $recent_titles = $this->get_recent_titles( $theme_slug, 20 );
        $avoid_titles  = ! empty( $recent_titles ) ? "\n\nDO NOT reuse or closely paraphrase any of these recent titles:\n- " . implode( "\n- ", $recent_titles ) : '';

        // Build pillar link instructions
        $pillar_links = '';
        if ( ! empty( $theme_config['pillar_urls'] ) ) {
            $pillar_links = "\n\nInclude at least one natural internal link to the pillar page: " . implode( ', ', $theme_config['pillar_urls'] );
        }

        // ── Link Sanitizer: approved internal links for prompt ──
        $post_lang = ! empty( $settings['wpml_language'] ) ? strtolower( substr( $settings['wpml_language'], 0, 2 ) ) : 'en';
        $approved_links_block = '';
        if ( class_exists( 'DBA_Link_Sanitizer' ) ) {
            $approved_links_block = DBA_Link_Sanitizer::get_prompt_block( $post_lang );
        }

        // Build H2 heading blueprint
        $h2_instructions = '';
        if ( ! empty( $theme_config['h2_blueprint'] ) ) {
            $h2_instructions = "\n\nH2 HEADING STRUCTURE (use these as your H2s, in this priority order — you may rephrase slightly for variety but keep the same topic and keyword in each H2):\n";
            foreach ( $theme_config['h2_blueprint'] as $i => $h2 ) {
                $h2_instructions .= ( $i + 1 ) . '. ' . $h2 . "\n";
            }
            $h2_instructions .= "Use H3 sub-headings under each H2 as needed. The first H2 MUST contain the primary keyword.";
        }

        // Feedback from failed validation (retry)
        $feedback_block = '';
        if ( ! empty( $feedback ) ) {
            $feedback_block = "\n\n⚠️ IMPORTANT — Your previous attempt failed validation. Fix these issues:\n- " . implode( "\n- ", $feedback );
        }

        $system_prompt = 'You are a senior Luxembourg corporate law and fund structuring specialist writing for Damalion, a professional services firm. '
            . 'You produce high-quality, technically precise, SEO-optimised articles for institutional investors, fund managers, family offices, and cross-border structuring professionals. '
            . 'Your writing is authoritative, factual, and avoids generic AI-style phrasing. '
            . 'You never use filler phrases like "in conclusion", "it is important to note", "let\'s dive in", "navigating the complex". '
            . 'You write in British English. You cite specific Luxembourg legal frameworks (e.g. Law of 10 August 1915, Law of 23 July 2016, CSSF circulars) where relevant. '
            . "\n\n"
            . 'STRICT WRITING STYLE RULES — apply to every sentence you write:' . "\n\n"
            . '1. ACTIVE VOICE (max 10% passive):' . "\n"
            . '   - Always make the actor the subject.' . "\n"
            . '   - WRONG: "The RAIF is managed by an authorised AIFM." → RIGHT: "An authorised AIFM manages the RAIF."' . "\n"
            . '   - WRONG: "Regulatory oversight is exercised indirectly." → RIGHT: "The AIFM exercises regulatory oversight indirectly."' . "\n"
            . '   - WRONG: "RAIFs are not subject to corporate income tax." → RIGHT: "RAIFs do not pay corporate income tax."' . "\n"
            . '   - WRONG: "This ring-fencing is enshrined in Article 50." → RIGHT: "Article 50 enshrines this ring-fencing."' . "\n"
            . '   - Only use passive for legal definitions where the agent is genuinely unknown.' . "\n"
            . '   - Self-check: if a sentence has "is/are/was/were/been + past participle", rewrite it.' . "\n\n"
            . '2. SHORT SENTENCES (max 25% over 20 words):' . "\n"
            . '   - Target 12–18 words per sentence. Never exceed 25 words.' . "\n"
            . '   - Split any sentence with a semicolon, colon-clause, or multiple comma-clauses into two sentences.' . "\n"
            . '   - WRONG (35 words): "The RAIF regime was established to meet the needs of institutional, professional, and well-informed investors seeking a streamlined, regulated alternative fund structure without direct supervision by the CSSF."' . "\n"
            . '   - RIGHT (14+10): "The RAIF regime serves institutional, professional, and well-informed investors. It offers a streamlined fund structure without direct CSSF supervision."' . "\n"
            . '   - One idea per sentence. Two facts = two sentences.' . "\n\n"
            . '3. TRANSITION WORDS (at least 30% of sentences):' . "\n"
            . '   - Start or connect sentences with transitions: However, Therefore, In addition, As a result, Furthermore, Moreover, Consequently, For example, In contrast, Meanwhile, Specifically, Accordingly, Nevertheless, Instead, Similarly, In particular, For this reason, By contrast, On the other hand, As such, In practice, Notably, In turn, Finally, Additionally.' . "\n"
            . '   - Use them naturally — not forced — but ensure at least 30% of sentences contain a transition word or phrase.' . "\n"
            . '   - WRONG: "The RAIF does not require CSSF approval. It can launch within weeks."' . "\n"
            . '   - RIGHT: "The RAIF does not require CSSF approval. As a result, it can launch within weeks."' . "\n\n"
            . '4. SELF-CHECK BEFORE OUTPUT: Scan every sentence. Rewrite until passive voice is under 10%, long sentences are under 25%, and transition words appear in at least 30% of sentences.' . "\n\n"
            . 'Output ONLY valid JSON — no markdown fences, no preamble, no commentary.';

        $user_prompt = <<<PROMPT
Write a professional article about: {$theme_config['label']}

Topic description: {$theme_config['description']}

Target keywords (use naturally, do not stuff): {$this->format_keywords($theme_config['keywords'])}

Requirements:
- Word count: MINIMUM {$word_min} words, target {$word_max} words. AIM FOR AT LEAST {$word_min} words — articles under {$word_min} words will be REJECTED. Count carefully before submitting.
- Structure: Use H2 and H3 subheadings following the blueprint below. At least 4 H2 sections. First H2 must contain the primary keyword.
- Tone: Authoritative, professional, no fluff
- Audience: Institutional investors, fund managers, tax advisors, family offices
- Include practical structuring insights, not just theory
- Include at least one relevant Luxembourg legal reference (law, regulation, or CSSF circular)
- ACTIVE VOICE: At least 90% of sentences must use active voice. Make the actor the subject. Write "The AIFM manages the RAIF" not "The RAIF is managed by the AIFM". Only use passive for legal definitions where the agent is unknown.
- SHORT SENTENCES: At least 75% of sentences must be under 20 words. Target 12–18 words. Never exceed 25 words. Split compound sentences into two. One idea per sentence.
- TRANSITION WORDS: At least 30% of sentences must contain a transition word or phrase (e.g. However, Therefore, In addition, As a result, Furthermore, Moreover, For example, Consequently, In contrast, Specifically, Accordingly, Instead, Similarly, In particular, For this reason, Meanwhile, Nevertheless, Additionally, Notably, As such, In turn). Place them naturally at the start or middle of sentences.
- Before finalising output: scan every sentence. If passive voice exceeds 10%, long sentences exceed 25%, or transition words fall below 30%, rewrite until all three thresholds are met.
- End with a brief call-to-action paragraph: "{$cta_text} <a href=\"{$cta_link}\" target=\"_blank\" rel=\"noopener\">{$cta_anchor}</a>"
{$h2_instructions}
{$pillar_links}
{$approved_links_block}
{$avoid_titles}
{$feedback_block}

Return ONLY a JSON object with these keys:
{
  "title": "SEO-optimised article title (50-80 chars)",
  "content": "Full HTML article body with <h2>, <h3>, <p>, <ul>/<li>, <a> tags. No <h1>.",
  "excerpt": "Compelling excerpt (120-135 chars STRICTLY — never exceed 135 characters)",
  "meta_description": "SEO meta description (120-135 chars STRICTLY — Google truncates beyond this, NEVER exceed 135 characters, count carefully)",
  "focus_keyword": "Primary 2-4 word keyword phrase",
  "image_alt": "SEO-optimised image alt text (60-100 chars). Include the primary keyword naturally. Describe a conceptual image related to the article topic. No brand names, no 'image of', no 'photo of'."
}
PROMPT;

        // ── SEO Sniper: inject competitive intelligence ──
        // Priority: live transient (Sniper-triggered) → stored analysis (every cron run)
        if ( class_exists( 'DBA_SEO_Sniper_Router' ) ) {
            $sniper_brief = DBA_SEO_Sniper_Router::get_brief_for_prompt( $theme_slug, 'luxstrat' );
            if ( $sniper_brief ) {
                $user_prompt .= "\n\n" . $sniper_brief;
            }
        }

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

        // ── KAIZEN: inject learning context from editor corrections ──
        if ( class_exists( 'DBA_LuxStrat_Kaizen' ) ) {
            $kaizen_ctx = DBA_LuxStrat_Kaizen::get_learning_context( $theme_slug );
            if ( $kaizen_ctx ) {
                $system_prompt .= $kaizen_ctx;
            }
        }

        // ── Direct OpenAI API call ──
        $body = array(
            'model'       => $model,
            'messages'    => array(
                array( 'role' => 'system', 'content' => $system_prompt ),
                array( 'role' => 'user',   'content' => $user_prompt ),
            ),
            'temperature' => 0.7,
            'max_tokens'  => 8192,
        );

        error_log( 'LuxStrat: Calling OpenAI API with model "' . $model . '"' );

        $api_response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', array(
            'timeout' => 120,
            'headers' => array(
                'Content-Type'  => 'application/json',
                'Authorization' => 'Bearer ' . $api_key,
            ),
            'body'    => wp_json_encode( $body ),
        ) );

        if ( is_wp_error( $api_response ) ) {
            return new WP_Error( 'api_error', 'OpenAI API request failed: ' . $api_response->get_error_message() );
        }

        $http_code = wp_remote_retrieve_response_code( $api_response );
        $raw_body  = wp_remote_retrieve_body( $api_response );

        if ( $http_code !== 200 ) {
            $error_data = json_decode( $raw_body, true );
            $error_msg  = isset( $error_data['error']['message'] ) ? $error_data['error']['message'] : $raw_body;
            return new WP_Error( 'api_http_error', 'OpenAI API returned HTTP ' . $http_code . ': ' . $error_msg );
        }

        $response_data = json_decode( $raw_body, true );
        if ( empty( $response_data['choices'][0]['message']['content'] ) ) {
            return new WP_Error( 'empty_response', 'OpenAI returned an empty response. Raw: ' . substr( $raw_body, 0, 500 ) );
        }

        $response = $response_data['choices'][0]['message']['content'];

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        // Parse JSON response
        $parsed = $this->parse_json_response( $response );
        if ( is_wp_error( $parsed ) ) {
            return $parsed;
        }

        // Ensure required fields
        $required = array( 'title', 'content' );
        foreach ( $required as $field ) {
            if ( empty( $parsed[ $field ] ) ) {
                return new WP_Error( 'missing_field', 'AI response missing required field: ' . $field );
            }
        }

        // Add theme to parsed data for validation
        $parsed['theme'] = $theme_slug;

        // ── Link Sanitizer: strip/replace hallucinated damalion.com links ──
        if ( class_exists( 'DBA_Link_Sanitizer' ) && ! empty( $parsed['content'] ) ) {
            $parsed['content'] = DBA_Link_Sanitizer::sanitize( $parsed['content'], $post_lang );
        }

        return $parsed;
    }

    /**
     * Create the WordPress post with all meta.
     *
     * @param  array  $data         Parsed AI response.
     * @param  string $theme_slug   Theme key.
     * @param  array  $theme_config Theme config.
     * @param  array  $settings     Plugin settings.
     * @return int|WP_Error         Post ID or error.
     */
    private function create_post( $data, $theme_slug, $theme_config, $settings ) {
        $post_status = ! empty( $settings['post_status'] ) ? $settings['post_status'] : 'publish';
        $author_id   = ! empty( $settings['author_id'] ) ? (int) $settings['author_id'] : 1;

        // ── Resolve max 2 categories per theme (auto-find or auto-create) ──
        $category_ids = $this->resolve_categories( $theme_config, $settings );

        $post_data = array(
            'post_title'   => sanitize_text_field( $data['title'] ),
            'post_content' => wp_kses_post( $data['content'] ),
            'post_excerpt' => ! empty( $data['excerpt'] ) ? sanitize_text_field( $data['excerpt'] ) : '',
            'post_status'  => $post_status,
            'post_author'  => $author_id,
            'post_type'    => 'post',
            'meta_input'   => array(
                self::META_PREFIX . 'auto_created'    => '1',
                self::META_PREFIX . 'theme'           => $theme_slug,
                self::META_PREFIX . 'theme_label'     => $theme_config['short_label'],
                self::META_PREFIX . 'generated_at'    => current_time( 'mysql' ),
                self::META_PREFIX . 'word_count'      => str_word_count( strip_tags( $data['content'] ) ),
                self::META_PREFIX . 'model'           => $this->get_openai_model(),
            ),
        );

        // Yoast / RankMath meta — STRICTLY 135 chars max
        if ( ! empty( $data['meta_description'] ) ) {
            $meta_desc = sanitize_text_field( $data['meta_description'] );
            // Hard enforcement: truncate to 135 characters if AI exceeded limit
            if ( mb_strlen( $meta_desc ) > 135 ) {
                $meta_desc = mb_substr( $meta_desc, 0, 132 ) . '...';
            }
            $post_data['meta_input']['_yoast_wpseo_metadesc'] = $meta_desc;
            $post_data['meta_input']['rank_math_description']  = $meta_desc;
        }
        if ( ! empty( $data['focus_keyword'] ) ) {
            $post_data['meta_input']['_yoast_wpseo_focuskw'] = sanitize_text_field( $data['focus_keyword'] );
            $post_data['meta_input']['rank_math_focus_keyword'] = sanitize_text_field( $data['focus_keyword'] );
        }
        // Store AI-generated image alt text for featured image
        if ( ! empty( $data['image_alt'] ) ) {
            $post_data['meta_input'][ self::META_PREFIX . 'image_alt' ] = sanitize_text_field( $data['image_alt'] );
        }

        // Assign resolved categories (max 2)
        if ( ! empty( $category_ids ) ) {
            $post_data['post_category'] = $category_ids;
        }

        $post_id = wp_insert_post( $post_data, true );

        if ( is_wp_error( $post_id ) ) {
            return $post_id;
        }

        // Apply auto-linker if available
        if ( class_exists( 'DBA_Auto_Linker' ) ) {
            try {
                $linker  = DBA_Auto_Linker::get_instance();
                $linked  = $linker->process( $data['content'] );
                if ( $linked !== $data['content'] ) {
                    wp_update_post( array(
                        'ID'           => $post_id,
                        'post_content' => $linked,
                    ) );
                }
            } catch ( \Throwable $e ) {
                error_log( 'LuxStrat: Auto-linker error for post #' . $post_id . ': ' . $e->getMessage() );
            }
        }

        // WPML: assign language if configured
        if ( ! empty( $settings['wpml_language'] ) && function_exists( 'wpml_set_element_language_details' ) ) {
            $this->set_wpml_language( $post_id, $settings['wpml_language'] );
        }

        return $post_id;
    }

    /**
     * Generate and attach a featured image.
     *
     * @param int    $post_id      Post ID.
     * @param string $title        Post title (used as image prompt base).
     * @param array  $theme_config Theme config.
     * @param array  $settings     Plugin settings.
     */
    /**
     * Resolve categories for a theme — find existing or create new.
     * Returns max 2 category IDs, prioritised by the theme config order.
     *
     * Logic:
     * 1. Check theme_config['categories'] array (ordered by priority)
     * 2. For each name: find existing WP category (case-insensitive)
     * 3. If not found: auto-create it
     * 4. Return max 2 IDs
     * 5. Fallback: settings['category_id'] if no theme categories defined
     *
     * @param  array $theme_config Theme configuration from registry.
     * @param  array $settings     Plugin settings.
     * @return array               Array of category IDs (max 2).
     */
    private function resolve_categories( $theme_config, $settings ) {
        $max_categories = 2;

        // ── Detect post language (WPML) ──
        $lang = 'en'; // default
        if ( ! empty( $settings['wpml_language'] ) ) {
            $lang = strtolower( substr( $settings['wpml_language'], 0, 2 ) );
        } elseif ( defined( 'ICL_LANGUAGE_CODE' ) ) {
            $lang = ICL_LANGUAGE_CODE;
        }

        // ── Pick the right slug set for the language ──
        $slug_key = ( $lang === 'fr' ) ? 'category_slugs_fr' : 'category_slugs_en';

        if ( empty( $theme_config[ $slug_key ] ) || ! is_array( $theme_config[ $slug_key ] ) ) {
            // Fallback to global setting
            $fallback = ! empty( $settings['category_id'] ) ? (int) $settings['category_id'] : 0;
            return $fallback > 0 ? array( $fallback ) : array();
        }

        $resolved = array();

        foreach ( $theme_config[ $slug_key ] as $cat_slug ) {
            if ( count( $resolved ) >= $max_categories ) {
                break;
            }

            $cat_slug = trim( $cat_slug );
            if ( empty( $cat_slug ) ) {
                continue;
            }

            // Look up existing category by slug — NEVER create new ones
            $term = get_term_by( 'slug', $cat_slug, 'category' );

            if ( $term && ! is_wp_error( $term ) ) {
                $resolved[] = (int) $term->term_id;
            } else {
                error_log( 'LuxStrat: Category slug "' . $cat_slug . '" not found (' . $lang . '). Skipping.' );
            }
        }

        // Deduplicate
        $resolved = array_unique( $resolved );

        // Final fallback
        if ( empty( $resolved ) ) {
            $fallback = ! empty( $settings['category_id'] ) ? (int) $settings['category_id'] : 0;
            return $fallback > 0 ? array( $fallback ) : array();
        }

        return array_slice( $resolved, 0, $max_categories );
    }

    private function generate_featured_image( $post_id, $title, $theme_config, $settings ) {
        $media_id = null;

        // Build DALL-E prompt
        $dalle_prompt = 'Professional, clean corporate illustration for a Luxembourg financial services article about '
            . $theme_config['short_label'] . '. '
            . 'Theme: ' . $title . '. '
            . 'Style: Modern, minimalist, corporate blue tones, no text, no faces, no logos. '
            . 'Suitable for a legal/financial advisory website header image.';

        $dba_settings = get_option( 'dba_settings', array() );
        $image_size   = ! empty( $dba_settings['image_size'] ) ? $dba_settings['image_size'] : '1792x1024';

        try {
            // ── Method A: Use main plugin's DBA_Image_Generator if available ──
            if ( class_exists( 'DBA_Image_Generator' ) ) {
                $image_gen = DBA_Image_Generator::get_instance();
                $media_id  = $image_gen->generate_and_attach( $dalle_prompt, $post_id, $title, $image_size );

                if ( is_wp_error( $media_id ) ) {
                    error_log( 'LuxStrat: DBA_Image_Generator failed: ' . $media_id->get_error_message() . '. Trying direct DALL-E.' );
                    $media_id = null;
                }
            }

            // ── Method B: Direct DALL-E API call (fallback) ──
            if ( ! $media_id ) {
                $media_id = $this->generate_image_direct( $dalle_prompt, $post_id, $title, $image_size, $dba_settings );
            }

            if ( ! $media_id || is_wp_error( $media_id ) ) {
                error_log( 'LuxStrat: All image generation methods failed for post #' . $post_id );
                return;
            }

            // ── Set as featured image ──
            set_post_thumbnail( $post_id, $media_id );
            update_post_meta( $post_id, self::META_PREFIX . 'media_id', $media_id );

            // ── SEO: Set alt text on the attachment ──
            $alt_text = get_post_meta( $post_id, self::META_PREFIX . 'image_alt', true );
            if ( empty( $alt_text ) ) {
                // Fallback: build alt from theme keywords + title
                $keywords = ! empty( $theme_config['keywords'] ) ? $theme_config['keywords'] : array();
                $primary  = ! empty( $keywords[0] ) ? $keywords[0] : $theme_config['short_label'];
                $alt_text = $primary . ' — ' . $title;
                // Cap at 125 chars
                if ( mb_strlen( $alt_text ) > 125 ) {
                    $alt_text = mb_substr( $alt_text, 0, 122 ) . '...';
                }
            }
            update_post_meta( $media_id, '_wp_attachment_image_alt', sanitize_text_field( $alt_text ) );

            // ── SEO: Set attachment title (used in some themes as title attr) ──
            wp_update_post( array(
                'ID'         => $media_id,
                'post_title' => sanitize_text_field( $title ),
            ) );

            error_log( 'LuxStrat: Featured image #' . $media_id . ' attached to post #' . $post_id . ' with alt: ' . $alt_text );

        } catch ( \Throwable $e ) {
            error_log( 'LuxStrat: Image generation failed for post #' . $post_id . ': ' . $e->getMessage() );
        }
    }

    /**
     * Direct DALL-E API call — fallback when DBA_Image_Generator is unavailable.
     *
     * @param  string $prompt      Image generation prompt.
     * @param  int    $post_id     Parent post ID.
     * @param  string $title       Image title.
     * @param  string $size        Image dimensions (e.g. '1792x1024').
     * @param  array  $dba_settings Main DBA settings.
     * @return int|WP_Error|null   Attachment ID or error.
     */
    private function generate_image_direct( $prompt, $post_id, $title, $size, $dba_settings ) {
        $api_key = ! empty( $dba_settings['openai_api_key'] ) ? $dba_settings['openai_api_key'] : '';

        if ( empty( $api_key ) ) {
            error_log( 'LuxStrat: No OpenAI API key for DALL-E image generation.' );
            return null;
        }

        error_log( 'LuxStrat: Calling DALL-E API directly for post #' . $post_id );

        // ── DALL-E 3 API call ──
        $api_response = wp_remote_post( 'https://api.openai.com/v1/images/generations', array(
            'timeout' => 120,
            'headers' => array(
                'Content-Type'  => 'application/json',
                'Authorization' => 'Bearer ' . $api_key,
            ),
            'body'    => wp_json_encode( array(
                'model'           => 'dall-e-3',
                'prompt'          => $prompt,
                'n'               => 1,
                'size'            => $size,
                'quality'         => 'standard',
                'response_format' => 'url',
            ) ),
        ) );

        if ( is_wp_error( $api_response ) ) {
            error_log( 'LuxStrat: DALL-E API request failed: ' . $api_response->get_error_message() );
            return null;
        }

        $http_code = wp_remote_retrieve_response_code( $api_response );
        $raw_body  = wp_remote_retrieve_body( $api_response );

        if ( $http_code !== 200 ) {
            $error_data = json_decode( $raw_body, true );
            $error_msg  = isset( $error_data['error']['message'] ) ? $error_data['error']['message'] : substr( $raw_body, 0, 300 );
            error_log( 'LuxStrat: DALL-E API returned HTTP ' . $http_code . ': ' . $error_msg );
            return null;
        }

        $response_data = json_decode( $raw_body, true );
        if ( empty( $response_data['data'][0]['url'] ) ) {
            error_log( 'LuxStrat: DALL-E returned no image URL. Raw: ' . substr( $raw_body, 0, 300 ) );
            return null;
        }

        $image_url = $response_data['data'][0]['url'];

        // ── Download and sideload into WP Media Library ──
        if ( ! function_exists( 'media_sideload_image' ) ) {
            require_once ABSPATH . 'wp-admin/includes/media.php';
            require_once ABSPATH . 'wp-admin/includes/file.php';
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        // Generate a clean filename from the title
        $filename = sanitize_file_name( 'luxstrat-' . sanitize_title( $title ) . '.png' );

        // Download to temp file
        $tmp = download_url( $image_url );
        if ( is_wp_error( $tmp ) ) {
            error_log( 'LuxStrat: Failed to download DALL-E image: ' . $tmp->get_error_message() );
            return null;
        }

        $file_array = array(
            'name'     => $filename,
            'tmp_name' => $tmp,
        );

        // Sideload into media library, attached to the post
        $media_id = media_handle_sideload( $file_array, $post_id, $title );

        if ( is_wp_error( $media_id ) ) {
            @unlink( $tmp );
            error_log( 'LuxStrat: Media sideload failed: ' . $media_id->get_error_message() );
            return null;
        }

        error_log( 'LuxStrat: DALL-E image sideloaded as attachment #' . $media_id );
        return $media_id;
    }

    /**
     * Determine the next theme to publish (round-robin through active themes).
     *
     * @param  bool $force  Skip weekly dedup check (for manual runs).
     * @return string|false Theme slug or false if none available.
     */
    private function get_next_theme( $force = false ) {
        // ── SEO Sniper override: force a specific theme ──
        $sniper_theme = get_transient( 'dba_sniper_force_theme' );
        if ( $sniper_theme ) {
            delete_transient( 'dba_sniper_force_theme' );
            error_log( 'LuxStrat: 🎯 Sniper forced theme "' . $sniper_theme . '"' );
            return $sniper_theme;
        }

        $settings      = get_option( 'dba_luxstrat_settings', array() );
        $active_themes = ! empty( $settings['active_themes'] ) ? $settings['active_themes'] : DBA_LuxStrat_Registry::get_theme_slugs();
        $last_theme    = get_option( 'dba_luxstrat_last_theme', '' );

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

        // Find the index of the last used theme
        $last_index = array_search( $last_theme, $active_themes, true );

        // Next in rotation
        if ( $last_index === false ) {
            $next_index = 0;
        } else {
            $next_index = ( $last_index + 1 ) % count( $active_themes );
        }

        $candidate = $active_themes[ $next_index ];

        // Skip weekly dedup check on forced/manual runs
        if ( $force ) {
            return $candidate;
        }

        // Skip if already posted this week for this theme
        if ( DBA_LuxStrat_Dedupe::theme_posted_this_week( $candidate ) ) {
            // Try remaining themes
            for ( $i = 1; $i < count( $active_themes ); $i++ ) {
                $try_index = ( $next_index + $i ) % count( $active_themes );
                $try_slug  = $active_themes[ $try_index ];
                if ( ! DBA_LuxStrat_Dedupe::theme_posted_this_week( $try_slug ) ) {
                    return $try_slug;
                }
            }
            return false; // All themes already posted this week
        }

        return $candidate;
    }

    /**
     * Advance the rotation pointer.
     *
     * @param string $theme_slug Theme that was just used.
     */
    private function advance_rotation( $theme_slug ) {
        update_option( 'dba_luxstrat_last_theme', $theme_slug, false );
    }

    /**
     * Get recent LuxStrat post titles for a theme to avoid repetition.
     *
     * @param  string $theme_slug Theme key.
     * @param  int    $limit      Number of titles.
     * @return array              List of titles.
     */
    private function get_recent_titles( $theme_slug, $limit = 20 ) {
        $query = new WP_Query( array(
            'post_type'      => 'post',
            'post_status'    => array( 'publish', 'draft', 'pending', 'private' ),
            'posts_per_page' => $limit,
            'orderby'        => 'date',
            'order'          => 'DESC',
            'meta_query'     => array(
                'relation' => 'AND',
                array(
                    'key'     => self::META_PREFIX . 'auto_created',
                    'compare' => 'EXISTS',
                ),
                array(
                    'key'     => self::META_PREFIX . 'theme',
                    'value'   => $theme_slug,
                    'compare' => '=',
                ),
            ),
            'no_found_rows'          => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        ) );

        $titles = array();
        if ( $query->have_posts() ) {
            foreach ( $query->posts as $post ) {
                $titles[] = $post->post_title;
            }
        }

        return $titles;
    }

    /**
     * Get the active OpenAI model from the main DBA settings.
     *
     * @return string Model identifier.
     */
    private function get_openai_model() {
        $dba_settings = get_option( 'dba_settings', array() );
        return ! empty( $dba_settings['openai_model'] ) ? $dba_settings['openai_model'] : 'gpt-4o-mini';
    }

    /**
     * Format keywords array as a readable string.
     *
     * @param  array  $keywords
     * @return string
     */
    private function format_keywords( $keywords ) {
        if ( empty( $keywords ) || ! is_array( $keywords ) ) {
            return '';
        }
        return implode( ', ', $keywords );
    }

    /**
     * Parse a JSON response string, stripping markdown fences if present.
     *
     * @param  string $response Raw response from OpenAI.
     * @return array|WP_Error  Parsed data or error.
     */
    private function parse_json_response( $response ) {
        if ( empty( $response ) ) {
            return new WP_Error( 'empty_response', 'Empty AI response.' );
        }

        // Strip markdown code fences
        $cleaned = $response;
        $cleaned = preg_replace( '/^```(?:json)?\s*/i', '', $cleaned );
        $cleaned = preg_replace( '/\s*```$/', '', $cleaned );
        $cleaned = trim( $cleaned );

        $decoded = json_decode( $cleaned, true );
        if ( json_last_error() !== JSON_ERROR_NONE ) {
            return new WP_Error( 'json_parse_error', 'Failed to parse AI JSON: ' . json_last_error_msg() . ' — Raw: ' . substr( $response, 0, 500 ) );
        }

        return $decoded;
    }

    /**
     * Set WPML language for a post.
     *
     * @param int    $post_id
     * @param string $lang_code e.g. 'en'
     */
    private function set_wpml_language( $post_id, $lang_code ) {
        if ( ! function_exists( 'wpml_set_element_language_details' ) ) {
            return;
        }

        $set_language_args = array(
            'element_id'    => $post_id,
            'element_type'  => 'post_post',
            'trid'          => false,
            'language_code' => $lang_code,
        );

        wpml_set_element_language_details( $set_language_args );
    }

    /**
     * Validate writing style: passive voice, sentence length, transition words.
     *
     * Returns array of error strings (empty = pass).
     * Feeds into the retry loop via generate_article() $feedback param.
     *
     * @param  array $data Parsed AI response with 'content' key.
     * @return array       Error messages (empty if all checks pass).
     */
    private function validate_writing_style( $data ) {
        $errors = array();

        if ( empty( $data['content'] ) ) {
            return $errors;
        }

        $text = strip_tags( $data['content'] );

        // ── Split into sentences ──
        $sentences = preg_split( '/(?<=[.!?])\s+/', $text, -1, PREG_SPLIT_NO_EMPTY );
        $sentences = array_filter( $sentences, function( $s ) {
            return mb_strlen( trim( $s ) ) > 10;
        });
        $sentences = array_values( $sentences );
        $sentence_count = count( $sentences );

        if ( $sentence_count < 5 ) {
            return $errors;
        }

        // ── 1. Passive voice check (threshold: 10%) ──
        $passive_count = 0;
        foreach ( $sentences as $s ) {
            if ( preg_match( '/\b(is|are|was|were|been|being|be)\s+(\w+ly\s+)?((?:[a-z]+ed|built|held|made|set|run|done|given|known|seen|shown|found|kept|meant|paid|sold|told|written|chosen|driven|spoken|taken|broken|frozen|gotten|proven|worn|torn|sworn|born|borne|drawn|grown|thrown|begun|drunk|shrunk|sunk|struck|stuck)\b)/i', $s ) ) {
                $passive_count++;
            }
        }
        $passive_pct = round( ( $passive_count / $sentence_count ) * 100, 1 );

        if ( $passive_pct > 10 ) {
            $errors[] = 'Passive voice is at ' . $passive_pct . '% (' . $passive_count . '/' . $sentence_count . ' sentences). Rewrite passive sentences in active voice. Target: under 10%.';
        }

        // ── 2. Sentence length check (threshold: 25% over 20 words) ──
        $long_count = 0;
        foreach ( $sentences as $s ) {
            if ( str_word_count( trim( $s ) ) > 20 ) {
                $long_count++;
            }
        }
        $long_pct = round( ( $long_count / $sentence_count ) * 100, 1 );

        if ( $long_pct > 25 ) {
            $errors[] = 'Long sentences: ' . $long_pct . '% exceed 20 words (' . $long_count . '/' . $sentence_count . '). Split or shorten them. Target: under 25%.';
        }

        // ── 3. Transition words check (threshold: 30%) ──
        $transitions = array(
            'however', 'therefore', 'in addition', 'as a result', 'furthermore',
            'moreover', 'consequently', 'for example', 'for instance', 'in contrast',
            'meanwhile', 'specifically', 'accordingly', 'nevertheless', 'instead',
            'similarly', 'in particular', 'for this reason', 'by contrast',
            'on the other hand', 'as such', 'in practice', 'notably', 'additionally',
            'alternatively', 'equally', 'likewise', 'yet', 'still',
            'hence', 'thus', 'overall', 'importantly', 'indeed', 'above all',
            'in other words', 'that said', 'to this end', 'in turn', 'as a consequence',
            'first', 'second', 'third', 'finally', 'next', 'then', 'also',
        );
        $transition_pattern = '/\b(' . implode( '|', array_map( 'preg_quote', $transitions ) ) . ')\b/i';
        $transition_count = 0;
        foreach ( $sentences as $s ) {
            if ( preg_match( $transition_pattern, $s ) ) {
                $transition_count++;
            }
        }
        $transition_pct = round( ( $transition_count / $sentence_count ) * 100, 1 );

        if ( $transition_pct < 30 ) {
            $errors[] = 'Transition words appear in only ' . $transition_pct . '% of sentences (' . $transition_count . '/' . $sentence_count . '). Add transitions like However, Therefore, In addition, As a result, Furthermore, For example, Consequently, Moreover, In particular, Notably, As such, In turn. Target: at least 30%.';
        }

        // ── Log style metrics ──
        error_log( 'LuxStrat style check — Passive: ' . $passive_pct . '% | Long sentences: ' . $long_pct . '% | Transitions: ' . $transition_pct . '%' );

        return $errors;
    }
}
