<?php
declare(strict_types=1);
// ==================================================
// Nuclear Fancy Pants Edition™ -- NUCLEAR+++
// File: /home/ploi/automarketer-ai.com/public/wp-content/plugins/automarketer-ai/includes/class-cluster-builder.php
// Description: Content Cluster Builder -- pillar + supporting post generation, Tue/Thu/Sat scheduling, internal linking - NUCLEAR+++
// Last Edited: 2026-05-18
// ==================================================
namespace AutoMarketerAI;

defined('ABSPATH') || exit;

class ClusterBuilder
{
    // Cluster slot days (N = ISO day of week: 2=Tue 4=Thu 6=Sat)
    private const CLUSTER_DAYS = [2, 4, 6];

    // Max slots per cluster (1 pillar + 5 supporting)
    private const CLUSTER_SIZE = 6;

    // Cron hook for background generation
    public const HOOK_GENERATE = 'amai_cluster_generate';

    // --------------------------------------------------
    // Register hooks
    // --------------------------------------------------
    public static function register(): void
    {
        add_action(self::HOOK_GENERATE, [self::class, 'runGeneration'], 10, 1);
        add_action('wp_ajax_amai_generate_cluster', [self::class, 'handleGenerateCluster']);
        add_action('wp_ajax_amai_cluster_status',   [self::class, 'handleClusterStatus']);
    }

    // --------------------------------------------------
    // AJAX: kick off cluster generation
    // --------------------------------------------------
    public static function handleGenerateCluster(): void
    {
        check_ajax_referer('amai_cluster_nonce', 'nonce');
        if (!current_user_can('edit_posts')) {
            wp_send_json_error(['message' => 'Permission denied.'], 403);
        }

        $topic = isset($_POST['topic']) ? sanitize_text_field(wp_unslash((string) $_POST['topic'])) : '';
        if ($topic === '') {
            wp_send_json_error(['message' => 'Topic is required.']);
        }

        // Block if a cluster is already in-flight
        if (self::hasActiveCluster()) {
            wp_send_json_error(['message' => 'A cluster is already being generated. Please wait for it to complete.']);
        }

        // Create the cluster record
        $cluster_id = self::createCluster($topic);
        if ($cluster_id === 0) {
            wp_send_json_error(['message' => 'Failed to create cluster record.']);
        }

        // Schedule background generation immediately
        wp_schedule_single_event(time() + 5, self::HOOK_GENERATE, [$cluster_id]);

        wp_send_json_success([
            'cluster_id' => $cluster_id,
            'message'    => 'Cluster generation started. Posts will appear over the next few minutes.',
        ]);
    }

    // --------------------------------------------------
    // AJAX: poll cluster status
    // Schedule BgOptimizer for any clusters that completed since last poll
    // --------------------------------------------------
    public static function handleClusterStatus(): void
    {
        check_ajax_referer('amai_cluster_nonce', 'nonce');
        if (!current_user_can('edit_posts')) {
            wp_send_json_error(['message' => 'Permission denied.'], 403);
        }

        $clusters = self::getRecentClusters(5);

        // Check for pending BgOptimizer work left by completed cron jobs.
        // wp_schedule_single_event is unreliable inside cron context -- schedule here instead.
        foreach ($clusters as $cl) {
            $cluster_id = (int) $cl['id'];
            if ((string) ($cl['status'] ?? '') !== 'complete') {
                continue;
            }
            $transient_key = 'amai_cluster_bg_queue_' . $cluster_id;
            $pending_ids   = get_transient($transient_key);
            if (!is_array($pending_ids) || empty($pending_ids)) {
                continue;
            }
            // Schedule BgOptimizer for each post staggered by 30s
            $delay = 10;
            foreach ($pending_ids as $post_id) {
                $post_id = (int) $post_id;
                if ($post_id > 0) {
                    wp_schedule_single_event(time() + $delay, 'amai_bg_optimize_post', [$post_id]);
                    $delay += 30;
                }
            }
            delete_transient($transient_key);
        }

        wp_send_json_success(['clusters' => $clusters]);
    }

    // --------------------------------------------------
    // Background generation -- fired by cron
    // --------------------------------------------------
    public static function runGeneration(int $cluster_id): void
    {
        $cluster = self::getCluster($cluster_id);
        if ($cluster === null) {
            return;
        }

        $topic         = (string) $cluster['topic'];
        $approval_mode = (get_option('amai_blog_approval_mode', 'review') === 'autopilot') ? 'autopilot' : 'review';
        $publish       = $approval_mode === 'autopilot';

        // Build business context
        $profile           = (new BusinessProfile())->get();
        $business_name     = get_bloginfo('name');
        $business_category = '';
        $context_parts     = [];

        if (is_array($profile)) {
            $cat_map           = BusinessProfile::BUSINESS_CATEGORIES;
            $cat_key           = (string) ($profile['business_category'] ?? '');
            $business_category = $cat_map[$cat_key] ?? $cat_key;
            if (!empty($profile['differentiator'])) {
                $context_parts[] = 'What makes us different: ' . $profile['differentiator'];
            }
            if (!empty($profile['customer_descriptor'])) {
                $context_parts[] = 'Our customers: ' . $profile['customer_descriptor'];
            }
        }
        $context = implode("\n", $context_parts);

        // Step 1: generate supporting post titles
        $supporting_titles = self::generateSupportingTitles($topic, $business_name, $business_category, $context);
        if (empty($supporting_titles)) {
            self::markClusterFailed($cluster_id);
            return;
        }

        // Step 2: generate and save the pillar post
        $pillar_title = self::buildPillarTitle($topic);
        $pillar_slot  = self::nextClusterSlot();

        $pillar_post_id = self::generateAndSavePost(
            $pillar_title,
            $topic,
            $supporting_titles,
            'pillar',
            $business_name,
            $business_category,
            $context,
            $publish,
            $pillar_slot,
            null
        );

        if ($pillar_post_id === 0) {
            self::markClusterFailed($cluster_id);
            return;
        }

        self::recordClusterPost($cluster_id, $pillar_post_id, 'pillar', $pillar_slot);

        // Update cluster with pillar post ID and mark active
        global $wpdb;
        $wpdb->update(
            $wpdb->prefix . 'amai_clusters',
            [
                'pillar_post_id' => $pillar_post_id,
                'status'         => 'active',
                'updated_at'     => gmdate('Y-m-d H:i:s'),
            ],
            ['id' => $cluster_id]
        );

        // Step 3: generate and save each supporting post
        $used_slots      = [$pillar_slot];
        $pillar_url      = get_permalink($pillar_post_id);
        $published_count = 1;
        $all_post_ids    = [$pillar_post_id];

        foreach ($supporting_titles as $supporting_title) {
            $slot = self::nextClusterSlot($used_slots);
            $used_slots[] = $slot;

            $post_id = self::generateAndSavePost(
                $supporting_title,
                $topic,
                $supporting_titles,
                'supporting',
                $business_name,
                $business_category,
                $context,
                $publish,
                $slot,
                [
                    'pillar_title' => $pillar_title,
                    'pillar_url'   => (string) $pillar_url,
                ]
            );

            if ($post_id === 0) {
                continue;
            }

            $all_post_ids[] = $post_id;
            self::recordClusterPost($cluster_id, $post_id, 'supporting', $slot);
            $published_count++;

            // Avoid hammering Core API -- brief pause between posts
            sleep(2);
        }

        // Step 4: inject internal links into pillar pointing to all supporting posts
        self::injectPillarLinks($pillar_post_id, $cluster_id);

        // Mark cluster complete if all 6 posts created
        if ($published_count >= self::CLUSTER_SIZE) {
            $wpdb->update(
                $wpdb->prefix . 'amai_clusters',
                ['status' => 'complete', 'updated_at' => gmdate('Y-m-d H:i:s')],
                ['id' => $cluster_id]
            );
        }

        // Store post IDs in transient -- BgOptimizer will be scheduled from AJAX context
        // on the next status poll, where wp_schedule_single_event is reliable
        set_transient('amai_cluster_bg_queue_' . $cluster_id, $all_post_ids, 2 * HOUR_IN_SECONDS);
    }

    // --------------------------------------------------
    // Generate supporting post titles via Core
    // --------------------------------------------------
    private static function generateSupportingTitles(
        string $topic,
        string $business_name,
        string $business_category,
        string $context
    ): array {
        $prompt = 'You are a content strategist building a topical authority cluster for a business blog.'
            . "\n\nBusiness: " . $business_name
            . "\nCategory: " . $business_category
            . "\nPillar topic: " . $topic
            . ($context !== '' ? "\n\nContext:\n" . $context : '')
            . "\n\nGenerate exactly 5 supporting blog post titles that explore different angles, subtopics,"
            . ' and questions related to the pillar topic. Each title should be distinct, specific, and'
            . ' optimized for search intent. Return ONLY a JSON array of 5 title strings. No preamble,'
            . ' no explanation, no markdown fences. Example: ["Title One","Title Two","Title Three","Title Four","Title Five"]';

        require_once AMAI_PLUGIN_DIR . 'modules/ai-claude.php';
        $module_id = (string) get_option('amai_active_ai_provider', 'ai-claude');
        $module_id = $module_id !== '' ? $module_id : 'ai-claude';
        $module_file = AMAI_PLUGIN_DIR . 'modules/' . $module_id . '.php';
        if (!is_file($module_file)) {
            return [];
        }
        require_once $module_file;

        $class_suffix = ucfirst(substr($module_id, 3));
        $class_name   = 'AutoMarketerAI\\Modules\\' . $class_suffix;
        if (!class_exists($class_name)) {
            return [];
        }

        $module  = new $class_name();
        $api_key = \AutoMarketerAI\KeyVault::decrypt((string) get_option('amai_api_key_' . $module_id, ''));
        if ($api_key === '') {
            return [];
        }

        $model = $module->get_default_model();
        $raw   = $module->generate_raw($prompt, $api_key, $model, 512);
        $raw   = trim($raw);

        // Strip markdown fences if present
        $raw = (string) preg_replace('/^```json\s*/i', '', $raw);
        $raw = (string) preg_replace('/\s*```$/', '', $raw);
        $raw = trim($raw);

        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return [];
        }

        $titles = [];
        foreach ($decoded as $item) {
            $t = trim((string) $item);
            if ($t !== '') {
                $titles[] = $t;
            }
        }

        return array_slice($titles, 0, 5);
    }

    // --------------------------------------------------
    // Build pillar post title from topic
    // --------------------------------------------------
    private static function buildPillarTitle(string $topic): string
    {
        return 'The Complete Guide to ' . $topic;
    }

    // --------------------------------------------------
    // Generate and save a single cluster post
    // --------------------------------------------------
    private static function generateAndSavePost(
        string $title,
        string $cluster_topic,
        array $all_supporting_titles,
        string $role,
        string $business_name,
        string $business_category,
        string $context,
        bool $publish,
        string $slot_date,
        ?array $pillar_ref
    ): int {
        // Build role-specific prompt context
        if ($role === 'pillar') {
            $role_instruction = 'Write a comprehensive, long-form pillar post (1500-2000 words) that covers'
                . ' the topic broadly. This post will internally link to 5 supporting posts on these subtopics: '
                . implode(', ', $all_supporting_titles) . '. Leave placeholders like [LINK: Supporting Title]'
                . ' where natural links to those supporting posts should appear. End with a "Related Reading" section'
                . ' listing all 5 supporting post titles as [LINK: Title].';
        } else {
            $role_instruction = 'Write a focused, in-depth supporting post (800-1200 words) on this specific subtopic.'
                . ' This is part of a content cluster on "' . $cluster_topic . '".'
                . ' End the post with a paragraph that references and links to the main pillar guide:'
                . ' [LINK: ' . ($pillar_ref['pillar_title'] ?? '') . '] for the complete overview.';
        }

        $content = BlogGenerator::callCoreGenerateContentStatic(
            $title,
            $business_name,
            $business_category,
            $context . "\n\n" . $role_instruction
        );

        if ($content === null || $content === '') {
            return 0;
        }

        // Strip non-ASCII
        $content = (string) preg_replace('/[^\x00-\x7F]/', '', $content);

        // For supporting posts, replace pillar placeholder with real URL
        if ($role === 'supporting' && $pillar_ref !== null) {
            $pillar_title = $pillar_ref['pillar_title'] ?? '';
            $pillar_url   = $pillar_ref['pillar_url'] ?? '';
            if ($pillar_title !== '' && $pillar_url !== '') {
                $content = str_replace(
                    '[LINK: ' . $pillar_title . ']',
                    '<a href="' . esc_url($pillar_url) . '">' . esc_html($pillar_title) . '</a>',
                    $content
                );
            }
        }

        $primary_user_id = (int) get_option('amai_primary_user_id', 1);

        // When autopilot, schedule the post to publish on the slot date at 9am site time.
        // When review mode, save as draft (slot date is informational only via cluster_posts table).
        if ($publish) {
            $tz            = wp_timezone();
            $publish_dt    = new \DateTime($slot_date . ' 09:00:00', $tz);
            $post_date     = $publish_dt->format('Y-m-d H:i:s');
            $publish_dt->setTimezone(new \DateTimeZone('UTC'));
            $post_date_gmt = $publish_dt->format('Y-m-d H:i:s');
            $post_status   = 'future';
        } else {
            $post_date     = '';
            $post_date_gmt = '';
            $post_status   = 'draft';
        }

        $post_args = [
            'post_title'   => $title,
            'post_content' => $content,
            'post_status'  => $post_status,
            'post_type'    => 'post',
            'post_author'  => $primary_user_id,
        ];

        if ($post_date !== '') {
            $post_args['post_date']     = $post_date;
            $post_args['post_date_gmt'] = $post_date_gmt;
        }

        $post_id = wp_insert_post($post_args, true);

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

        // Tag as cluster-generated
        update_post_meta((int) $post_id, '_amai_cluster_generated', '1');
        update_post_meta((int) $post_id, '_amai_cluster_role', $role);

        // Featured image -- respect amai_blog_image_source setting
        $image_source = (string) get_option('amai_blog_image_source', 'unsplash');
        $image_set    = false;

        if ($image_source === 'ai' && class_exists('\AutoMarketerAI\ImageProvider')) {
            $prompt    = 'A high-quality professional photograph for a blog post titled "' . $title . '"';
            $local_url = \AutoMarketerAI\ImageProvider::resolveForPost($prompt);
            if ($local_url !== null) {
                $attachment_id = (int) attachment_url_to_postid($local_url);
                if ($attachment_id > 0) {
                    set_post_thumbnail((int) $post_id, $attachment_id);
                    $image_set = true;
                }
            }
        }

        if (!$image_set && class_exists('\AutoMarketerAI\Unsplash') && defined('AMAI_UNSPLASH_ACCESS_KEY') && AMAI_UNSPLASH_ACCESS_KEY !== '') {
            $keywords = Unsplash::extractKeywordsAi($title . ' ' . $business_category);
            if ($keywords !== '') {
                $photo = Unsplash::search($keywords);
                if ($photo !== null && !empty($photo['url']) && !empty($photo['photographer'])) {
                    $attachment_id = Unsplash::downloadToMedia($photo['url'], $photo['photographer'], (int) $post_id, (string) ($photo['profile_url'] ?? ''));
                    if ($attachment_id > 0) {
                        set_post_thumbnail((int) $post_id, $attachment_id);
                    }
                }
            }
        }

        return (int) $post_id;
    }

    // --------------------------------------------------
    // Inject real internal links into pillar post
    // after all supporting posts are created
    // --------------------------------------------------
    private static function injectPillarLinks(int $pillar_post_id, int $cluster_id): void
    {
        global $wpdb;
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT post_id FROM `{$wpdb->prefix}amai_cluster_posts`
                 WHERE cluster_id = %d AND post_role = 'supporting'",
                $cluster_id
            ),
            ARRAY_A
        );

        if (empty($rows)) {
            return;
        }

        $pillar = get_post($pillar_post_id);
        if (!$pillar instanceof \WP_Post) {
            return;
        }

        $content = $pillar->post_content;

        foreach ($rows as $row) {
            $post_id = (int) $row['post_id'];
            $post    = get_post($post_id);
            if (!$post instanceof \WP_Post) {
                continue;
            }
            $supporting_title = $post->post_title;
            $supporting_url   = get_permalink($post_id);
            $placeholder      = '[LINK: ' . $supporting_title . ']';
            $link             = '<a href="' . esc_url((string) $supporting_url) . '">' . esc_html($supporting_title) . '</a>';
            $content          = str_replace($placeholder, $link, $content);
        }

        wp_update_post([
            'ID'           => $pillar_post_id,
            'post_content' => $content,
        ]);
    }

    // --------------------------------------------------
    // Find next available Tue/Thu/Sat slot
    // --------------------------------------------------
    private static function nextClusterSlot(array $exclude = []): string
    {
        $tz = wp_timezone();
        $dt = new \DateTime('now', $tz);
        $dt->modify('+1 day');

        $attempts = 0;
        while ($attempts < 60) {
            $dow  = (int) $dt->format('N');
            $date = $dt->format('Y-m-d');

            if (in_array($dow, self::CLUSTER_DAYS, true) && !in_array($date, $exclude, true)) {
                // Check no existing cluster post already occupies this slot
                if (!self::slotOccupied($date)) {
                    return $date;
                }
            }

            $dt->modify('+1 day');
            $attempts++;
        }

        // Fallback -- should never happen in practice
        return $dt->format('Y-m-d');
    }

    // --------------------------------------------------
    // Check if a cluster slot date is already taken
    // --------------------------------------------------
    private static function slotOccupied(string $date): bool
    {
        global $wpdb;
        $count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM `{$wpdb->prefix}amai_cluster_posts` WHERE slot_date = %s",
                $date
            )
        );
        return $count > 0;
    }

    // --------------------------------------------------
    // Record a cluster post assignment
    // --------------------------------------------------
    private static function recordClusterPost(int $cluster_id, int $post_id, string $role, string $slot_date): void
    {
        global $wpdb;
        $wpdb->insert(
            $wpdb->prefix . 'amai_cluster_posts',
            [
                'cluster_id' => $cluster_id,
                'post_id'    => $post_id,
                'post_role'  => $role,
                'slot_date'  => $slot_date,
                'status'     => 'scheduled',
                'created_at' => gmdate('Y-m-d H:i:s'),
                'updated_at' => gmdate('Y-m-d H:i:s'),
            ]
        );
    }

    // --------------------------------------------------
    // Create a cluster record
    // --------------------------------------------------
    private static function createCluster(string $topic): int
    {
        global $wpdb;
        $wpdb->insert(
            $wpdb->prefix . 'amai_clusters',
            [
                'topic'      => $topic,
                'status'     => 'pending',
                'created_at' => gmdate('Y-m-d H:i:s'),
                'updated_at' => gmdate('Y-m-d H:i:s'),
            ]
        );
        return (int) $wpdb->insert_id;
    }

    // --------------------------------------------------
    // Check if any cluster is currently pending or active
    // --------------------------------------------------
    private static function hasActiveCluster(): bool
    {
        global $wpdb;
        $count = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM `{$wpdb->prefix}amai_clusters`
             WHERE status IN ('pending','active')"
        );
        return $count > 0;
    }

    // --------------------------------------------------
    // Mark cluster as failed
    // --------------------------------------------------
    private static function markClusterFailed(int $cluster_id): void
    {
        global $wpdb;
        $wpdb->update(
            $wpdb->prefix . 'amai_clusters',
            ['status' => 'complete', 'updated_at' => gmdate('Y-m-d H:i:s')],
            ['id' => $cluster_id]
        );
    }

    // --------------------------------------------------
    // Fetch cluster record
    // --------------------------------------------------
    private static function getCluster(int $cluster_id): ?array
    {
        global $wpdb;
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM `{$wpdb->prefix}amai_clusters` WHERE id = %d",
                $cluster_id
            ),
            ARRAY_A
        );
        return is_array($row) ? $row : null;
    }

    // --------------------------------------------------
    // Fetch recent clusters with post counts for UI
    // --------------------------------------------------
    public static function getRecentClusters(int $limit = 5): array
    {
        global $wpdb;
        $clusters = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT c.*, COUNT(cp.id) as post_count,
                        SUM(CASE WHEN cp.status = 'published' THEN 1 ELSE 0 END) as published_count
                 FROM `{$wpdb->prefix}amai_clusters` c
                 LEFT JOIN `{$wpdb->prefix}amai_cluster_posts` cp ON cp.cluster_id = c.id
                 GROUP BY c.id
                 ORDER BY c.created_at DESC
                 LIMIT %d",
                $limit
            ),
            ARRAY_A
        );

        if (!is_array($clusters)) {
            return [];
        }

        // Attach post details to each cluster
        foreach ($clusters as &$cluster) {
            $cluster_id = (int) $cluster['id'];
            $posts      = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT cp.post_id, cp.post_role, cp.slot_date, cp.status
                     FROM `{$wpdb->prefix}amai_cluster_posts` cp
                     WHERE cp.cluster_id = %d
                     ORDER BY cp.post_role DESC, cp.slot_date ASC",
                    $cluster_id
                ),
                ARRAY_A
            );

            $cluster['posts'] = [];
            if (is_array($posts)) {
                foreach ($posts as $cp) {
                    $post_id   = (int) $cp['post_id'];
                    $wp_post   = get_post($post_id);
                    $edit_url  = $post_id > 0 ? get_edit_post_link($post_id) : '';
                    $view_url  = $post_id > 0 ? get_permalink($post_id) : '';
                    $cluster['posts'][] = [
                        'post_id'   => $post_id,
                        'post_role' => $cp['post_role'],
                        'slot_date' => $cp['slot_date'],
                        'status'    => $cp['status'],
                        'title'     => $wp_post instanceof \WP_Post ? $wp_post->post_title : '',
                        'wp_status' => $wp_post instanceof \WP_Post ? $wp_post->post_status : '',
                        'edit_url'  => (string) $edit_url,
                        'view_url'  => (string) $view_url,
                    ];
                }
            }
        }
        unset($cluster);

        return $clusters;
    }

    // --------------------------------------------------
    // Render the Clusters tab UI
    // --------------------------------------------------
    public static function render(): void
    {
        $has_active    = self::hasActiveCluster();
        $recent        = self::getRecentClusters(5);
        $nonce         = wp_create_nonce('amai_cluster_nonce');
        $approval_mode = (string) get_option('amai_blog_approval_mode', 'review');
        $mode_label    = $approval_mode === 'autopilot' ? 'Posts will be published automatically.' : 'Posts will be saved as drafts for your review.';

        include AMAI_PLUGIN_DIR . 'templates/cluster-builder.php';
    }
}