<?php
if (!defined('ABSPATH')) {
    exit;
}

final class Aysa_Onpage_Workspace
{
    private string $inventory_table;
    private string $runs_table;

    public function __construct()
    {
        global $wpdb;
        $this->inventory_table = $wpdb->prefix . 'aysa_onpage_inventory';
        $this->runs_table = $wpdb->prefix . 'aysa_onpage_projection_runs';
    }

    public function ensure_schema(): void
    {
        global $wpdb;

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        $charset = $wpdb->get_charset_collate();

        dbDelta(
            "CREATE TABLE {$this->inventory_table} (
                id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
                project_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
                entity_key VARCHAR(191) NOT NULL,
                entity_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
                object_kind VARCHAR(16) NOT NULL DEFAULT 'post',
                post_type VARCHAR(64) DEFAULT NULL,
                taxonomy VARCHAR(64) DEFAULT NULL,
                url TEXT DEFAULT NULL,
                title TEXT DEFAULT NULL,
                entity_group VARCHAR(32) NOT NULL DEFAULT 'other',
                publication_state VARCHAR(32) NOT NULL DEFAULT 'published',
                mapping_state VARCHAR(32) NOT NULL DEFAULT 'needs_mapping',
                seo_state VARCHAR(32) NOT NULL DEFAULT 'not_optimized',
                optimization_scope VARCHAR(32) NOT NULL DEFAULT 'none',
                meta_state VARCHAR(32) NOT NULL DEFAULT 'none',
                rewrite_state VARCHAR(32) NOT NULL DEFAULT 'not_needed',
                output_state VARCHAR(32) NOT NULL DEFAULT 'none',
                main_keyword VARCHAR(255) DEFAULT NULL,
                confidence_score DECIMAL(5,2) DEFAULT NULL,
                comparison_result VARCHAR(64) DEFAULT NULL,
                keyword_owner_entity_key VARCHAR(191) DEFAULT NULL,
                keyword_owner_url TEXT DEFAULT NULL,
                keyword_conflict_keyword VARCHAR(255) DEFAULT NULL,
                keyword_conflict_reason TEXT DEFAULT NULL,
                decision_reason TEXT DEFAULT NULL,
                next_step VARCHAR(255) DEFAULT NULL,
                seo_score SMALLINT DEFAULT NULL,
                content_source VARCHAR(64) DEFAULT NULL,
                meta_package_json LONGTEXT DEFAULT NULL,
                rewrite_decision_json LONGTEXT DEFAULT NULL,
                rewrite_package_json LONGTEXT DEFAULT NULL,
                worker_run_id BIGINT UNSIGNED DEFAULT NULL,
                last_evaluated_at DATETIME DEFAULT NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                PRIMARY KEY  (id),
                UNIQUE KEY uniq_project_entity (project_id, entity_key),
                KEY idx_project_group (project_id, entity_group),
                KEY idx_project_mapping (project_id, mapping_state),
                KEY idx_project_seo (project_id, seo_state),
                KEY idx_project_scope (project_id, optimization_scope),
                KEY idx_project_publication (project_id, publication_state)
            ) {$charset};"
        );

        dbDelta(
            "CREATE TABLE {$this->runs_table} (
                id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
                project_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
                run_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
                run_type VARCHAR(64) NOT NULL DEFAULT 'onpage',
                status VARCHAR(32) NOT NULL DEFAULT 'idle',
                current_task_key VARCHAR(128) DEFAULT NULL,
                current_milestone VARCHAR(255) DEFAULT NULL,
                summary_json LONGTEXT DEFAULT NULL,
                started_at DATETIME DEFAULT NULL,
                finished_at DATETIME DEFAULT NULL,
                created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                PRIMARY KEY  (id),
                UNIQUE KEY uniq_project_run (project_id, run_id),
                KEY idx_project_status (project_id, status)
            ) {$charset};"
        );
    }

    public function build_workspace(int $project_id, array $args = array()): array
    {
        $this->ensure_schema();
        $project_id = max(0, $project_id);
        $this->ensure_seeded($project_id, !empty($args['refresh']));

        $entity_group = sanitize_key((string) ($args['entity_group'] ?? 'page'));
        if (!in_array($entity_group, array('page', 'article', 'ecommerce_category', 'product', 'other', 'keyword_ownership'), true)) {
            $entity_group = 'page';
        }
        $filters = isset($args['filters']) && is_array($args['filters']) ? $args['filters'] : array();
        $page = max(1, intval($args['page'] ?? 1));
        $per_page = max(1, min(100, intval($args['per_page'] ?? 50)));

        $rows = $this->get_inventory_rows($project_id);
        $run = $this->get_latest_run($project_id);
        $ownership_map = $this->build_keyword_uniqueness_map($rows);
        $source_rows = $entity_group === 'keyword_ownership'
            ? array_values($ownership_map['rows'] ?? array())
            : $rows;
        $filtered_rows = $this->apply_filters($source_rows, $entity_group, $filters);
        $total = count($filtered_rows);
        $offset = ($page - 1) * $per_page;
        $paged_rows = array_slice($filtered_rows, $offset, $per_page);

        return array(
            'summary' => $this->build_summary($rows, $entity_group, $run, $ownership_map),
            'filters' => $this->build_filter_counts($rows, $entity_group, $ownership_map),
            'rows' => array_values($paged_rows),
            'pagination' => array(
                'page' => $page,
                'per_page' => $per_page,
                'total' => $total,
                'pages' => max(1, (int) ceil($total / $per_page)),
            ),
            'run' => $run,
            'active_entity_group' => $entity_group,
            'ownership_summary' => isset($ownership_map['summary']) && is_array($ownership_map['summary']) ? $ownership_map['summary'] : array(),
            'ownership_rows' => isset($ownership_map['rows']) && is_array($ownership_map['rows']) ? array_values($ownership_map['rows']) : array(),
        );
    }

    public function get_detail(int $project_id, string $entity_key): array
    {
        global $wpdb;
        $this->ensure_schema();
        $this->ensure_seeded($project_id, false);

        $inventory = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$this->inventory_table} WHERE project_id = %d AND entity_key = %s LIMIT 1", $project_id, $entity_key),
            ARRAY_A
        );

        if (!is_array($inventory)) {
            return array('inventory' => array(), 'seo_history' => array(), 'keywords' => array());
        }

        $keywords = $this->get_keywords_for_entity($project_id, $entity_key);
        $history = $this->get_current_history_record(
            intval($inventory['entity_id'] ?? 0),
            (string) ($inventory['object_kind'] ?? ''),
            (string) ($inventory['post_type'] ?? ''),
            (string) ($inventory['taxonomy'] ?? '')
        );

        return array(
            'inventory' => $inventory,
            'seo_history' => is_array($history) ? $history : array(),
            'keywords' => $keywords,
            'meta_package' => $this->decode_inventory_json($inventory['meta_package_json'] ?? null),
            'rewrite_decision' => $this->decode_inventory_json($inventory['rewrite_decision_json'] ?? null),
            'rewrite_package' => $this->decode_inventory_json($inventory['rewrite_package_json'] ?? null),
        );
    }

    public function build_run_selection_meta(int $project_id, string $entity_group = 'all', array $filters = array(), array $selection = array()): array
    {
        $this->ensure_schema();
        $this->ensure_seeded($project_id, false);

        $entity_group = sanitize_key($entity_group);
        $selection = array_values(array_filter(array_map('strval', $selection)));
        $selection_lookup = $selection !== array() ? array_fill_keys($selection, true) : array();

        $rows = $this->get_inventory_rows($project_id);
        if ($entity_group !== '' && $entity_group !== 'all') {
            $rows = $this->apply_filters($rows, $entity_group, $filters);
        }

        $out = array();
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $entity_key = (string) ($row['entity_key'] ?? '');
            if ($entity_key === '') {
                continue;
            }
            if ($selection_lookup !== array() && !isset($selection_lookup[$entity_key])) {
                continue;
            }

            $detail = $this->get_detail($project_id, $entity_key);
            $inventory = isset($detail['inventory']) && is_array($detail['inventory']) ? $detail['inventory'] : $row;
            $history = isset($detail['seo_history']) && is_array($detail['seo_history']) ? $detail['seo_history'] : array();
            $keywords = isset($detail['keywords']) && is_array($detail['keywords']) ? array_values(array_filter(array_map('strval', $detail['keywords']))) : array();

            $context = $this->build_entity_run_context($inventory, $history, $keywords);
            if ($context !== array()) {
                $out[] = $context;
            }
        }

        return $out;
    }

    public function build_selection_meta_for_entity(int $project_id, string $entity_key): array
    {
        $rows = $this->build_run_selection_meta($project_id, 'all', array(), array($entity_key));
        return isset($rows[0]) && is_array($rows[0]) ? $rows[0] : array();
    }

    public function sync_remote_run_status(int $project_id, array $run, array $tasks = array()): array
    {
        global $wpdb;
        $this->ensure_schema();

        $run_id = intval($run['id'] ?? 0);
        if ($project_id <= 0 || $run_id <= 0) {
            return array('ok' => false, 'error' => 'missing_run_context');
        }

        $payload = array(
            'project_id' => $project_id,
            'run_id' => $run_id,
            'run_type' => sanitize_key((string) ($run['requested_run_type'] ?? $run['run_type'] ?? 'onpage')),
            'status' => sanitize_key((string) ($run['status'] ?? 'queued')),
            'current_task_key' => sanitize_text_field((string) ($run['current_task_key'] ?? '')),
            'current_milestone' => sanitize_text_field((string) ($run['current_milestone'] ?? '')),
            'summary_json' => wp_json_encode(array(
                'task_counts' => isset($run['task_counts']) && is_array($run['task_counts']) ? $run['task_counts'] : array(),
                'tasks' => array_values(array_filter(array_map(function ($task) {
                    if (!is_array($task)) {
                        return null;
                    }
                    return array(
                        'task_key' => (string) ($task['task_key'] ?? ''),
                        'status' => (string) ($task['status'] ?? ''),
                        'error_text' => (string) ($task['error_text'] ?? ''),
                    );
                }, $tasks))),
            )),
            'started_at' => $this->nullable_datetime($run['started_at'] ?? current_time('mysql')),
            'finished_at' => $this->nullable_datetime($run['finished_at'] ?? null),
        );

        $existing = $wpdb->get_var(
            $wpdb->prepare("SELECT id FROM {$this->runs_table} WHERE project_id = %d AND run_id = %d LIMIT 1", $project_id, $run_id)
        );

        if ($existing) {
            $wpdb->update($this->runs_table, $payload, array('id' => intval($existing)));
        } else {
            $wpdb->insert($this->runs_table, $payload);
        }

        foreach ($tasks as $task) {
            if (!is_array($task)) {
                continue;
            }
            $inputSummary = isset($task['input_summary']) && is_array($task['input_summary']) ? $task['input_summary'] : array();
            if ($inputSummary === array() && !empty($task['input_json']) && is_string($task['input_json'])) {
                $decoded = json_decode($task['input_json'], true);
                if (is_array($decoded)) {
                    $inputSummary = $decoded;
                }
            }
            $selection = isset($inputSummary['selection']) && is_array($inputSummary['selection']) ? array_values(array_map('strval', $inputSummary['selection'])) : array();
            foreach ($selection as $entity_key) {
                if ($entity_key === '') {
                    continue;
                }
                $taskKey = (string) ($task['task_key'] ?? '');
                $taskStatus = (string) ($task['status'] ?? '');
                $rewrite_state = null;
                $meta_state = null;
                $output_state = null;
                $next_step = null;
                if ($taskKey === 'onpage_rewrite_decision') {
                    $rewrite_state = match ($taskStatus) {
                        'queued' => 'recommended',
                        'running' => 'recommended',
                        'retry' => 'recommended',
                        'failed' => 'review_required',
                        default => null,
                    };
                    $output_state = in_array($taskStatus, array('queued', 'running', 'retry'), true) ? 'processing' : null;
                    $next_step = in_array($taskStatus, array('queued', 'running', 'retry'), true) ? 'Await rewrite decision' : null;
                } elseif ($taskKey === 'onpage_generate_content_rewrite') {
                    $rewrite_state = match ($taskStatus) {
                        'queued' => 'review_required',
                        'running' => 'review_required',
                        'retry' => 'review_required',
                        'failed' => 'review_required',
                        default => null,
                    };
                    $output_state = in_array($taskStatus, array('queued', 'running', 'retry'), true) ? 'processing' : null;
                    $next_step = in_array($taskStatus, array('queued', 'running', 'retry'), true) ? 'Await generated rewrite' : null;
                } elseif (strpos($taskKey, 'onpage_generate_meta_only') === 0) {
                    $meta_state = in_array($taskStatus, array('queued', 'running', 'retry'), true) ? 'generated' : null;
                    $output_state = in_array($taskStatus, array('queued', 'running', 'retry'), true) ? 'processing' : null;
                    $next_step = in_array($taskStatus, array('queued', 'running', 'retry'), true) ? 'Await generated meta' : null;
                }

                $update = array(
                    'worker_run_id' => $run_id,
                    'last_evaluated_at' => current_time('mysql'),
                );
                if ($rewrite_state !== null) {
                    $update['rewrite_state'] = $rewrite_state;
                    $update['optimization_scope'] = 'content_rewrite';
                }
                if ($meta_state !== null) {
                    $update['meta_state'] = $meta_state;
                    if (empty($update['optimization_scope'])) {
                        $update['optimization_scope'] = 'meta_only';
                    }
                }
                if ($output_state !== null) {
                    $update['output_state'] = $output_state;
                }
                if ($next_step !== null) {
                    $update['next_step'] = $next_step;
                }
                $wpdb->update(
                    $this->inventory_table,
                    $update,
                    array(
                        'project_id' => $project_id,
                        'entity_key' => $entity_key,
                    )
                );
            }
        }

        return array('ok' => true, 'run_id' => $run_id);
    }

    public function apply_projection(int $project_id, int $run_id, array $projection): array
    {
        global $wpdb;
        $this->ensure_schema();

        $mapping_updates = isset($projection['mapping_updates']) && is_array($projection['mapping_updates']) ? $projection['mapping_updates'] : array();
        if ($mapping_updates !== array()) {
            $this->sync_mapping_updates($project_id, $mapping_updates);
        }

        $inventory_rows = isset($projection['inventory_rows']) && is_array($projection['inventory_rows']) ? $projection['inventory_rows'] : array();
        if ($inventory_rows === [] && !empty($projection['refresh_inventory'])) {
            $inventory_rows = $this->build_projection_rows($project_id);
        }
        foreach ($inventory_rows as $row) {
            if (is_array($row)) {
                $this->upsert_inventory($project_id, $row);
            }
        }

        $meta_apply_result = array();
        if (!empty($projection['meta_apply_plan']) && is_array($projection['meta_apply_plan'])) {
            $meta_apply_result = $this->apply_meta_plan($project_id, $run_id, $projection['meta_apply_plan']);
        }

        $existing = $wpdb->get_var(
            $wpdb->prepare("SELECT id FROM {$this->runs_table} WHERE project_id = %d AND run_id = %d LIMIT 1", $project_id, $run_id)
        );

        $payload = array(
            'project_id' => $project_id,
            'run_id' => $run_id,
            'run_type' => sanitize_key((string) ($projection['run_type'] ?? 'onpage')),
            'status' => sanitize_key((string) ($projection['run_status'] ?? 'completed')),
            'current_task_key' => sanitize_text_field((string) ($projection['current_task_key'] ?? 'plugin_projection_sync')),
            'current_milestone' => sanitize_text_field((string) ($projection['current_milestone'] ?? 'Projection synced')),
            'summary_json' => wp_json_encode(array_merge(
                isset($projection['summary']) && is_array($projection['summary']) ? $projection['summary'] : array(),
                $meta_apply_result !== array() ? array('meta_apply' => $meta_apply_result) : array()
            )),
            'started_at' => $this->nullable_datetime($projection['started_at'] ?? current_time('mysql')),
            'finished_at' => $this->nullable_datetime($projection['finished_at'] ?? current_time('mysql')),
        );

        if ($existing) {
            $wpdb->update($this->runs_table, $payload, array('id' => intval($existing)));
        } else {
            $wpdb->insert($this->runs_table, $payload);
        }

        return array(
            'ok' => true,
            'run_id' => $run_id,
            'inventory_rows' => count($inventory_rows),
            'mapping_updates' => count($mapping_updates),
            'meta_apply' => $meta_apply_result,
        );
    }

    private function apply_meta_plan(int $project_id, int $run_id, array $plan): array
    {
        $idempotency_key = sanitize_text_field((string) ($plan['idempotency_key'] ?? ''));
        $payload_hash = sanitize_text_field((string) ($plan['payload_hash'] ?? ''));
        $items = isset($plan['items']) && is_array($plan['items']) ? $plan['items'] : array();
        $result = array(
            'action' => 'execute_onpage_plan',
            'section' => sanitize_key((string) ($plan['section'] ?? 'meta_snippets')),
            'operation' => 'apply_meta',
            'run_id' => $run_id,
            'payload_hash' => $payload_hash,
            'accepted_count' => count($items),
            'applied_count' => 0,
            'failed_count' => 0,
            'skipped_count' => 0,
            'status' => 'skipped',
            'errors' => array(),
            'item_results' => array(),
            'timestamp' => current_time('mysql'),
        );
        if ($idempotency_key === '' || $items === array()) {
            $result['status'] = 'rejected';
            $result['error'] = 'onpage_execution_plan_invalid';
            $this->log_agent8v3_onpage_execution($idempotency_key, $result);
            return $result;
        }

        $applied = get_option('aysa_agent8v3_onpage_execution_idempotency', array());
        $applied = is_array($applied) ? $applied : array();
        $previous_item_results = array();
        if (!empty($applied[$idempotency_key]) && is_array($applied[$idempotency_key])) {
            $previous = $applied[$idempotency_key];
            $previous_item_results = is_array($previous['item_results'] ?? null) ? $previous['item_results'] : array();
            if (intval($previous['failed_count'] ?? 0) <= 0 && intval($previous['skipped_count'] ?? 0) <= 0 && intval($previous['applied_count'] ?? 0) > 0) {
                $previous['status'] = 'already_synced';
                $this->log_agent8v3_onpage_execution($idempotency_key, $previous);
                return $previous;
            }
        }

        $previous_applied = array();
        foreach ($previous_item_results as $previous_item) {
            if (is_array($previous_item) && (string) ($previous_item['status'] ?? '') === 'applied') {
                $previous_applied[sanitize_text_field((string) ($previous_item['item_id'] ?? $previous_item['entity_key'] ?? ''))] = true;
            }
        }

        foreach ($items as $item) {
            if (!is_array($item)) {
                $result['skipped_count']++;
                $result['item_results'][] = array('status' => 'skipped', 'reason' => 'invalid_item');
                continue;
            }
            $entity_key = sanitize_text_field((string) ($item['entity_key'] ?? ''));
            $item_id = sanitize_text_field((string) ($item['item_id'] ?? $entity_key));
            if ($entity_key === '') {
                $result['skipped_count']++;
                $result['item_results'][] = array('item_id' => $item_id, 'entity_key' => '', 'status' => 'skipped', 'reason' => 'entity_key_required');
                continue;
            }
            if ($item_id !== '' && isset($previous_applied[$item_id])) {
                $result['skipped_count']++;
                $result['item_results'][] = array('item_id' => $item_id, 'entity_key' => $entity_key, 'status' => 'already_synced', 'reason' => '');
                continue;
            }
            $apply = $this->apply_meta_output($project_id, $entity_key);
            if (!empty($apply['ok'])) {
                $result['applied_count']++;
                $result['item_results'][] = array('item_id' => $item_id, 'entity_key' => $entity_key, 'status' => 'applied', 'reason' => '');
            } else {
                $reason = sanitize_key((string) ($apply['error'] ?? 'onpage_apply_failed'));
                $result['failed_count']++;
                $result['errors'][] = $reason;
                $result['item_results'][] = array('item_id' => $item_id, 'entity_key' => $entity_key, 'status' => 'failed', 'reason' => $reason);
            }
        }

        $result['status'] = $result['failed_count'] > 0 && $result['applied_count'] === 0 ? 'failed' : 'completed';
        $applied[$idempotency_key] = array_merge($result, array(
            'item_results' => array_slice($result['item_results'], 0, 500),
            'errors' => array_values(array_slice($result['errors'], 0, 20)),
        ));
        update_option('aysa_agent8v3_onpage_execution_idempotency', array_slice($applied, -100, null, true), false);
        $this->log_agent8v3_onpage_execution($idempotency_key, $result);
        return $result;
    }

    private function log_agent8v3_onpage_execution(string $idempotency_key, array $result): void
    {
        $events = get_option('aysa_agent8v3_onpage_executions', array());
        $events = is_array($events) ? $events : array();
        array_unshift($events, array_merge($result, array(
            'idempotency_key' => $idempotency_key,
            'timestamp' => current_time('mysql'),
        )));
        update_option('aysa_agent8v3_onpage_executions', array_slice($events, 0, 50), false);
    }

    public function seed_projection(int $project_id): array
    {
        $rows = $this->build_projection_rows($project_id);
        $summary = $this->build_seed_summary($rows);

        return array(
            'run_type' => 'onpage',
            'run_status' => 'completed',
            'current_task_key' => 'plugin_projection_sync',
            'current_milestone' => 'Projection prepared from plugin workspace',
            'refresh_inventory' => true,
            'inventory_rows' => $rows,
            'state_summary' => $summary,
            'summary' => $summary,
            'finished_at' => current_time('mysql'),
        );
    }

    public function refresh_entity(int $project_id, string $entity_key): array
    {
        global $wpdb;
        $this->ensure_schema();

        $rows = $this->build_projection_rows($project_id);
        $target_row = null;
        foreach ($rows as $row) {
            if ((string) ($row['entity_key'] ?? '') === $entity_key) {
                $target_row = $row;
                break;
            }
        }

        if (!is_array($target_row)) {
            $wpdb->delete($this->inventory_table, array('project_id' => $project_id, 'entity_key' => $entity_key));
            return array('ok' => false, 'error' => 'entity_not_found');
        }

        $this->upsert_inventory($project_id, $target_row);

        return array(
            'ok' => true,
            'inventory' => $target_row,
        );
    }

    public function accept_mapping_review(int $project_id, string $entity_key): array
    {
        global $wpdb;
        $this->ensure_schema();

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$this->inventory_table} WHERE project_id = %d AND entity_key = %s LIMIT 1", $project_id, $entity_key),
            ARRAY_A
        );
        if (!is_array($row)) {
            return array('ok' => false, 'error' => 'entity_not_found');
        }

        $keyword = trim((string) ($row['main_keyword'] ?? ''));
        if ($keyword === '') {
            return array('ok' => false, 'error' => 'missing_review_keyword');
        }

        $associated_on = $this->resolve_association_type_from_inventory($row);
        $entity_id = intval($row['entity_id'] ?? 0);
        $value = trim((string) ($row['title'] ?? ''));
        if ($associated_on === '' || $entity_id <= 0 || $value === '') {
            return array('ok' => false, 'error' => 'invalid_review_context');
        }

        $this->sync_mapping_updates($project_id, array(array(
            'keyword' => $keyword,
            'associated_on' => $associated_on,
            'id_intern' => $entity_id,
            'value' => $value,
        )));

        $wpdb->update(
            $this->inventory_table,
            array(
                'mapping_state' => 'mapped',
                'comparison_result' => 'accepted_review_keyword',
                'keyword_owner_entity_key' => null,
                'keyword_owner_url' => null,
                'keyword_conflict_keyword' => null,
                'keyword_conflict_reason' => null,
                'next_step' => 'Generate meta package',
                'decision_reason' => 'Suggested keyword accepted and saved to local mapping.',
                'last_evaluated_at' => current_time('mysql'),
            ),
            array(
                'project_id' => $project_id,
                'entity_key' => $entity_key,
            )
        );

        return array('ok' => true, 'entity_key' => $entity_key, 'keyword' => $keyword);
    }

    public function reject_mapping_review(int $project_id, string $entity_key): array
    {
        global $wpdb;
        $this->ensure_schema();

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$this->inventory_table} WHERE project_id = %d AND entity_key = %s LIMIT 1", $project_id, $entity_key),
            ARRAY_A
        );
        if (!is_array($row)) {
            return array('ok' => false, 'error' => 'entity_not_found');
        }

        $wpdb->update(
            $this->inventory_table,
            array(
                'mapping_state' => 'needs_mapping',
                'main_keyword' => null,
                'confidence_score' => null,
                'comparison_result' => 'rejected_review_keyword',
                'keyword_owner_entity_key' => null,
                'keyword_owner_url' => null,
                'keyword_conflict_keyword' => null,
                'keyword_conflict_reason' => null,
                'next_step' => 'Keyword mapping needed',
                'decision_reason' => 'Suggested keyword rejected.',
                'last_evaluated_at' => current_time('mysql'),
            ),
            array(
                'project_id' => $project_id,
                'entity_key' => $entity_key,
            )
        );

        return array('ok' => true, 'entity_key' => $entity_key);
    }

    public function apply_meta_output(int $project_id, string $entity_key): array
    {
        $detail = $this->get_detail($project_id, $entity_key);
        $inventory = isset($detail['inventory']) && is_array($detail['inventory']) ? $detail['inventory'] : array();
        if ($inventory === array()) {
            return array('ok' => false, 'error' => 'entity_not_found');
        }

        $meta_package = isset($detail['meta_package']) && is_array($detail['meta_package']) ? $detail['meta_package'] : array();
        $meta_title = trim((string) ($meta_package['meta_title'] ?? ''));
        $meta_description = trim((string) ($meta_package['meta_description'] ?? ''));
        if ($meta_title === '' && $meta_description === '') {
            return array('ok' => false, 'error' => 'meta_package_missing');
        }

        $entity_id = intval($inventory['entity_id'] ?? 0);
        $object_kind = (string) ($inventory['object_kind'] ?? 'post');
        if ($entity_id <= 0) {
            return array('ok' => false, 'error' => 'invalid_entity_id');
        }

        $previous_meta_title = $object_kind === 'term'
            ? trim((string) get_term_meta($entity_id, '_aysa_seo_title', true))
            : trim((string) get_post_meta($entity_id, '_aysa_seo_title', true));
        $previous_meta_description = $object_kind === 'term'
            ? trim((string) get_term_meta($entity_id, '_aysa_seo_description', true))
            : trim((string) get_post_meta($entity_id, '_aysa_seo_description', true));

        $this->persist_entity_meta($inventory, $meta_title, $meta_description);
        $this->log_onpage_history_entry($inventory, array(
            'meta_title' => $meta_title,
            'meta_description' => $meta_description,
            'previous_meta_title' => $previous_meta_title,
            'previous_meta_description' => $previous_meta_description,
            'generator_type' => 'agent8_onpage_meta',
            'generation_method' => 'onpage_workspace_apply_meta',
            'status' => 'applied',
            'is_applied' => 1,
            'confidence_score' => isset($meta_package['confidence_score']) && is_numeric($meta_package['confidence_score'])
                ? (float) $meta_package['confidence_score']
                : null,
            'notes' => 'Applied generated on-page meta package from workspace.',
        ));

        if ($object_kind === 'term') {
            update_term_meta($entity_id, '_aysa_generated_meta_title', $meta_title);
            update_term_meta($entity_id, '_aysa_generated_meta_description', $meta_description);
        } else {
            update_post_meta($entity_id, '_aysa_generated_meta_title', $meta_title);
            update_post_meta($entity_id, '_aysa_generated_meta_description', $meta_description);
        }

        $this->refresh_entity($project_id, $entity_key);

        return array(
            'ok' => true,
            'entity_key' => $entity_key,
            'meta_title' => $meta_title,
            'meta_description' => $meta_description,
        );
    }

    public function apply_rewrite_output(int $project_id, string $entity_key): array
    {
        $detail = $this->get_detail($project_id, $entity_key);
        $inventory = isset($detail['inventory']) && is_array($detail['inventory']) ? $detail['inventory'] : array();
        if ($inventory === array()) {
            return array('ok' => false, 'error' => 'entity_not_found');
        }

        $rewrite_package = isset($detail['rewrite_package']) && is_array($detail['rewrite_package']) ? $detail['rewrite_package'] : array();
        $rewritten_content = trim((string) ($rewrite_package['rewritten_content'] ?? ''));
        $content_title = trim((string) ($rewrite_package['content_title'] ?? ''));
        $content_description = trim((string) ($rewrite_package['content_description'] ?? ''));
        if ($rewritten_content === '' && $content_title === '' && $content_description === '') {
            return array('ok' => false, 'error' => 'rewrite_package_missing');
        }

        $entity_id = intval($inventory['entity_id'] ?? 0);
        $object_kind = (string) ($inventory['object_kind'] ?? 'post');
        if ($entity_id <= 0) {
            return array('ok' => false, 'error' => 'invalid_entity_id');
        }

        $previous_content_title = '';
        $previous_content_description = '';
        $previous_short_description = '';

        if ($object_kind === 'term') {
            $term = get_term($entity_id, (string) ($inventory['taxonomy'] ?? ''));
            if (!$term || is_wp_error($term)) {
                return array('ok' => false, 'error' => 'term_not_found');
            }
            $previous_content_title = (string) $term->name;
            $previous_content_description = (string) $term->description;
            $term_update = array();
            if ($content_title !== '' && in_array((string) ($inventory['entity_group'] ?? ''), array('ecommerce_category', 'other'), true)) {
                $term_update['name'] = $content_title;
            }
            if ($rewritten_content !== '') {
                $term_update['description'] = $rewritten_content;
            } elseif ($content_description !== '') {
                $term_update['description'] = $content_description;
            }
            if ($term_update !== array()) {
                wp_update_term($entity_id, (string) $term->taxonomy, $term_update);
            }
            update_term_meta($entity_id, '_aysa_generated_content', array(
                'content_title' => $content_title,
                'content_description' => $content_description,
                'rewritten_content' => $rewritten_content,
                'applied_at' => current_time('mysql'),
            ));
        } else {
            $post = get_post($entity_id);
            if (!$post instanceof WP_Post) {
                return array('ok' => false, 'error' => 'post_not_found');
            }
            $previous_content_title = (string) $post->post_title;
            $previous_content_description = (string) $post->post_content;
            $previous_short_description = (string) $post->post_excerpt;
            $post_update = array('ID' => $entity_id);
            if ($rewritten_content !== '') {
                $post_update['post_content'] = $rewritten_content;
            } elseif ($content_description !== '') {
                $post_update['post_content'] = $content_description;
            }
            if ($content_description !== '') {
                $post_update['post_excerpt'] = $content_description;
            }
            if ($content_title !== '' && in_array((string) ($inventory['entity_group'] ?? ''), array('article', 'product'), true)) {
                $post_update['post_title'] = $content_title;
            }
            if (count($post_update) > 1) {
                wp_update_post($post_update);
            }
            update_post_meta($entity_id, '_aysa_generated_content', array(
                'content_title' => $content_title,
                'content_description' => $content_description,
                'rewritten_content' => $rewritten_content,
                'applied_at' => current_time('mysql'),
            ));
        }

        $this->log_onpage_history_entry($inventory, array(
            'content_title' => $content_title !== '' ? $content_title : $previous_content_title,
            'content_description' => $rewritten_content !== '' ? $rewritten_content : $content_description,
            'short_description' => $content_description !== '' ? $content_description : null,
            'previous_content_title' => $previous_content_title,
            'previous_content_description' => $previous_content_description,
            'previous_short_description' => $previous_short_description,
            'generator_type' => 'agent8_onpage_rewrite',
            'generation_method' => 'onpage_workspace_apply_rewrite',
            'status' => 'applied',
            'is_applied' => 1,
            'confidence_score' => isset($rewrite_package['confidence_score']) && is_numeric($rewrite_package['confidence_score'])
                ? (float) $rewrite_package['confidence_score']
                : null,
            'notes' => 'Applied generated on-page rewrite from workspace.',
        ));

        $this->refresh_entity($project_id, $entity_key);

        return array(
            'ok' => true,
            'entity_key' => $entity_key,
            'content_title' => $content_title,
        );
    }

    public function skip_entity(int $project_id, string $entity_key): array
    {
        global $wpdb;
        $this->ensure_schema();

        $updated = $wpdb->update(
            $this->inventory_table,
            array(
                'next_step' => 'Skipped manually',
                'decision_reason' => 'Entity was skipped from the on-page queue.',
                'output_state' => 'skipped',
                'last_evaluated_at' => current_time('mysql'),
            ),
            array(
                'project_id' => $project_id,
                'entity_key' => $entity_key,
            )
        );
        if ($updated === false) {
            return array('ok' => false, 'error' => 'skip_update_failed');
        }

        return array('ok' => true, 'entity_key' => $entity_key);
    }

    public function batch_apply_operation(int $project_id, string $entity_group, array $filters, array $selection, string $operation, array $options = array()): array
    {
        $batch_size_raw = $options['batch_size'] ?? null;
        $limit = is_string($batch_size_raw) && strtolower($batch_size_raw) === 'all'
            ? 0
            : max(0, intval($batch_size_raw));
        $entity_keys = $this->resolve_batch_entity_keys($project_id, $entity_group, $filters, $selection, $operation, $limit);
        $results = array(
            'selected' => count($entity_keys),
            'attempted_count' => count($entity_keys),
            'eligible' => 0,
            'applied' => 0,
            'applied_count' => 0,
            'failed' => 0,
            'failed_count' => 0,
            'skipped' => 0,
            'skipped_count' => 0,
            'remaining_eligible_count' => 0,
            'items' => array(),
        );

        foreach ($entity_keys as $entity_key) {
            $detail = $this->get_detail($project_id, $entity_key);
            $inventory = isset($detail['inventory']) && is_array($detail['inventory']) ? $detail['inventory'] : array();
            if (!$this->row_matches_operation($inventory, $operation)) {
                $results['skipped']++;
                continue;
            }

            $results['eligible']++;
            $item_result = match ($operation) {
                'apply_meta' => $this->apply_meta_output($project_id, $entity_key),
                'apply_rewrite' => $this->apply_rewrite_output($project_id, $entity_key),
                'skip' => $this->skip_entity($project_id, $entity_key),
                default => array('ok' => false, 'error' => 'unsupported_batch_operation'),
            };
            $results['items'][] = $item_result;
            if (!empty($item_result['ok'])) {
                $results['applied']++;
                $results['applied_count']++;
            } else {
                $results['failed']++;
                $results['failed_count']++;
            }
        }
        $results['skipped_count'] = $results['skipped'];
        $results['remaining_eligible_count'] = count($this->resolve_batch_entity_keys($project_id, $entity_group, $filters, array(), $operation, 0));

        return array('ok' => true) + $results;
    }

    private function ensure_seeded(int $project_id, bool $refresh): void
    {
        global $wpdb;
        if ($project_id <= 0) {
            return;
        }

        $count = intval($wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->inventory_table} WHERE project_id = %d", $project_id)));
        if (!$refresh && $count > 0) {
            return;
        }

        $rows = $this->build_projection_rows($project_id);
        $wpdb->delete($this->inventory_table, array('project_id' => $project_id));
        foreach ($rows as $row) {
            $this->upsert_inventory($project_id, $row);
        }
    }

    private function build_projection_rows(int $project_id): array
    {
        $entities = $this->collect_entities($project_id);
        $rows = array();
        foreach ($entities as $entity) {
            $rows[] = $this->build_projection_row($project_id, $entity);
        }
        $rows = $this->apply_keyword_ownership_rules($rows);

        usort($rows, static function (array $a, array $b): int {
            $groupCmp = strcmp((string) ($a['entity_group'] ?? ''), (string) ($b['entity_group'] ?? ''));
            if ($groupCmp !== 0) {
                return $groupCmp;
            }
            return strcasecmp((string) ($a['title'] ?? ''), (string) ($b['title'] ?? ''));
        });

        return $rows;
    }

    private function collect_entities(int $project_id): array
    {
        $keyword_map = $this->build_keyword_map($project_id);
        $entities = array();

        $post_types = get_post_types(array('public' => true), 'objects');
        foreach ($post_types as $post_type => $object) {
            if (!$object || in_array($post_type, array('attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache'), true)) {
                continue;
            }
            $posts = get_posts(array(
                'post_type' => $post_type,
                'post_status' => array('publish', 'future', 'draft', 'pending'),
                'posts_per_page' => -1,
                'orderby' => 'ID',
                'order' => 'ASC',
                'fields' => 'ids',
                'suppress_filters' => true,
                'update_post_meta_cache' => false,
                'update_post_term_cache' => false,
            ));
            foreach ($posts as $post_id) {
                $post = get_post($post_id);
                if (!$post instanceof WP_Post) {
                    continue;
                }
                $entity_key = 'post:' . intval($post_id);
                $fallback_keyword = trim((string) ($keyword_map[$entity_key]['main_keyword'] ?? ''));
                if ($fallback_keyword === '') {
                    $fallback_keyword = $this->get_entity_saved_keyword('post', intval($post_id));
                }
                $url = (string) get_permalink($post_id);
                if ($this->should_exclude_public_url($url)) {
                    continue;
                }
                $entities[$entity_key] = array(
                    'entity_key' => $entity_key,
                    'entity_id' => intval($post_id),
                    'object_kind' => 'post',
                    'post_type' => $post_type,
                    'taxonomy' => '',
                    'title' => $this->normalize_title(get_the_title($post), 'Post #' . intval($post_id)),
                    'url' => $url,
                    'status' => (string) $post->post_status,
                    'main_keyword' => $fallback_keyword,
                    'keywords' => $keyword_map[$entity_key]['keywords'] ?? array(),
                );
            }
        }

        $taxonomies = get_taxonomies(array('public' => true), 'objects');
        foreach ($taxonomies as $taxonomy => $object) {
            if (!$object || in_array($taxonomy, array('nav_menu', 'link_category', 'post_format'), true)) {
                continue;
            }
            $terms = get_terms(array(
                'taxonomy' => $taxonomy,
                'hide_empty' => false,
            ));
            if (!is_array($terms) || is_wp_error($terms)) {
                continue;
            }
            foreach ($terms as $term) {
                if (!$term instanceof WP_Term) {
                    continue;
                }
                $entity_key = 'term:' . $taxonomy . ':' . intval($term->term_id);
                $fallback_keyword = trim((string) ($keyword_map[$entity_key]['main_keyword'] ?? ''));
                if ($fallback_keyword === '') {
                    $fallback_keyword = $this->get_entity_saved_keyword('term', intval($term->term_id));
                }
                $url = $this->safe_term_link($term);
                if ($this->should_exclude_public_url($url)) {
                    continue;
                }
                $entities[$entity_key] = array(
                    'entity_key' => $entity_key,
                    'entity_id' => intval($term->term_id),
                    'object_kind' => 'term',
                    'post_type' => '',
                    'taxonomy' => $taxonomy,
                    'title' => $this->normalize_title($term->name, 'Term #' . intval($term->term_id)),
                    'url' => $url,
                    'status' => 'publish',
                    'main_keyword' => $fallback_keyword,
                    'keywords' => $keyword_map[$entity_key]['keywords'] ?? array(),
                );
            }
        }

        return array_values($entities);
    }

    private function get_entity_saved_keyword(string $object_kind, int $entity_id): string
    {
        if ($entity_id <= 0) {
            return '';
        }

        $keyword_data = $object_kind === 'term'
            ? get_term_meta($entity_id, 'aysa_keyword_data', true)
            : get_post_meta($entity_id, 'aysa_keyword_data', true);
        if (!is_array($keyword_data)) {
            return '';
        }

        $candidates = array(
            trim((string) ($keyword_data['keyword'] ?? '')),
            trim((string) ($keyword_data['aysa_keyword'] ?? '')),
        );
        foreach ($candidates as $candidate) {
            if ($candidate !== '') {
                return $candidate;
            }
        }

        return '';
    }

    private function build_keyword_map(int $project_id): array
    {
        $option_key = 'aysa_keyword_discovery_local_associations_' . intval($project_id);
        $rows = get_option($option_key, array());
        if (!is_array($rows)) {
            return array();
        }

        $mapped = array();
        $keyword_owners = array();
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $keyword = trim((string) ($row['keyword'] ?? ''));
            $associated_on = trim((string) ($row['associatedOn'] ?? $row['associated_on'] ?? ''));
            $id_intern = intval($row['idIntern'] ?? $row['id_intern'] ?? 0);
            if ($keyword === '' || $associated_on === '' || $id_intern <= 0) {
                continue;
            }

            $entity_key = $this->resolve_entity_key_from_association($associated_on, $id_intern);
            if ($entity_key === '') {
                continue;
            }

            $normalized_keyword = $this->normalize_keyword($keyword);
            if ($normalized_keyword !== '') {
                if (isset($keyword_owners[$normalized_keyword]) && $keyword_owners[$normalized_keyword] !== $entity_key) {
                    continue;
                }
                $keyword_owners[$normalized_keyword] = $entity_key;
            }

            if (!isset($mapped[$entity_key])) {
                $mapped[$entity_key] = array(
                    'main_keyword' => '',
                    'keywords' => array(),
                );
            }
            $is_main = !empty($row['isMain']) || !empty($row['is_main']) || !empty($row['IsMain']);
            $execution_primary = trim((string) ($row['executionPrimaryKeyword'] ?? $row['execution_primary_keyword'] ?? ''));
            if ($is_main || ($execution_primary !== '' && strcasecmp($execution_primary, $keyword) === 0)) {
                $mapped[$entity_key]['main_keyword'] = $keyword;
                continue;
            }
            if (strcasecmp((string) ($mapped[$entity_key]['main_keyword'] ?? ''), $keyword) !== 0) {
                $mapped[$entity_key]['keywords'][] = $keyword;
            }
        }

        foreach ($mapped as $entity_key => $data) {
            $keywords = array_values(array_unique(array_filter(array_map('trim', (array) ($data['keywords'] ?? array())))));
            $main_keyword = trim((string) ($data['main_keyword'] ?? ''));
            $mapped[$entity_key]['keywords'] = $keywords;
            $mapped[$entity_key]['main_keyword'] = $main_keyword;
        }

        return $mapped;
    }

    private function resolve_entity_key_from_association(string $associated_on, int $id_intern): string
    {
        $associated_on = strtolower(trim($associated_on));
        if ($id_intern <= 0) {
            return '';
        }

        if (in_array($associated_on, array('product', 'page', 'post', 'article'), true)) {
            return 'post:' . $id_intern;
        }

        if (in_array($associated_on, array('category', 'term', 'product_cat', 'product_tag', 'tag'), true)) {
            $term = get_term($id_intern);
            if ($term && !is_wp_error($term) && !empty($term->taxonomy)) {
                return 'term:' . $term->taxonomy . ':' . $id_intern;
            }
        }

        $post = get_post($id_intern);
        if ($post instanceof WP_Post) {
            return 'post:' . $id_intern;
        }

        $term = get_term($id_intern);
        if ($term && !is_wp_error($term) && !empty($term->taxonomy)) {
            return 'term:' . $term->taxonomy . ':' . $id_intern;
        }

        return '';
    }

    private function build_projection_row(int $project_id, array $entity): array
    {
        $entity_id = intval($entity['entity_id'] ?? 0);
        $entity_key = (string) ($entity['entity_key'] ?? '');
        $object_kind = (string) ($entity['object_kind'] ?? 'post');
        $post_type = (string) ($entity['post_type'] ?? '');
        $taxonomy = (string) ($entity['taxonomy'] ?? '');
        $history = $this->get_current_history_record($entity_id, $object_kind, $post_type, $taxonomy);
        $quality = $this->get_cached_quality($object_kind, $entity_id);
        $generation_signals = $this->get_entity_aysa_generation_signals($object_kind, $entity_id);
        $existing_inventory = $this->get_existing_inventory_row($project_id, $entity_key);

        $entity_group = $this->resolve_entity_group($object_kind, $post_type, $taxonomy);
        $publication_state = $this->resolve_publication_state((string) ($entity['status'] ?? 'publish'));
        $main_keyword = trim((string) ($entity['main_keyword'] ?? ''));
        $mapping_state = $main_keyword === '' ? 'needs_mapping' : 'mapped';
        $history_applied = $this->is_history_applied($history);
        $optimization_scope = $this->resolve_optimization_scope($history, $quality, $history_applied);
        $meta_state = $this->resolve_meta_state($history, $history_applied);
        $rewrite_state = $this->resolve_rewrite_state($entity_group, $optimization_scope, $history_applied, $mapping_state);
        $output_state = $this->resolve_output_state($optimization_scope, $history_applied, $rewrite_state);
        $seo_state = $output_state === 'none' ? 'not_optimized' : 'optimized';
        $confidence_score = $this->resolve_confidence_score($mapping_state, $optimization_scope, $history_applied);
        $decision_reason = $this->resolve_decision_reason($mapping_state, $seo_state, $optimization_scope, $entity_group, $publication_state);
        $next_step = $this->resolve_next_step($seo_state, $optimization_scope, $publication_state, $main_keyword, $mapping_state, $rewrite_state, $meta_state);
        $comparison_result = null;
        $keyword_owner_entity_key = null;
        $keyword_owner_url = null;
        $keyword_conflict_keyword = null;
        $keyword_conflict_reason = null;
        $meta_package_json = null;
        $rewrite_decision_json = null;
        $rewrite_package_json = null;
        $worker_run_id = 0;
        $content_source = (string) ($quality['content_source'] ?? '');
        $existing_meta_package = is_array($existing_inventory) ? $this->nullable_string($existing_inventory['meta_package_json'] ?? null) : null;
        $current_meta_title = trim((string) ($history['meta_title'] ?? ''));
        $current_meta_description = trim((string) ($history['meta_description'] ?? ''));

        if ($entity_id > 0 && $object_kind !== 'post') {
            if ($current_meta_title === '') {
                $current_meta_title = trim((string) get_term_meta($entity_id, '_aysa_seo_title', true));
            }
            if ($current_meta_title === '') {
                $current_meta_title = trim((string) get_term_meta($entity_id, '_aysa_generated_meta_title', true));
            }
            if ($current_meta_description === '') {
                $current_meta_description = trim((string) get_term_meta($entity_id, '_aysa_seo_description', true));
            }
            if ($current_meta_description === '') {
                $current_meta_description = trim((string) get_term_meta($entity_id, '_aysa_generated_meta_description', true));
            }
        } elseif ($entity_id > 0) {
            if ($current_meta_title === '') {
                $current_meta_title = trim((string) get_post_meta($entity_id, '_aysa_seo_title', true));
            }
            if ($current_meta_title === '') {
                $current_meta_title = trim((string) get_post_meta($entity_id, '_aysa_generated_meta_title', true));
            }
            if ($current_meta_description === '') {
                $current_meta_description = trim((string) get_post_meta($entity_id, '_aysa_seo_description', true));
            }
            if ($current_meta_description === '') {
                $current_meta_description = trim((string) get_post_meta($entity_id, '_aysa_generated_meta_description', true));
            }
        }

        if ($history_applied && $existing_meta_package !== null) {
            $meta_package_json = $existing_meta_package;
        }

        if (is_array($existing_inventory) && (string) ($existing_inventory['mapping_state'] ?? '') === 'mapping_review') {
            $existing_review_keyword = trim((string) ($existing_inventory['main_keyword'] ?? ''));
            if ($existing_review_keyword !== '') {
                $main_keyword = $existing_review_keyword;
                $mapping_state = 'mapping_review';
                $confidence_score = isset($existing_inventory['confidence_score']) && is_numeric($existing_inventory['confidence_score'])
                    ? round((float) $existing_inventory['confidence_score'], 2)
                    : $confidence_score;
                $decision_reason = trim((string) ($existing_inventory['decision_reason'] ?? '')) !== ''
                    ? (string) $existing_inventory['decision_reason']
                    : $decision_reason;
                $next_step = trim((string) ($existing_inventory['next_step'] ?? '')) !== ''
                    ? (string) $existing_inventory['next_step']
                    : 'Review suggested keyword';
                $comparison_result = $this->nullable_string($existing_inventory['comparison_result'] ?? null);
                $keyword_owner_entity_key = $this->nullable_string($existing_inventory['keyword_owner_entity_key'] ?? null);
                $keyword_owner_url = $this->nullable_string($existing_inventory['keyword_owner_url'] ?? null);
                $keyword_conflict_keyword = $this->nullable_string($existing_inventory['keyword_conflict_keyword'] ?? null);
                $keyword_conflict_reason = $this->nullable_string($existing_inventory['keyword_conflict_reason'] ?? null);
            }
        } elseif (is_array($existing_inventory) && !$history_applied) {
            $existingMetaState = sanitize_key((string) ($existing_inventory['meta_state'] ?? 'none'));
            $existingRewriteState = sanitize_key((string) ($existing_inventory['rewrite_state'] ?? 'not_needed'));
            $existingRewriteDecision = $this->nullable_string($existing_inventory['rewrite_decision_json'] ?? null);
            $existingRewritePackage = $this->nullable_string($existing_inventory['rewrite_package_json'] ?? null);

            if ($existingMetaState === 'generated' && $existing_meta_package !== null) {
                $meta_state = 'generated';
                if ($optimization_scope === 'none') {
                    $optimization_scope = 'meta_only';
                }
                $decision_reason = $this->nullable_string($existing_inventory['decision_reason'] ?? null) ?? $decision_reason;
                $next_step = 'Review generated meta';
                $meta_package_json = $existing_meta_package;
                $worker_run_id = intval($existing_inventory['worker_run_id'] ?? 0);
                $content_source = 'worker_meta_generated';
            }

            if (in_array($existingRewriteState, array('recommended', 'review_required', 'generated'), true)) {
                $rewrite_state = $existingRewriteState;
                if ($existingRewriteState !== 'not_needed') {
                    $optimization_scope = 'content_rewrite';
                }
                if ($existingRewriteState === 'recommended') {
                    $next_step = 'Generate content rewrite';
                } elseif (in_array($existingRewriteState, array('review_required', 'generated'), true)) {
                    $next_step = 'Review content rewrite';
                    $output_state = 'review_ready';
                }
                $decision_reason = $this->nullable_string($existing_inventory['decision_reason'] ?? null) ?? $decision_reason;
                $rewrite_decision_json = $existingRewriteDecision;
                $rewrite_package_json = $existingRewritePackage;
                $worker_run_id = intval($existing_inventory['worker_run_id'] ?? 0);
                $content_source = $existingRewritePackage !== null ? 'worker_rewrite_generated' : 'worker_rewrite_decision';
            }
        }

        $has_existing_aysa_content = $this->has_existing_aysa_generated_content(
            $history,
            $history_applied,
            $optimization_scope,
            $content_source,
            $generation_signals
        );
        $worker_rewrite_pending = !$history_applied && in_array($rewrite_state, array('recommended', 'review_required', 'generated'), true);
        if ($has_existing_aysa_content && !$worker_rewrite_pending) {
            $optimization_scope = 'content_rewrite';
            $rewrite_state = 'applied';
            $output_state = 'rewrite_applied';
            $seo_state = 'optimized';
            $confidence_score = 0.96;
            $decision_reason = $history_applied
                ? 'Applied AYSA rewrite exists in SEO history.'
                : 'Existing AYSA-generated content already covers this entity.';
            $next_step = 'Already optimized';
            $content_source = 'aysa_generated';
        }

        return array(
            'entity_key' => $entity_key,
            'entity_id' => $entity_id,
            'object_kind' => $object_kind,
            'post_type' => $post_type,
            'taxonomy' => $taxonomy,
            'url' => (string) ($entity['url'] ?? ''),
            'title' => (string) ($entity['title'] ?? ''),
            'entity_group' => $entity_group,
            'publication_state' => $publication_state,
            'mapping_state' => $mapping_state,
            'seo_state' => $seo_state,
            'optimization_scope' => $optimization_scope,
            'meta_state' => $meta_state,
            'rewrite_state' => $rewrite_state,
            'output_state' => $output_state,
            'main_keyword' => $main_keyword,
            'confidence_score' => $confidence_score,
            'comparison_result' => $comparison_result,
            'keyword_owner_entity_key' => $keyword_owner_entity_key,
            'keyword_owner_url' => $keyword_owner_url,
            'keyword_conflict_keyword' => $keyword_conflict_keyword,
            'keyword_conflict_reason' => $keyword_conflict_reason,
            'decision_reason' => $decision_reason,
            'next_step' => $next_step,
            'seo_score' => isset($quality['seo_score']) && is_numeric($quality['seo_score']) ? intval($quality['seo_score']) : null,
            'content_source' => $content_source,
            'current_meta_title' => $current_meta_title,
            'current_meta_description' => $current_meta_description,
            'applied_meta_title' => $history_applied ? $current_meta_title : '',
            'applied_meta_description' => $history_applied ? $current_meta_description : '',
            'meta_package_json' => $meta_package_json,
            'rewrite_decision_json' => $rewrite_decision_json,
            'rewrite_package_json' => $rewrite_package_json,
            'worker_run_id' => $worker_run_id,
            'last_evaluated_at' => current_time('mysql'),
        );
    }

    private function get_existing_inventory_row(int $project_id, string $entity_key): array
    {
        global $wpdb;
        if ($project_id <= 0 || $entity_key === '') {
            return array();
        }

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$this->inventory_table} WHERE project_id = %d AND entity_key = %s LIMIT 1", $project_id, $entity_key),
            ARRAY_A
        );

        return is_array($row) ? $row : array();
    }

    private function get_current_history_record(int $entity_id, string $object_kind, string $post_type, string $taxonomy): array
    {
        global $wpdb;
        if ($entity_id <= 0) {
            return array();
        }

        $history_table = $wpdb->prefix . 'aysa_seo_history';
        $type_candidates = $this->history_entity_type_candidates($object_kind, $post_type, $taxonomy);
        if (empty($type_candidates)) {
            return array();
        }

        $placeholders = implode(',', array_fill(0, count($type_candidates), '%s'));
        $params = array_merge(array($entity_id), $type_candidates);
        $sql = $wpdb->prepare(
            "SELECT * FROM {$history_table} WHERE entity_id = %d AND entity_type IN ({$placeholders}) AND is_current_version = 1 ORDER BY is_applied DESC, id DESC LIMIT 1",
            ...$params
        );
        $row = $wpdb->get_row($sql, ARRAY_A);

        return is_array($row) ? $row : array();
    }

    private function is_history_applied(array $history): bool
    {
        return !empty($history) && intval($history['is_applied'] ?? 0) === 1;
    }

    private function build_entity_run_context(array $inventory, array $history, array $keywords): array
    {
        $entity_key = (string) ($inventory['entity_key'] ?? '');
        $entity_id = intval($inventory['entity_id'] ?? 0);
        $object_kind = (string) ($inventory['object_kind'] ?? 'post');
        $post_type = (string) ($inventory['post_type'] ?? '');
        $taxonomy = (string) ($inventory['taxonomy'] ?? '');
        if ($entity_key === '' || $entity_id <= 0) {
            return array();
        }

        $quality = $this->get_cached_quality($object_kind, $entity_id);
        $generation_signals = $this->get_entity_aysa_generation_signals($object_kind, $entity_id);
        $history_applied = $this->is_history_applied($history);

        $main_keyword = trim((string) ($inventory['main_keyword'] ?? ''));
        if ($main_keyword === '' && !empty($keywords[0])) {
            $main_keyword = trim((string) $keywords[0]);
        }

        $title = trim((string) ($inventory['title'] ?? ''));
        $url = trim((string) ($inventory['url'] ?? ''));
        $entity_group = sanitize_key((string) ($inventory['entity_group'] ?? 'other'));
        $current_meta_title = trim((string) ($history['meta_title'] ?? ''));
        $current_meta_description = trim((string) ($history['meta_description'] ?? ''));
        $current_h1 = $title;
        $content_excerpt = '';
        $content_snippet = '';
        $current_content_html = '';
        $current_content_text = '';

        if ($object_kind === 'post') {
            $post = get_post($entity_id);
            if ($post instanceof WP_Post) {
                $current_h1 = $this->normalize_title(get_the_title($post), $title !== '' ? $title : ('Post #' . $entity_id));
                $current_content_html = trim((string) $post->post_content);
                $current_content_text = wp_strip_all_tags((string) $post->post_content);
                $content_excerpt = trim((string) $post->post_excerpt);
                if ($content_excerpt === '') {
                    $content_excerpt = wp_trim_words(wp_strip_all_tags((string) $post->post_content), 40, '...');
                }
                $content_snippet = wp_trim_words(wp_strip_all_tags((string) $post->post_content), 120, '...');
                if ($current_meta_title === '') {
                    $current_meta_title = trim((string) get_post_meta($entity_id, '_aysa_seo_title', true));
                }
                if ($current_meta_description === '') {
                    $current_meta_description = trim((string) get_post_meta($entity_id, '_aysa_seo_description', true));
                }
            }
        } else {
            $term = get_term($entity_id);
            if ($term && !is_wp_error($term)) {
                $current_h1 = $this->normalize_title((string) $term->name, $title !== '' ? $title : ('Term #' . $entity_id));
                $current_content_html = trim((string) $term->description);
                $current_content_text = wp_strip_all_tags((string) $term->description);
                $content_excerpt = wp_trim_words(wp_strip_all_tags((string) $term->description), 40, '...');
                $content_snippet = wp_trim_words(wp_strip_all_tags((string) $term->description), 120, '...');
                if ($current_meta_title === '') {
                    $current_meta_title = trim((string) get_term_meta($entity_id, '_aysa_seo_title', true));
                }
                if ($current_meta_description === '') {
                    $current_meta_description = trim((string) get_term_meta($entity_id, '_aysa_seo_description', true));
                }
            }
        }

        $secondary_keywords = array_values(array_filter(array_map('strval', $keywords), static function (string $keyword) use ($main_keyword): bool {
            return $keyword !== '' && strcasecmp($keyword, $main_keyword) !== 0;
        }));
        $profile_context = $this->resolve_generation_profile_context();
        $commerce_context = $this->resolve_generation_commerce_context($object_kind, $entity_id, $taxonomy);

        return array(
            'entity_key' => $entity_key,
            'entity_id' => $entity_id,
            'object_kind' => $object_kind,
            'post_type' => $post_type,
            'taxonomy' => $taxonomy,
            'entity_group' => $entity_group,
            'url' => $url,
            'title' => $title,
            'current_h1' => $current_h1,
            'content_excerpt' => $content_excerpt,
            'content_snippet' => $content_snippet,
            'current_content_text' => mb_substr($current_content_text, 0, 6000),
            'publication_state' => (string) ($inventory['publication_state'] ?? 'published'),
            'mapping_state' => (string) ($inventory['mapping_state'] ?? ($main_keyword !== '' ? 'mapped' : 'needs_mapping')),
            'seo_state' => (string) ($inventory['seo_state'] ?? 'not_optimized'),
            'optimization_scope' => (string) ($inventory['optimization_scope'] ?? 'none'),
            'meta_state' => (string) ($inventory['meta_state'] ?? 'none'),
            'rewrite_state' => (string) ($inventory['rewrite_state'] ?? 'not_needed'),
            'output_state' => (string) ($inventory['output_state'] ?? 'none'),
            'history_applied' => $history_applied,
            'content_source' => (string) ($quality['content_source'] ?? ''),
            'has_aysa_content_signal' => !empty($generation_signals['has_content_signal']),
            'has_aysa_meta_signal' => !empty($generation_signals['has_meta_signal']),
            'meta_package_json' => $this->decode_inventory_json($inventory['meta_package_json'] ?? null),
            'rewrite_decision_json' => $this->decode_inventory_json($inventory['rewrite_decision_json'] ?? null),
            'rewrite_package_json' => $this->decode_inventory_json($inventory['rewrite_package_json'] ?? null),
            'main_keyword' => $main_keyword,
            'keywords' => $keywords,
            'secondary_keywords' => $secondary_keywords,
            'language' => $this->resolve_short_language_code(),
            'country_code' => $this->resolve_target_country_code(),
            'brand_name' => $profile_context['brand_name'],
            'company_name' => $profile_context['company_name'],
            'industry' => $profile_context['industry'],
            'target_country' => $profile_context['target_country'],
            'target_region' => $profile_context['target_region'],
            'target_city' => $profile_context['target_city'],
            'tone_of_voice' => $profile_context['tone_of_voice'],
            'business_differentiators' => $profile_context['business_differentiators'],
            'trust_signals' => $profile_context['trust_signals'],
            'allowed_claims' => $profile_context['allowed_claims'],
            'general_details' => isset($profile_context['general_details']) && is_array($profile_context['general_details']) ? $profile_context['general_details'] : array(),
            'generation_context' => isset($profile_context['generation_context']) && is_array($profile_context['generation_context']) ? $profile_context['generation_context'] : array(),
            'seo_profile' => $profile_context['seo_profile'],
            'rag_context' => $profile_context['rag_context'],
            'site_host' => (string) wp_parse_url(home_url('/'), PHP_URL_HOST),
            'page_type' => $this->resolve_page_type_for_generation($entity_group, $post_type, $taxonomy),
            'current_slug' => $this->extract_generation_slug($url),
            'current_title' => $title,
            'current_h1' => $current_h1,
            'current_meta_title' => $current_meta_title,
            'current_meta_description' => $current_meta_description,
            'content_excerpt' => $content_excerpt,
            'content_snippet' => $content_snippet,
            'current_content_html' => $this->truncate_generation_text($current_content_html, 12000),
            'current_content_text' => $this->truncate_generation_text($current_content_text, 8000),
            'category_data' => $commerce_context['category_data'],
            'product_data' => $commerce_context['product_data'],
        );
    }

    private function resolve_page_type_for_generation(string $entity_group, string $post_type, string $taxonomy): string
    {
        return match ($entity_group) {
            'product' => 'product',
            'ecommerce_category' => 'category',
            'article' => 'article',
            'page' => 'landing_page',
            default => $taxonomy !== '' ? 'category' : ($post_type === 'post' ? 'article' : ($post_type === 'product' ? 'product' : 'landing_page')),
        };
    }

    private function resolve_generation_profile_context(): array
    {
        $user_id = get_current_user_id();
        $general = $user_id > 0 ? get_option('aysa_wizard_general_' . intval($user_id), array()) : array();
        $objectives = $user_id > 0 ? get_option('aysa_wizard_objectives_' . intval($user_id), array()) : array();
        $seo_settings = $user_id > 0 ? get_option('aysa_wizard_seo-settings_' . intval($user_id), array()) : array();
        $tone_settings = $user_id > 0 ? get_option('aysa_wizard_tone-of-voice_' . intval($user_id), array()) : array();
        $company_name = trim((string) get_option('aysa_company_name', is_array($general) ? ($general['company_name'] ?? get_bloginfo('name')) : get_bloginfo('name')));
        $brand_name = trim((string) (is_array($general) ? ($general['brand_name'] ?? '') : ''));
        $industry = trim((string) get_option('aysa_industry', is_array($general) ? ($general['industry'] ?? '') : ''));
        $company_description = trim((string) (is_array($general) ? ($general['company_description'] ?? '') : ''));
        $website_type = trim((string) (is_array($general) ? ($general['website_type'] ?? '') : ''));
        $company_info = trim((string) (is_array($general) ? ($general['company_info'] ?? '') : ''));
        $tone = trim((string) get_option('aysa_tone_of_voice', is_array($tone_settings) ? ($tone_settings['tone_of_voice'] ?? '') : ''));
        $target_country = is_array($seo_settings) ? trim((string) ($seo_settings['targeted_country'] ?? '')) : '';
        $target_region = is_array($seo_settings) ? trim((string) ($seo_settings['targeted_region'] ?? '')) : '';
        $target_city = is_array($seo_settings) ? trim((string) ($seo_settings['targeted_city'] ?? '')) : '';
        $primary_language = is_array($seo_settings) ? trim((string) ($seo_settings['primary_language'] ?? '')) : '';
        $custom_objectives = is_array($objectives) ? trim((string) ($objectives['custom_objectives'] ?? '')) : '';
        $objective_list = is_array($objectives) && isset($objectives['objectives']) && is_array($objectives['objectives'])
            ? array_values(array_filter(array_map('strval', $objectives['objectives'])))
            : array();
        $website_url = home_url('/');
        $website_name = trim((string) get_bloginfo('name'));
        $project_id = intval(get_option('aysa_project_id', 0));
        $monitoring_rag = $project_id > 0 ? get_option('aysa_monitoring_client_rag_context_' . $project_id, array()) : array();
        if (!is_array($monitoring_rag)) {
            $monitoring_rag = array();
        }
        $preferredBrand = $brand_name !== '' ? $brand_name : ($website_name !== '' ? $website_name : $company_name);
        $claims = array_values(array_filter(array_unique(array_map('strval', [
            $company_name,
            $brand_name,
            $website_name,
            $industry,
            $target_city !== '' ? 'Target city: ' . $target_city : '',
            $target_region !== '' ? 'Target region: ' . $target_region : '',
            $target_country !== '' ? 'Target country: ' . $target_country : '',
            $website_url !== '' ? 'Website: ' . $website_url : '',
        ]))));

        $seo_profile = array(
            'audience_profile' => $website_type !== '' ? ('Audience for ' . $website_type . ' visitors') : '',
            'audience_awareness_level' => 'mixed',
            'brand_positioning' => $company_description !== '' ? $company_description : $company_info,
            'tone_of_voice' => $tone,
            'market' => $industry,
            'language_register' => $primary_language !== '' ? $primary_language : '',
            'topical_authority' => array_values(array_filter(array_unique(array_filter(array($industry, $website_type))))),
            'business_model' => $website_type,
            'primary_conversion_goal' => isset($objective_list[0]) ? (string) $objective_list[0] : '',
            'secondary_conversion_goal' => isset($objective_list[1]) ? (string) $objective_list[1] : '',
            'brand_constraints' => array(),
            'forbidden_claims' => array(),
            'geo_scope' => trim(implode(' / ', array_filter(array($target_city, $target_region, $target_country)))),
            'must_include_if_relevant' => array_values(array_filter(array(
                $target_city !== '' ? 'livrare în ' . $target_city : '',
                $target_region !== '' ? 'relevanță pentru ' . $target_region : '',
            ))),
            'must_avoid_topics' => array(),
            'preferred_entities' => array_values(array_filter(array($preferredBrand, $company_name, $website_name))),
            'disallowed_phrases' => array(),
        );

        return array(
            'brand_name' => $preferredBrand,
            'company_name' => $company_name !== '' ? $company_name : $website_name,
            'industry' => $industry,
            'target_country' => $target_country,
            'target_region' => $target_region,
            'target_city' => $target_city,
            'tone_of_voice' => $tone,
            'business_differentiators' => array_values(array_filter(array(
                $company_description,
                $company_info,
                $custom_objectives,
            ))),
            'trust_signals' => array_values(array_filter(array_map('strval', [
                $website_url !== '' ? 'Official website available' : '',
                $company_name !== '' ? 'Brand identified in setup profile' : '',
            ]))),
            'allowed_claims' => $claims,
            'general_details' => array(
                'brand_name' => $preferredBrand,
                'company_name' => $company_name,
                'company_description' => $company_description,
                'industry' => $industry,
                'website_type' => $website_type,
                'objectives' => $objective_list,
                'custom_objectives' => $custom_objectives,
                'primary_language' => $primary_language,
                'company_info' => $company_info,
            ),
            'generation_context' => array(
                'missing_profile_fields' => array_values(array_filter(array(
                    $company_description === '' ? 'company_description' : '',
                    $website_type === '' ? 'website_type' : '',
                    $tone === '' ? 'tone_of_voice' : '',
                    $target_country === '' ? 'targeted_country' : '',
                ))),
                'source' => 'setup_seo_profile',
                'monitoring_context' => $monitoring_rag,
            ),
            'seo_profile' => $seo_profile,
            'rag_context' => array(
                'approved_facts' => $claims,
                'differentiators' => array_values(array_filter(array($company_description, $company_info))),
                'business_context' => array_values(array_filter(array_map('strval', [
                    $industry !== '' ? 'Industry: ' . $industry : '',
                    $target_city !== '' ? 'Target city: ' . $target_city : '',
                    $target_region !== '' ? 'Target region: ' . $target_region : '',
                    $target_country !== '' ? 'Target country: ' . $target_country : '',
                ]))),
                'customer_objections' => array(),
                'limitations' => array(),
                'low_confidence_context' => array(),
                'do_not_use' => array(),
                'monitoring_context' => $monitoring_rag,
            ),
        );
    }

    private function extract_generation_slug(string $url): string
    {
        $path = trim((string) wp_parse_url($url, PHP_URL_PATH));
        if ($path === '') {
            return '';
        }
        return trim((string) basename(rtrim($path, '/')));
    }

    private function resolve_generation_commerce_context(string $object_kind, int $entity_id, string $taxonomy): array
    {
        $product_data = null;
        $category_data = null;

        if ($object_kind === 'post' && get_post_type($entity_id) === 'product') {
            $product_data = array(
                'sku' => trim((string) get_post_meta($entity_id, '_sku', true)),
                'price' => trim((string) get_post_meta($entity_id, '_price', true)),
                'regular_price' => trim((string) get_post_meta($entity_id, '_regular_price', true)),
                'sale_price' => trim((string) get_post_meta($entity_id, '_sale_price', true)),
                'short_description' => trim((string) get_post_field('post_excerpt', $entity_id)),
            );
            $product_data = array_filter($product_data, static function ($value) {
                return $value !== '';
            });
            if ($product_data === array()) {
                $product_data = null;
            }
        }

        if ($object_kind !== 'post' && $taxonomy !== '') {
            $term = get_term($entity_id, $taxonomy);
            if ($term && !is_wp_error($term)) {
                $category_data = array(
                    'taxonomy' => $taxonomy,
                    'name' => (string) $term->name,
                    'slug' => (string) $term->slug,
                    'count' => (int) $term->count,
                    'description' => trim((string) $term->description),
                );
            }
        }

        return array(
            'product_data' => $product_data,
            'category_data' => $category_data,
        );
    }

    private function resolve_short_language_code(): string
    {
        $locale = get_locale();
        if (!is_string($locale) || $locale === '') {
            return 'ro';
        }
        $locale = str_replace('-', '_', strtolower($locale));
        $parts = explode('_', $locale);
        $lang = trim((string) ($parts[0] ?? 'ro'));
        return $lang !== '' ? $lang : 'ro';
    }

    private function resolve_target_country_code(): string
    {
        $user_id = get_current_user_id();
        $seo_settings = $user_id > 0 ? get_option('aysa_wizard_seo-settings_' . intval($user_id), array()) : array();
        $country = is_array($seo_settings) ? strtolower(trim((string) ($seo_settings['targeted_country'] ?? ''))) : '';
        return match ($country) {
            'romania', 'ro' => 'ro',
            'bulgaria', 'bg' => 'bg',
            default => 'ro',
        };
    }

    private function history_entity_type_candidates(string $object_kind, string $post_type, string $taxonomy): array
    {
        $candidates = array();
        if ($object_kind === 'post') {
            if ($post_type !== '') {
                $candidates[] = $post_type;
            }
            if ($post_type === 'post') {
                $candidates[] = 'Post';
                $candidates[] = 'Article';
            } elseif ($post_type === 'page') {
                $candidates[] = 'Page';
            } elseif ($post_type === 'product') {
                $candidates[] = 'Product';
            }
        } else {
            if ($taxonomy !== '') {
                $candidates[] = $taxonomy;
            }
            if ($taxonomy === 'product_cat') {
                $candidates[] = 'Category';
            } elseif ($taxonomy === 'category') {
                $candidates[] = 'Article Category';
                $candidates[] = 'Category';
            } else {
                $candidates[] = 'Term';
            }
        }

        return array_values(array_unique(array_filter(array_map('strval', $candidates))));
    }

    private function get_cached_quality(string $object_kind, int $entity_id): array
    {
        if ($object_kind === 'term') {
            return array(
                'seo_score' => get_term_meta($entity_id, '_aysa_entity_seo_score_cache', true),
                'content_source' => get_term_meta($entity_id, '_aysa_entity_content_source_cache', true),
            );
        }

        return array(
            'seo_score' => get_post_meta($entity_id, '_aysa_entity_seo_score_cache', true),
            'content_source' => get_post_meta($entity_id, '_aysa_entity_content_source_cache', true),
        );
    }

    private function get_entity_aysa_generation_signals(string $object_kind, int $entity_id): array
    {
        if ($entity_id <= 0) {
            return array(
                'has_content_signal' => false,
                'has_meta_signal' => false,
            );
        }

        $get_meta = static function (string $key) use ($object_kind, $entity_id) {
            return $object_kind === 'term'
                ? get_term_meta($entity_id, $key, true)
                : get_post_meta($entity_id, $key, true);
        };

        $content_keys = array(
            '_aysa_generated_content',
            '_aysa_seo_content',
            '_aysa_generated_h1',
            'blog_content_outline',
            'blog_content_tldr',
            'blog_content_introduction',
            'blog_content_extended',
            'blog_content_faq',
            'blog_content_conclusion',
        );
        $meta_keys = array(
            '_aysa_generated_meta_title',
            '_aysa_generated_meta_description',
        );

        return array(
            'has_content_signal' => $this->entity_meta_keys_have_content($get_meta, $content_keys),
            'has_meta_signal' => $this->entity_meta_keys_have_content($get_meta, $meta_keys),
        );
    }

    private function entity_meta_keys_have_content(callable $get_meta, array $meta_keys): bool
    {
        foreach ($meta_keys as $meta_key) {
            $value = $get_meta($meta_key);
            if (is_array($value)) {
                if (!empty(array_filter($value, static function ($item): bool {
                    return trim((string) $item) !== '';
                }))) {
                    return true;
                }
                continue;
            }

            if (trim((string) $value) !== '') {
                return true;
            }
        }

        return false;
    }

    private function has_existing_aysa_generated_content(array $history, bool $history_applied, string $optimization_scope, string $content_source, array $generation_signals): bool
    {
        if ($history_applied && $optimization_scope === 'content_rewrite') {
            return true;
        }

        $content_source = strtolower(trim($content_source));
        if ($content_source !== 'aysa_generated') {
            return false;
        }

        if (!empty($generation_signals['has_content_signal'])) {
            return true;
        }

        $history_content_fields = array(
            trim((string) ($history['content_title'] ?? '')),
            trim((string) ($history['content_description'] ?? '')),
            trim((string) ($history['short_description'] ?? '')),
        );
        foreach ($history_content_fields as $value) {
            if ($value !== '') {
                return true;
            }
        }

        return false;
    }

    private function resolve_entity_group(string $object_kind, string $post_type, string $taxonomy): string
    {
        if ($object_kind === 'post') {
            return match ($post_type) {
                'page' => 'page',
                'post' => 'article',
                'product' => 'product',
                default => 'other',
            };
        }

        return match ($taxonomy) {
            'product_cat' => 'ecommerce_category',
            default => 'other',
        };
    }

    private function resolve_publication_state(string $status): string
    {
        return match ($status) {
            'future' => 'scheduled',
            'draft', 'pending', 'auto-draft' => 'draft_or_pending',
            default => 'published',
        };
    }

    private function resolve_optimization_scope(array $history, array $quality, bool $history_applied): string
    {
        if (!$history_applied) {
            $content_source = strtolower(trim((string) ($quality['content_source'] ?? '')));
            return $content_source === 'aysa_generated' ? 'content_rewrite' : 'none';
        }

        $content_fields = array(
            trim((string) ($history['content_title'] ?? '')),
            trim((string) ($history['content_description'] ?? '')),
            trim((string) ($history['short_description'] ?? '')),
        );
        foreach ($content_fields as $value) {
            if ($value !== '') {
                return 'content_rewrite';
            }
        }

        $meta_fields = array(
            trim((string) ($history['meta_title'] ?? '')),
            trim((string) ($history['meta_description'] ?? '')),
        );
        foreach ($meta_fields as $value) {
            if ($value !== '') {
                return 'meta_only';
            }
        }

        $content_source = strtolower(trim((string) ($quality['content_source'] ?? '')));
        if ($content_source === 'aysa_generated') {
            return 'content_rewrite';
        }

        return 'none';
    }

    private function resolve_meta_state(array $history, bool $history_applied): string
    {
        $has_meta = trim((string) ($history['meta_title'] ?? '')) !== '' || trim((string) ($history['meta_description'] ?? '')) !== '';
        if (!$has_meta) {
            return 'none';
        }
        return $history_applied ? 'applied' : 'generated';
    }

    private function resolve_rewrite_state(string $entity_group, string $optimization_scope, bool $history_applied, string $mapping_state): string
    {
        if ($optimization_scope === 'content_rewrite') {
            return $history_applied ? 'applied' : 'generated';
        }
        return 'not_needed';
    }

    private function resolve_output_state(string $optimization_scope, bool $history_applied, string $rewrite_state): string
    {
        if ($history_applied && $optimization_scope === 'meta_only') {
            return 'meta_only_applied';
        }
        if ($history_applied && $optimization_scope === 'content_rewrite') {
            return 'rewrite_applied';
        }
        if (in_array($rewrite_state, array('generated', 'review_required'), true)) {
            return 'review_ready';
        }
        return 'none';
    }

    private function resolve_confidence_score(string $mapping_state, string $optimization_scope, bool $history_applied): ?float
    {
        if ($history_applied && $optimization_scope === 'content_rewrite') {
            return 0.96;
        }
        if ($history_applied && $optimization_scope === 'meta_only') {
            return 0.88;
        }
        if ($mapping_state === 'mapped') {
            return 0.72;
        }
        return null;
    }

    private function resolve_decision_reason(string $mapping_state, string $seo_state, string $optimization_scope, string $entity_group, string $publication_state): string
    {
        if ($publication_state !== 'published' && $seo_state === 'not_optimized') {
            return 'Entity is not published yet and should be optimized before release.';
        }
        if ($mapping_state !== 'mapped') {
            return 'Entity has no canonical keyword mapping yet.';
        }
        if ($seo_state === 'optimized' && $optimization_scope === 'content_rewrite') {
            return 'Applied AYSA rewrite exists in SEO history.';
        }
        if ($seo_state === 'optimized' && $optimization_scope === 'meta_only') {
            return 'Applied AYSA meta package exists in SEO history.';
        }
        if ($entity_group === 'product') {
            return 'Entity is mapped, but products default to a meta-first path unless a rewrite is explicitly recommended.';
        }
        return 'Entity has keyword context but no applied AYSA on-page output yet.';
    }

    private function resolve_next_step(string $seo_state, string $optimization_scope, string $publication_state, string $main_keyword, string $mapping_state, string $rewrite_state, string $meta_state): string
    {
        if ($publication_state !== 'published') {
            if ($seo_state === 'optimized') {
                return 'Ready for publication';
            }
            return 'Optimize before publication';
        }

        if ($mapping_state !== 'mapped' || $main_keyword === '') {
            return 'Keyword mapping needed';
        }

        if ($rewrite_state === 'recommended') {
            return 'Generate content rewrite';
        }

        if (in_array($rewrite_state, array('review_required', 'generated'), true)) {
            return 'Review content rewrite';
        }

        if ($seo_state === 'optimized' && $optimization_scope === 'content_rewrite') {
            return 'Already optimized';
        }

        if ($meta_state === 'none') {
            return 'Generate meta package';
        }

        if ($meta_state === 'generated') {
            return 'Review generated meta';
        }

        if ($seo_state === 'not_optimized') {
            return 'Decide rewrite';
        }

        if ($optimization_scope === 'meta_only') {
            return 'Upgrade to content rewrite';
        }

        return 'Maintain current output';
    }

    private function get_inventory_rows(int $project_id): array
    {
        global $wpdb;
        $rows = $wpdb->get_results(
            $wpdb->prepare("SELECT * FROM {$this->inventory_table} WHERE project_id = %d ORDER BY entity_group ASC, title ASC", $project_id),
            ARRAY_A
        );
        return is_array($rows) ? $rows : array();
    }

    private function get_latest_run(int $project_id): array
    {
        global $wpdb;
        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$this->runs_table} WHERE project_id = %d ORDER BY updated_at DESC, id DESC LIMIT 1", $project_id),
            ARRAY_A
        );
        if (!is_array($row)) {
            return array();
        }
        $row['summary'] = json_decode((string) ($row['summary_json'] ?? '{}'), true);
        if (!is_array($row['summary'])) {
            $row['summary'] = array();
        }
        return $row;
    }

    private function build_summary(array $rows, string $active_group, array $run, array $ownership_map = array()): array
    {
        $summary = $this->build_seed_summary($rows);
        $summary['groups']['keyword_ownership'] = intval($ownership_map['summary']['all'] ?? 0);
        $summary['active_group'] = $active_group;
        $summary['status'] = (string) ($run['status'] ?? 'idle');
        $summary['current_milestone'] = (string) ($run['current_milestone'] ?? '');
        $summary['ownership'] = isset($ownership_map['summary']) && is_array($ownership_map['summary']) ? $ownership_map['summary'] : array();
        return $summary;
    }

    private function build_seed_summary(array $rows): array
    {
        $summary = array(
            'all' => count($rows),
            'optimized' => 0,
            'not_optimized' => 0,
            'to_be_published' => 0,
            'keyword_mapping_needed' => 0,
            'mapping_review' => 0,
            'content_rewrite' => 0,
            'meta_only' => 0,
            'meta_applied' => 0,
            'meta_ready' => 0,
            'rewrite_recommended' => 0,
            'rewrite_review' => 0,
            'apply_queue' => 0,
            'keyword_validation_ready' => 0,
            'groups' => array(
                'page' => 0,
                'article' => 0,
                'ecommerce_category' => 0,
                'product' => 0,
                'other' => 0,
                'keyword_ownership' => 0,
            ),
        );

        foreach ($rows as $row) {
            $group = (string) ($row['entity_group'] ?? 'other');
            if (isset($summary['groups'][$group])) {
                $summary['groups'][$group]++;
            }
            if ((string) ($row['seo_state'] ?? '') === 'optimized') {
                $summary['optimized']++;
            } else {
                $summary['not_optimized']++;
            }
            if ((string) ($row['publication_state'] ?? '') !== 'published') {
                $summary['to_be_published']++;
            }
            if ((string) ($row['mapping_state'] ?? '') === 'needs_mapping') {
                $summary['keyword_mapping_needed']++;
            } elseif ((string) ($row['mapping_state'] ?? '') === 'mapping_review') {
                $summary['mapping_review']++;
            }
            if ((string) ($row['optimization_scope'] ?? '') === 'content_rewrite') {
                $summary['content_rewrite']++;
            } elseif ((string) ($row['optimization_scope'] ?? '') === 'meta_only') {
                $summary['meta_only']++;
            }
            if ((string) ($row['meta_state'] ?? '') === 'applied') {
                $summary['meta_applied']++;
            } elseif ((string) ($row['meta_state'] ?? '') === 'generated') {
                $summary['meta_ready']++;
            }
            if ((string) ($row['rewrite_state'] ?? '') === 'recommended') {
                $summary['rewrite_recommended']++;
            } elseif ((string) ($row['rewrite_state'] ?? '') === 'review_required') {
                $summary['rewrite_review']++;
            }
            if ($this->row_matches_operation($row, 'apply_meta') || $this->row_matches_operation($row, 'apply_rewrite')) {
                $summary['apply_queue']++;
            }
            if ($this->row_matches_keyword_validation($row)) {
                $summary['keyword_validation_ready']++;
            }
        }

        return $summary;
    }

    private function build_filter_counts(array $rows, string $active_group, array $ownership_map = array()): array
    {
        if ($active_group === 'keyword_ownership') {
            $ownership_summary = isset($ownership_map['summary']) && is_array($ownership_map['summary']) ? $ownership_map['summary'] : array();
            return array(
                'all' => intval($ownership_summary['all'] ?? 0),
                'unique' => intval($ownership_summary['unique'] ?? 0),
                'conflict' => intval($ownership_summary['conflict'] ?? 0),
                'needs_review' => intval($ownership_summary['needs_review'] ?? 0),
            );
        }

        $counts = array(
            'all' => 0,
            'optimized' => 0,
            'not_optimized' => 0,
            'to_be_published' => 0,
            'keyword_mapping_needed' => 0,
            'mapping_review' => 0,
            'content_rewrite' => 0,
            'meta_only' => 0,
            'meta_applied' => 0,
            'meta_ready' => 0,
            'rewrite_recommended' => 0,
            'rewrite_review' => 0,
            'apply_queue' => 0,
            'keyword_validation_ready' => 0,
        );

        foreach ($rows as $row) {
            if ((string) ($row['entity_group'] ?? '') !== $active_group) {
                continue;
            }
            $counts['all']++;
            if ((string) ($row['seo_state'] ?? '') === 'optimized') {
                $counts['optimized']++;
            } else {
                $counts['not_optimized']++;
            }
            if ((string) ($row['publication_state'] ?? '') !== 'published') {
                $counts['to_be_published']++;
            }
            if ((string) ($row['mapping_state'] ?? '') === 'needs_mapping') {
                $counts['keyword_mapping_needed']++;
            } elseif ((string) ($row['mapping_state'] ?? '') === 'mapping_review') {
                $counts['mapping_review']++;
            }
            if ((string) ($row['optimization_scope'] ?? '') === 'content_rewrite') {
                $counts['content_rewrite']++;
            } elseif ((string) ($row['optimization_scope'] ?? '') === 'meta_only') {
                $counts['meta_only']++;
            }
            if ((string) ($row['meta_state'] ?? '') === 'applied') {
                $counts['meta_applied']++;
            } elseif ((string) ($row['meta_state'] ?? '') === 'generated') {
                $counts['meta_ready']++;
            }
            if ((string) ($row['rewrite_state'] ?? '') === 'recommended') {
                $counts['rewrite_recommended']++;
            } elseif ((string) ($row['rewrite_state'] ?? '') === 'review_required') {
                $counts['rewrite_review']++;
            }
            if ($this->row_matches_operation($row, 'apply_meta') || $this->row_matches_operation($row, 'apply_rewrite')) {
                $counts['apply_queue']++;
            }
            if ($this->row_matches_keyword_validation($row)) {
                $counts['keyword_validation_ready']++;
            }
        }

        return $counts;
    }

    private function apply_filters(array $rows, string $active_group, array $filters): array
    {
        $filter = sanitize_key((string) ($filters['view'] ?? 'all'));
        if ($filter === '') {
            $filter = 'all';
        }
        $search = strtolower(trim((string) ($filters['q'] ?? '')));

        return array_values(array_filter($rows, function (array $row) use ($active_group, $filter, $search): bool {
            if ($active_group === 'keyword_ownership') {
                $status = (string) ($row['status'] ?? 'unique');
                $matches_filter = match ($filter) {
                    'conflict' => $status === 'conflict',
                    'unique' => $status === 'unique',
                    'needs_review' => $status === 'needs_review',
                    default => true,
                };
                if (!$matches_filter) {
                    return false;
                }
                if ($search === '') {
                    return true;
                }

                $haystacks = array(
                    strtolower((string) ($row['main_keyword'] ?? '')),
                    strtolower((string) ($row['url'] ?? '')),
                    strtolower((string) ($row['title'] ?? '')),
                    strtolower((string) ($row['entity_group'] ?? '')),
                );
                foreach ((array) ($row['conflicting_entities'] ?? array()) as $conflict) {
                    if (!is_array($conflict)) {
                        continue;
                    }
                    $haystacks[] = strtolower((string) ($conflict['url'] ?? ''));
                    $haystacks[] = strtolower((string) ($conflict['title'] ?? ''));
                }
                foreach ($haystacks as $haystack) {
                    if ($haystack !== '' && str_contains($haystack, $search)) {
                        return true;
                    }
                }
                return false;
            }

            if ((string) ($row['entity_group'] ?? '') !== $active_group) {
                return false;
            }

            $matches_filter = match ($filter) {
                'optimized' => (string) ($row['seo_state'] ?? '') === 'optimized',
                'not_optimized' => (string) ($row['seo_state'] ?? '') === 'not_optimized',
                'to_be_published' => (string) ($row['publication_state'] ?? '') !== 'published',
                'keyword_mapping_needed' => (string) ($row['mapping_state'] ?? '') === 'needs_mapping',
                'mapping_review' => (string) ($row['mapping_state'] ?? '') === 'mapping_review',
                'meta_ready' => (string) ($row['meta_state'] ?? '') === 'generated',
                'meta_applied' => (string) ($row['meta_state'] ?? '') === 'applied',
                'rewrite_recommended' => (string) ($row['rewrite_state'] ?? '') === 'recommended',
                'rewrite_review' => (string) ($row['rewrite_state'] ?? '') === 'review_required',
                'apply_queue' => $this->row_matches_operation($row, 'apply_meta') || $this->row_matches_operation($row, 'apply_rewrite'),
                'keyword_validation_ready' => $this->row_matches_keyword_validation($row),
                'content_rewrite' => (string) ($row['optimization_scope'] ?? '') === 'content_rewrite',
                'meta_only' => (string) ($row['optimization_scope'] ?? '') === 'meta_only',
                default => true,
            };
            if (!$matches_filter) {
                return false;
            }
            if ($search === '') {
                return true;
            }

            $haystacks = array(
                strtolower((string) ($row['url'] ?? '')),
                strtolower((string) ($row['title'] ?? '')),
                strtolower((string) ($row['main_keyword'] ?? '')),
                strtolower((string) ($row['next_step'] ?? '')),
                strtolower((string) ($row['decision_reason'] ?? '')),
                strtolower((string) ($row['keyword_owner_url'] ?? '')),
            );
            foreach ($haystacks as $haystack) {
                if ($haystack !== '' && str_contains($haystack, $search)) {
                    return true;
                }
            }
            return false;
        }));
    }

    private function upsert_inventory(int $project_id, array $row): void
    {
        global $wpdb;

        $entity_key = sanitize_text_field((string) ($row['entity_key'] ?? ''));
        if ($project_id <= 0 || $entity_key === '') {
            return;
        }

        $payload = array(
            'project_id' => $project_id,
            'entity_key' => $entity_key,
            'entity_id' => intval($row['entity_id'] ?? 0),
            'object_kind' => sanitize_key((string) ($row['object_kind'] ?? 'post')),
            'post_type' => $this->nullable_string($row['post_type'] ?? null),
            'taxonomy' => $this->nullable_string($row['taxonomy'] ?? null),
            'url' => $this->nullable_string($row['url'] ?? null),
            'title' => $this->nullable_string($row['title'] ?? null),
            'entity_group' => sanitize_key((string) ($row['entity_group'] ?? 'other')),
            'publication_state' => sanitize_key((string) ($row['publication_state'] ?? 'published')),
            'mapping_state' => sanitize_key((string) ($row['mapping_state'] ?? 'needs_mapping')),
            'seo_state' => sanitize_key((string) ($row['seo_state'] ?? 'not_optimized')),
            'optimization_scope' => sanitize_key((string) ($row['optimization_scope'] ?? 'none')),
            'meta_state' => sanitize_key((string) ($row['meta_state'] ?? 'none')),
            'rewrite_state' => sanitize_key((string) ($row['rewrite_state'] ?? 'not_needed')),
            'output_state' => sanitize_key((string) ($row['output_state'] ?? 'none')),
            'main_keyword' => $this->nullable_string($row['main_keyword'] ?? null),
            'confidence_score' => isset($row['confidence_score']) && is_numeric($row['confidence_score']) ? round((float) $row['confidence_score'], 2) : null,
            'comparison_result' => $this->nullable_string($row['comparison_result'] ?? null),
            'keyword_owner_entity_key' => $this->nullable_string($row['keyword_owner_entity_key'] ?? null),
            'keyword_owner_url' => $this->nullable_string($row['keyword_owner_url'] ?? null),
            'keyword_conflict_keyword' => $this->nullable_string($row['keyword_conflict_keyword'] ?? null),
            'keyword_conflict_reason' => $this->nullable_string($row['keyword_conflict_reason'] ?? null),
            'decision_reason' => $this->nullable_string($row['decision_reason'] ?? null),
            'next_step' => $this->nullable_string($row['next_step'] ?? null),
            'seo_score' => isset($row['seo_score']) && is_numeric($row['seo_score']) ? intval($row['seo_score']) : null,
            'content_source' => $this->nullable_string($row['content_source'] ?? null),
            'meta_package_json' => $this->maybe_json_encode($row['meta_package_json'] ?? null),
            'rewrite_decision_json' => $this->maybe_json_encode($row['rewrite_decision_json'] ?? null),
            'rewrite_package_json' => $this->maybe_json_encode($row['rewrite_package_json'] ?? null),
            'worker_run_id' => isset($row['worker_run_id']) && is_numeric($row['worker_run_id']) ? intval($row['worker_run_id']) : null,
            'last_evaluated_at' => $this->nullable_datetime($row['last_evaluated_at'] ?? current_time('mysql')),
        );

        $existing = $wpdb->get_var(
            $wpdb->prepare("SELECT id FROM {$this->inventory_table} WHERE project_id = %d AND entity_key = %s LIMIT 1", $project_id, $entity_key)
        );

        if ($existing) {
            $wpdb->update($this->inventory_table, $payload, array('id' => intval($existing)));
        } else {
            $wpdb->insert($this->inventory_table, $payload);
        }
    }

    private function resolve_association_type_from_inventory(array $row): string
    {
        $object_kind = strtolower(trim((string) ($row['object_kind'] ?? 'post')));
        if ($object_kind === 'term') {
            $taxonomy = strtolower(trim((string) ($row['taxonomy'] ?? '')));
            return match ($taxonomy) {
                'product_cat' => 'product_cat',
                'category' => 'category',
                'post_tag' => 'tag',
                'product_tag' => 'product_tag',
                default => 'term',
            };
        }

        $post_type = strtolower(trim((string) ($row['post_type'] ?? 'page')));
        return match ($post_type) {
            'product' => 'product',
            'post' => 'article',
            'page' => 'page',
            default => $post_type !== '' ? $post_type : 'page',
        };
    }

    private function resolve_batch_entity_keys(int $project_id, string $entity_group, array $filters, array $selection, string $operation, int $limit = 0): array
    {
        $selection = array_values(array_filter(array_map('strval', $selection)));
        if ($selection !== array()) {
            return $selection;
        }

        $rows = $this->get_inventory_rows($project_id);
        if ($entity_group !== '' && $entity_group !== 'all') {
            $rows = $this->apply_filters($rows, $entity_group, $filters);
        }
        $entity_keys = array();
        foreach ($rows as $row) {
            if ($this->row_matches_operation($row, $operation)) {
                $entity_key = trim((string) ($row['entity_key'] ?? ''));
                if ($entity_key !== '') {
                    $entity_keys[] = $entity_key;
                    if ($limit > 0 && count($entity_keys) >= $limit) {
                        break;
                    }
                }
            }
        }

        return array_values(array_unique($entity_keys));
    }

    private function row_matches_keyword_validation(array $row): bool
    {
        $main_keyword = trim((string) ($row['main_keyword'] ?? ''));
        $mapping_state = (string) ($row['mapping_state'] ?? '');
        $meta_state = (string) ($row['meta_state'] ?? 'none');
        $rewrite_state = (string) ($row['rewrite_state'] ?? 'not_needed');

        return $main_keyword !== ''
            && in_array($mapping_state, array('mapped', 'mapping_review'), true)
            && $meta_state === 'none'
            && !in_array($rewrite_state, array('generated', 'review_required', 'applied'), true);
    }

    private function row_matches_operation(array $row, string $operation): bool
    {
        $meta_state = (string) ($row['meta_state'] ?? 'none');
        $rewrite_state = (string) ($row['rewrite_state'] ?? 'not_needed');
        $output_state = (string) ($row['output_state'] ?? 'none');

        return match ($operation) {
            'apply_meta' => $meta_state === 'generated',
            'apply_rewrite' => in_array($rewrite_state, array('generated', 'review_required'), true)
                && $output_state !== 'skipped'
                && $this->row_has_valid_rewrite_package($row),
            'skip' => $output_state !== 'skipped',
            default => false,
        };
    }

    private function row_has_valid_rewrite_package(array $row): bool
    {
        $package = array();
        $raw_package = $row['rewrite_package_json'] ?? null;
        if (is_array($raw_package)) {
            $package = $raw_package;
        } elseif (is_string($raw_package) && trim($raw_package) !== '') {
            $decoded = json_decode($raw_package, true);
            if (is_array($decoded)) {
                $package = $decoded;
            }
        }

        if ($package === array()) {
            return false;
        }

        $rewritten_content = trim((string) ($package['rewritten_content'] ?? ''));
        $content_title = trim((string) ($package['content_title'] ?? ''));
        $content_description = trim((string) ($package['content_description'] ?? ''));

        return $rewritten_content !== '' || $content_title !== '' || $content_description !== '';
    }

    private function persist_entity_meta(array $inventory, string $meta_title, string $meta_description): void
    {
        $entity_id = intval($inventory['entity_id'] ?? 0);
        if ($entity_id <= 0) {
            return;
        }

        if ((string) ($inventory['object_kind'] ?? 'post') === 'term') {
            update_term_meta($entity_id, '_aysa_seo_title', $meta_title);
            update_term_meta($entity_id, '_aysa_seo_description', $meta_description);
            update_term_meta($entity_id, '_aysa_meta_title', $meta_title);
            update_term_meta($entity_id, '_aysa_meta_description', $meta_description);
            if (function_exists('is_plugin_active') && is_plugin_active('wordpress-seo/wp-seo.php')) {
                update_term_meta($entity_id, '_yoast_wpseo_title', $meta_title);
                update_term_meta($entity_id, '_yoast_wpseo_desc', $meta_description);
            }
            if (function_exists('is_plugin_active') && is_plugin_active('seo-by-rank-math/rank-math.php')) {
                update_term_meta($entity_id, 'rank_math_title', $meta_title);
                update_term_meta($entity_id, 'rank_math_description', $meta_description);
            }
            return;
        }

        update_post_meta($entity_id, '_aysa_seo_title', $meta_title);
        update_post_meta($entity_id, '_aysa_seo_description', $meta_description);
        update_post_meta($entity_id, '_aysa_meta_title', $meta_title);
        update_post_meta($entity_id, '_aysa_meta_description', $meta_description);
        if (function_exists('is_plugin_active') && is_plugin_active('wordpress-seo/wp-seo.php')) {
            update_post_meta($entity_id, '_yoast_wpseo_title', $meta_title);
            update_post_meta($entity_id, '_yoast_wpseo_metadesc', $meta_description);
        }
        if (function_exists('is_plugin_active') && is_plugin_active('seo-by-rank-math/rank-math.php')) {
            update_post_meta($entity_id, 'rank_math_title', $meta_title);
            update_post_meta($entity_id, 'rank_math_description', $meta_description);
        }
    }

    private function log_onpage_history_entry(array $inventory, array $data): void
    {
        if (!function_exists('aysa_get_seo_history')) {
            return;
        }

        $history = aysa_get_seo_history();
        if (!$history || !method_exists($history, 'log_seo_generation')) {
            return;
        }

        $entry_id = $history->log_seo_generation(array(
            'entity_id' => intval($inventory['entity_id'] ?? 0),
            'entity_type' => $this->resolve_history_entity_type($inventory),
            'entity_name' => (string) ($inventory['title'] ?? ''),
            'keyword' => (string) ($inventory['main_keyword'] ?? ''),
            'language' => $this->resolve_short_language_code(),
            'website_name' => (string) get_bloginfo('name'),
        ) + $data);

        if ($entry_id && !empty($data['is_applied']) && method_exists($history, 'mark_as_applied')) {
            $history->mark_as_applied($entry_id);
        }
    }

    private function resolve_history_entity_type(array $inventory): string
    {
        $object_kind = strtolower(trim((string) ($inventory['object_kind'] ?? 'post')));
        if ($object_kind === 'term') {
            $taxonomy = trim((string) ($inventory['taxonomy'] ?? ''));
            return $taxonomy !== '' ? $taxonomy : 'term';
        }

        $post_type = trim((string) ($inventory['post_type'] ?? ''));
        return $post_type !== '' ? $post_type : 'post';
    }

    private function get_keywords_for_entity(int $project_id, string $entity_key): array
    {
        $map = $this->build_keyword_map($project_id);
        $row = $map[$entity_key] ?? array();
        $keywords = isset($row['keywords']) && is_array($row['keywords']) ? $row['keywords'] : array();
        return array_values(array_filter(array_map('strval', $keywords)));
    }

    private function nullable_string($value): ?string
    {
        if ($value === null) {
            return null;
        }
        $value = trim((string) $value);
        return $value === '' ? null : $value;
    }

    /**
     * @param mixed $value
     */
    private function maybe_json_encode($value): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }
        if (is_string($value)) {
            $trimmed = trim($value);
            if ($trimmed === '') {
                return null;
            }
            json_decode($trimmed, true);
            return json_last_error() === JSON_ERROR_NONE ? $trimmed : wp_json_encode($trimmed);
        }
        if (is_array($value)) {
            return wp_json_encode($value);
        }
        return wp_json_encode($value);
    }

    /**
     * @param mixed $value
     * @return array<string,mixed>
     */
    private function decode_inventory_json($value): array
    {
        if (!is_string($value) || trim($value) === '') {
            return array();
        }
        $decoded = json_decode($value, true);
        return is_array($decoded) ? $decoded : array();
    }

    private function truncate_generation_text(string $value, int $maxChars): string
    {
        $value = trim($value);
        if ($value === '') {
            return '';
        }
        if (function_exists('mb_substr') && function_exists('mb_strlen')) {
            return mb_strlen($value) > $maxChars ? mb_substr($value, 0, $maxChars) : $value;
        }
        return strlen($value) > $maxChars ? substr($value, 0, $maxChars) : $value;
    }

    private function nullable_datetime($value): ?string
    {
        $value = trim((string) $value);
        return $value === '' ? null : $value;
    }

    private function sync_mapping_updates(int $project_id, array $updates): void
    {
        if ($project_id <= 0) {
            return;
        }

        $option_key = 'aysa_keyword_discovery_local_associations_' . intval($project_id);
        $rows = get_option($option_key, array());
        if (!is_array($rows)) {
            $rows = array();
        }

        foreach ($updates as $update) {
            if (!is_array($update)) {
                continue;
            }
            $keyword = trim((string) ($update['keyword'] ?? ''));
            $associated_on = trim((string) ($update['associated_on'] ?? ''));
            $id_intern = intval($update['id_intern'] ?? 0);
            $value = trim((string) ($update['value'] ?? ''));
            if ($keyword === '' || $associated_on === '' || $id_intern <= 0 || $value === '') {
                continue;
            }
            $normalized_keyword = $this->normalize_keyword($keyword);
            $normalized_association = strtolower(trim($associated_on));

            $rows = array_values(array_filter($rows, function ($row) use ($normalized_keyword, $normalized_association, $id_intern): bool {
                if (!is_array($row)) {
                    return false;
                }
                $row_keyword = $this->normalize_keyword((string) ($row['keyword'] ?? ''));
                $row_assoc = strtolower(trim((string) ($row['associatedOn'] ?? $row['associated_on'] ?? '')));
                $row_id = intval($row['idIntern'] ?? $row['id_intern'] ?? 0);

                if ($row_assoc === $normalized_association && $row_id === $id_intern) {
                    return false;
                }
                if ($normalized_keyword !== '' && $row_keyword === $normalized_keyword) {
                    return false;
                }
                return true;
            }));

            $rows[] = array(
                'id' => $this->build_local_association_id($keyword, $associated_on, $id_intern),
                'keyword' => $keyword,
                'value' => $value,
                'associatedOn' => $associated_on,
                'idIntern' => $id_intern,
            );

            $this->sync_entity_saved_keyword($associated_on, $id_intern, $keyword);
        }

        update_option($option_key, array_values($rows), false);
    }

    private function build_local_association_id(string $keyword, string $associated_on, int $id_intern): int
    {
        $signature = strtolower(trim($keyword)) . '|' . strtolower(trim($associated_on)) . '|' . intval($id_intern);
        return intval(sprintf('%u', crc32($signature)));
    }

    private function sync_entity_saved_keyword(string $associated_on, int $id_intern, string $keyword): void
    {
        if ($id_intern <= 0 || $keyword === '') {
            return;
        }

        $assoc = strtolower(trim($associated_on));
        $payload = array(
            'keyword' => $keyword,
            'aysa_keyword' => $keyword,
        );

        if (in_array($assoc, array('term', 'category', 'product_cat', 'product_tag', 'tag'), true)) {
            update_term_meta($id_intern, 'aysa_keyword_data', $payload);
            return;
        }

        update_post_meta($id_intern, 'aysa_keyword_data', $payload);
    }

    private function normalize_title($value, string $fallback): string
    {
        $value = trim(wp_strip_all_tags((string) $value));
        return $value !== '' ? $value : $fallback;
    }

    private function safe_term_link(WP_Term $term): string
    {
        $link = get_term_link($term);
        return is_wp_error($link) ? '' : (string) $link;
    }

    private function should_exclude_public_url(string $url): bool
    {
        $url = trim($url);
        if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
            return true;
        }

        $query = (string) parse_url($url, PHP_URL_QUERY);
        if ($query !== '') {
            parse_str($query, $query_args);
            if (!empty($query_args['page_id'])) {
                return true;
            }
        }

        return str_contains(strtolower($url), '?page_id=');
    }

    private function normalize_keyword(string $keyword): string
    {
        $keyword = remove_accents($keyword);
        $keyword = preg_replace('/\s+/u', ' ', $keyword);
        return strtolower(trim((string) $keyword));
    }

    private function apply_keyword_ownership_rules(array $rows): array
    {
        $grouped = array();
        foreach ($rows as $index => $row) {
            if (!is_array($row)) {
                continue;
            }
            $normalized_keyword = $this->normalize_keyword((string) ($row['main_keyword'] ?? ''));
            if ($normalized_keyword === '') {
                continue;
            }
            if (!isset($grouped[$normalized_keyword])) {
                $grouped[$normalized_keyword] = array();
            }
            $grouped[$normalized_keyword][] = $index;
        }

        foreach ($grouped as $indexes) {
            if (count($indexes) <= 1) {
                $single_index = $indexes[0] ?? null;
                if ($single_index !== null && isset($rows[$single_index])) {
                    $comparison_result = (string) ($rows[$single_index]['comparison_result'] ?? '');
                    if (str_starts_with($comparison_result, 'keyword_conflict_')) {
                        $rows[$single_index]['comparison_result'] = null;
                        $rows[$single_index]['keyword_owner_entity_key'] = null;
                        $rows[$single_index]['keyword_owner_url'] = null;
                        $rows[$single_index]['keyword_conflict_keyword'] = null;
                        $rows[$single_index]['keyword_conflict_reason'] = null;
                        if ((string) ($rows[$single_index]['mapping_state'] ?? '') === 'mapping_review') {
                            $rows[$single_index]['mapping_state'] = 'mapped';
                            $rows[$single_index]['next_step'] = 'Generate meta package';
                        }
                    }
                }
                continue;
            }

            $owner_index = array_shift($indexes);
            foreach ($indexes as $candidate_index) {
                if ($this->is_preferred_keyword_owner($rows[$candidate_index], $rows[$owner_index])) {
                    $owner_index = $candidate_index;
                }
            }

            $owner = $rows[$owner_index];
            $owner_keyword = trim((string) ($owner['main_keyword'] ?? ''));
            $owner_url = trim((string) ($owner['url'] ?? ''));
            $owner_entity_key = trim((string) ($owner['entity_key'] ?? ''));

            $rows[$owner_index]['comparison_result'] = null;
            $rows[$owner_index]['keyword_owner_entity_key'] = null;
            $rows[$owner_index]['keyword_owner_url'] = null;
            $rows[$owner_index]['keyword_conflict_keyword'] = null;
            $rows[$owner_index]['keyword_conflict_reason'] = null;

            foreach ($indexes as $loser_index) {
                if (!isset($rows[$loser_index])) {
                    continue;
                }
                $rows[$loser_index]['mapping_state'] = 'mapping_review';
                $rows[$loser_index]['comparison_result'] = 'keyword_conflict_existing_owner';
                $rows[$loser_index]['keyword_owner_entity_key'] = $owner_entity_key !== '' ? $owner_entity_key : null;
                $rows[$loser_index]['keyword_owner_url'] = $owner_url !== '' ? $owner_url : null;
                $rows[$loser_index]['keyword_conflict_keyword'] = $owner_keyword !== '' ? $owner_keyword : (string) ($rows[$loser_index]['main_keyword'] ?? '');
                $rows[$loser_index]['keyword_conflict_reason'] = $owner_url !== ''
                    ? sprintf('Keyword already owned by %s.', $owner_url)
                    : 'Keyword already owned by another page in this project.';
                $rows[$loser_index]['decision_reason'] = 'Suggested keyword conflicts with another page already owning this keyword.';
                $rows[$loser_index]['next_step'] = 'Review keyword conflict';
            }
        }

        return $rows;
    }

    private function is_preferred_keyword_owner(array $candidate, array $current_owner): bool
    {
        $candidate_score = 0.0;
        $owner_score = 0.0;

        if ((string) ($candidate['mapping_state'] ?? '') === 'mapped') {
            $candidate_score += 100.0;
        }
        if ((string) ($current_owner['mapping_state'] ?? '') === 'mapped') {
            $owner_score += 100.0;
        }
        if ((string) ($candidate['publication_state'] ?? '') === 'published') {
            $candidate_score += 5.0;
        }
        if ((string) ($current_owner['publication_state'] ?? '') === 'published') {
            $owner_score += 5.0;
        }

        $candidate_score += (float) ($candidate['confidence_score'] ?? 0.0);
        $owner_score += (float) ($current_owner['confidence_score'] ?? 0.0);
        $candidate_score += $this->keyword_owner_url_specificity_score((string) ($candidate['url'] ?? ''));
        $owner_score += $this->keyword_owner_url_specificity_score((string) ($current_owner['url'] ?? ''));

        if ($candidate_score === $owner_score) {
            return strcasecmp((string) ($candidate['entity_key'] ?? ''), (string) ($current_owner['entity_key'] ?? '')) < 0;
        }

        return $candidate_score > $owner_score;
    }

    private function keyword_owner_url_specificity_score(string $url): float
    {
        $path = trim((string) parse_url($url, PHP_URL_PATH), '/');
        if ($path === '') {
            return 0.0;
        }

        $segments = array_values(array_filter(explode('/', $path), static function ($segment): bool {
            return trim((string) $segment) !== '';
        }));

        return (count($segments) * 2.0) + min(strlen($path) / 100.0, 1.0);
    }

    private function build_keyword_uniqueness_map(array $rows): array
    {
        $grouped = array();
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $normalized_keyword = $this->normalize_keyword((string) ($row['main_keyword'] ?? ''));
            if ($normalized_keyword === '') {
                continue;
            }
            if (!isset($grouped[$normalized_keyword])) {
                $grouped[$normalized_keyword] = array();
            }
            $grouped[$normalized_keyword][] = $row;
        }

        $ownership_rows = array();
        $summary = array(
            'all' => 0,
            'unique' => 0,
            'conflict' => 0,
            'needs_review' => 0,
        );

        foreach ($grouped as $normalized_keyword => $entities) {
            if ($entities === array()) {
                continue;
            }

            $owner = $entities[0];
            foreach ($entities as $candidate) {
                if ($this->is_preferred_keyword_owner($candidate, $owner)) {
                    $owner = $candidate;
                }
            }

            $conflicts = array_values(array_filter($entities, static function ($candidate) use ($owner): bool {
                return (string) ($candidate['entity_key'] ?? '') !== (string) ($owner['entity_key'] ?? '');
            }));

            $status = 'unique';
            if ($conflicts !== array()) {
                $status = 'conflict';
                foreach ($conflicts as $conflict) {
                    if ((string) ($conflict['mapping_state'] ?? '') === 'mapping_review') {
                        $status = 'needs_review';
                        break;
                    }
                }
            }

            $ownership_rows[] = array(
                'entity_key' => (string) ($owner['entity_key'] ?? ''),
                'ownership_key' => $normalized_keyword,
                'main_keyword' => (string) ($owner['main_keyword'] ?? ''),
                'url' => (string) ($owner['url'] ?? ''),
                'title' => (string) ($owner['title'] ?? ''),
                'entity_group' => (string) ($owner['entity_group'] ?? 'other'),
                'status' => $status,
                'conflict_count' => count($conflicts),
                'next_step' => $status === 'unique' ? 'Owner confirmed' : 'Review keyword conflict',
                'owner_entity_key' => (string) ($owner['entity_key'] ?? ''),
                'owner_url' => (string) ($owner['url'] ?? ''),
                'owner_title' => (string) ($owner['title'] ?? ''),
                'owner_entity_group' => (string) ($owner['entity_group'] ?? 'other'),
                'conflicting_entities' => array_values(array_map(function ($conflict): array {
                    return array(
                        'entity_key' => (string) ($conflict['entity_key'] ?? ''),
                        'url' => (string) ($conflict['url'] ?? ''),
                        'title' => (string) ($conflict['title'] ?? ''),
                        'entity_group' => (string) ($conflict['entity_group'] ?? 'other'),
                        'mapping_state' => (string) ($conflict['mapping_state'] ?? ''),
                        'comparison_result' => (string) ($conflict['comparison_result'] ?? ''),
                        'next_step' => (string) ($conflict['next_step'] ?? ''),
                    );
                }, $conflicts)),
            );

            $summary['all']++;
            if ($status === 'unique') {
                $summary['unique']++;
            } elseif ($status === 'needs_review') {
                $summary['needs_review']++;
            } else {
                $summary['conflict']++;
            }
        }

        usort($ownership_rows, static function (array $a, array $b): int {
            $status_cmp = strcmp((string) ($a['status'] ?? ''), (string) ($b['status'] ?? ''));
            if ($status_cmp !== 0) {
                return $status_cmp;
            }
            return strcasecmp((string) ($a['main_keyword'] ?? ''), (string) ($b['main_keyword'] ?? ''));
        });

        return array(
            'summary' => $summary,
            'rows' => $ownership_rows,
        );
    }
}
