<?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 = esc_sql($wpdb->prefix . 'aysa_onpage_inventory');
        $this->runs_table = esc_sql($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);
        if ((string) ($filters['view'] ?? '') === 'content_rewrite_current') {
            $filters['worker_run_id'] = $this->latest_content_rewrite_inventory_run_id($rows);
        }
        $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, bool $include_public_readback = true): array
    {
        global $wpdb;
        $this->ensure_schema();
        $this->ensure_seeded($project_id, false);

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $inventory = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $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'] ?? '')
        );

        $featured_image_alt_target = $this->resolve_featured_image_alt_target(
            (string) ($inventory['object_kind'] ?? ''),
            intval($inventory['entity_id'] ?? 0),
            (string) ($inventory['taxonomy'] ?? ''),
            isset($keywords[0]) ? (string) $keywords[0] : (string) ($inventory['main_keyword'] ?? '')
        );
        if ($featured_image_alt_target !== array()) {
            $inventory['featured_image_alt_target'] = $featured_image_alt_target;
            $inventory['featured_image_alt_state'] = (string) ($featured_image_alt_target['state'] ?? '');
        }
        $inventory = $this->hydrate_seo_appendix_status($inventory);
        $meta_package = $this->decode_inventory_json($inventory['meta_package_json'] ?? null);
        $metadata_readback = $this->build_metadata_readback(
            $inventory,
            is_array($history) ? $history : array(),
            is_array($meta_package) ? $meta_package : array(),
            $include_public_readback
        );
        $inventory = $this->apply_metadata_readback_projection($inventory, $metadata_readback);

        return array(
            'inventory' => $inventory,
            'seo_history' => is_array($history) ? $history : array(),
            'keywords' => $keywords,
            'meta_package' => $meta_package,
            'metadata_readback' => $metadata_readback,
            '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');
        }

        $existing_summary = $this->get_run_summary($project_id, $run_id);
        $status_summary = 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))),
        );

        $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_merge($existing_summary, $status_summary)),
            'started_at' => $this->nullable_datetime($run['started_at'] ?? current_time('mysql')),
            'finished_at' => $this->nullable_datetime($run['finished_at'] ?? null),
        );

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $existing = $wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $wpdb->prepare("SELECT id FROM {$this->runs_table} WHERE project_id = %d AND run_id = %d LIMIT 1", $project_id, $run_id)
        );

        if ($existing) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $wpdb->update($this->runs_table, $payload, array('id' => intval($existing)));
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $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;
                }
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
                $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();
        $full_inventory_refresh = $inventory_rows === [] && !empty($projection['refresh_inventory']);
        if ($full_inventory_refresh) {
            $inventory_rows = $this->build_projection_rows($project_id);
        }
        if ($full_inventory_refresh) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $wpdb->delete($this->inventory_table, array('project_id' => $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']);
        }

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $existing = $wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $wpdb->prepare("SELECT id FROM {$this->runs_table} WHERE project_id = %d AND run_id = %d LIMIT 1", $project_id, $run_id)
        );

        $existing_summary = $this->get_run_summary($project_id, $run_id);
        $projection_summary = isset($projection['summary']) && is_array($projection['summary']) ? $projection['summary'] : array();
        $reconciliation = isset($projection['provider_output_reconciliation']) && is_array($projection['provider_output_reconciliation'])
            ? $projection['provider_output_reconciliation']
            : (isset($projection_summary['provider_output_reconciliation']) && is_array($projection_summary['provider_output_reconciliation'])
                ? $projection_summary['provider_output_reconciliation']
                : array());
        if ($reconciliation !== array()) {
            $projection_summary['provider_output_reconciliation'] = $reconciliation;
        }

        $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(
                $existing_summary,
                $projection_summary,
                $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) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $wpdb->update($this->runs_table, $payload, array('id' => intval($existing)));
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $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' => sanitize_key((string) ($plan['operation'] ?? 'apply_meta')),
            'write_class' => sanitize_key((string) ($plan['write_class'] ?? '')),
            'run_id' => $run_id,
            'payload_hash' => $payload_hash,
            'idempotency_key' => $idempotency_key,
            'accepted_count' => count($items),
            'applied_count' => 0,
            'verified_count' => 0,
            'failed_count' => 0,
            'skipped_count' => 0,
            'status' => 'skipped',
            'errors' => array(),
            'item_results' => array(),
            'billing_action_key' => 'onpage_plan_execution_bridge',
            'billing_policy' => 'verified_changed_onpage_fields_only_cost_x5',
            'billing_unit_cents' => 5,
            'billing_cost_cents' => 0,
            'actual_credits_cents' => 0,
            '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) {
                $verified_count = intval($previous['verified_count'] ?? 0);
                $billing_unit_cents = max(0, intval($previous['billing_unit_cents'] ?? $result['billing_unit_cents']));
                if ($verified_count > 0 && intval($previous['billing_cost_cents'] ?? 0) <= 0) {
                    $previous['billing_cost_cents'] = $verified_count * $billing_unit_cents;
                    $previous['actual_credits_cents'] = intval($previous['billing_cost_cents']);
                }
                $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 = ((string) ($result['operation'] ?? 'apply_meta') === 'apply_rewrite')
                ? (
                    (string) ($result['section'] ?? '') === 'aeo_content_qa' || (string) ($result['write_class'] ?? '') === 'onpage_aeo_appendix_apply'
                        ? $this->aeo_appendix_contract_blocked_result($entity_key)
                        : $this->apply_rewrite_output($project_id, $entity_key)
                )
                : $this->apply_meta_output($project_id, $entity_key);
            if (!empty($apply['ok'])) {
                $field_results = $this->build_meta_apply_verified_item_results($item_id, $entity_key, $apply);
                foreach ($field_results as $field_result) {
                    $status = sanitize_key((string) ($field_result['status'] ?? ''));
                    if ($status === 'verified' && !empty($field_result['changed'])) {
                        $result['applied_count']++;
                        $result['verified_count']++;
                    } else {
                        $result['skipped_count']++;
                    }
                    $result['item_results'][] = $field_result;
                }
            } else {
                $reason = sanitize_key((string) ($apply['error'] ?? 'onpage_apply_failed'));
                $status = sanitize_key((string) ($apply['status'] ?? 'failed'));
                if (in_array($status, array('blocked_high_risk', 'contract_needed', 'manual_review_needed'), true)) {
                    $result['skipped_count']++;
                } else {
                    $result['failed_count']++;
                    $result['errors'][] = $reason;
                }
                $item_result = array(
                    'item_id' => $item_id,
                    'entity_key' => $entity_key,
                    'status' => $status !== '' ? $status : 'failed',
                    'reason' => $reason,
                );
                foreach (array('write_class', 'writes_enabled', 'billing_status', 'billing_cost_cents', 'actual_credits_cents', 'required_contract') as $safe_key) {
                    if (array_key_exists($safe_key, $apply)) {
                        $item_result[$safe_key] = $apply[$safe_key];
                    }
                }
                $result['item_results'][] = $item_result;
            }
        }

        if ($result['failed_count'] > 0 && $result['verified_count'] === 0) {
            $result['status'] = 'failed';
        } elseif ($result['verified_count'] > 0 && $result['failed_count'] === 0) {
            $result['status'] = 'verified';
        } elseif ($result['verified_count'] > 0) {
            $result['status'] = 'partial_failed';
        } else {
            $result['status'] = 'skipped';
        }
        $result['billing_cost_cents'] = intval($result['verified_count']) * intval($result['billing_unit_cents']);
        $result['actual_credits_cents'] = intval($result['billing_cost_cents']);
        $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 build_meta_apply_verified_item_results(string $item_id, string $entity_key, array $apply): array
    {
        $metadata_readback = is_array($apply['metadata_readback'] ?? null) ? $apply['metadata_readback'] : array();
        $field_statuses = is_array($metadata_readback['field_status'] ?? null) ? $metadata_readback['field_status'] : array();
        $fields = array(
            'meta_title' => array(
                'old' => trim((string) ($apply['previous_meta_title'] ?? '')),
                'new' => trim((string) ($apply['meta_title'] ?? '')),
                'readback_key' => 'title',
            ),
            'meta_description' => array(
                'old' => trim((string) ($apply['previous_meta_description'] ?? '')),
                'new' => trim((string) ($apply['meta_description'] ?? '')),
                'readback_key' => 'meta_description',
            ),
        );
        $results = array();
        foreach ($fields as $field => $values) {
            if ($values['new'] === '') {
                continue;
            }
            $changed = $values['old'] !== $values['new'];
            $field_readback = is_array($field_statuses[$values['readback_key']] ?? null)
                ? $field_statuses[$values['readback_key']]
                : array();
            $provider_matched = (string) ($field_readback['provider_status'] ?? '') === 'matched';
            $public_matched = (string) ($field_readback['public_status'] ?? '') === 'matched';
            $verified = $provider_matched && $public_matched;
            $results[] = array(
                'item_id' => $item_id . ':' . $field,
                'entity_key' => $entity_key,
                'field' => $field,
                'status' => !$changed ? 'skipped_no_change' : ($verified ? 'verified' : 'failed_readback'),
                'changed' => $changed,
                'verified' => $verified,
                'reason' => !$changed ? 'no_change' : ($verified ? '' : 'provider_or_public_readback_mismatch'),
                'old_value' => $values['old'],
                'proposed_value' => $values['new'],
                'actual_value' => trim((string) ($field_readback['public'] ?? $field_readback['provider'] ?? '')),
                'provider_status' => (string) ($field_readback['provider_status'] ?? 'not_checked'),
                'public_status' => (string) ($field_readback['public_status'] ?? 'not_checked'),
            );
        }

        return $results !== array() ? $results : array(array(
            'item_id' => $item_id,
            'entity_key' => $entity_key,
            'status' => 'skipped_no_change',
            'changed' => false,
            'verified' => true,
            'reason' => 'no_metadata_fields',
        ));
    }

    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)) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $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();

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $row = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $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,
        )));

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $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();

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $row = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $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');
        }

        $rejected_keyword = trim((string) ($row['main_keyword'] ?? ''));
        $this->remove_local_mapping_for_inventory_row($project_id, $row);

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $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' => $rejected_keyword !== '' ? $rejected_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_manual_keyword_mapping(int $project_id, string $target_entity_key, string $keyword): array
    {
        $project_id = intval($project_id);
        $target_entity_key = trim($target_entity_key);
        $keyword = trim($keyword);
        if ($project_id <= 0 || $target_entity_key === '' || $keyword === '') {
            return array('ok' => false, 'error' => 'invalid_manual_mapping_payload');
        }

        $context = $this->build_selection_meta_for_entity($project_id, $target_entity_key);
        if ($context === array()) {
            return array('ok' => false, 'error' => 'target_entity_not_found');
        }

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

        $this->sync_mapping_updates($project_id, array(array(
            'entity_key' => $target_entity_key,
            'keyword' => $keyword,
            'primary_keyword' => $keyword,
            'main_keyword' => $keyword,
            'keyword_family' => array($keyword),
            'secondary_keywords' => array(),
            'mapping_state' => 'mapped',
            'mapping_status' => 'mapped',
            'review_status' => 'accepted',
            'source' => 'agent8_v3_manual_keyword_remap',
            'keyword_source' => 'agent8_v3_manual_keyword_remap',
            'associated_on' => $associated_on,
            'id_intern' => $entity_id,
            'value' => $value,
        )));

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

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

    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');
        }

        $previousMeta = $this->read_entity_meta($inventory);
        $previous_meta_title = trim((string) ($previousMeta['meta_title'] ?? ''));
        $previous_meta_description = trim((string) ($previousMeta['meta_description'] ?? ''));

        $this->persist_entity_meta($inventory, $meta_title, $meta_description);
        $after_readback = $this->build_metadata_readback($inventory, array(), array(
            'meta_title' => $meta_title,
            'meta_description' => $meta_description,
        ), true);
        $field_status = isset($after_readback['field_status']) && is_array($after_readback['field_status']) ? $after_readback['field_status'] : array();
        $title_provider_status = (string) ($field_status['title']['provider_status'] ?? 'not_expected');
        $description_provider_status = (string) ($field_status['meta_description']['provider_status'] ?? 'not_expected');
        $title_public_status = (string) ($field_status['title']['public_status'] ?? 'not_expected');
        $description_public_status = (string) ($field_status['meta_description']['public_status'] ?? 'not_expected');
        $provider_verified = ($meta_title === '' || $title_provider_status === 'matched')
            && ($meta_description === '' || $description_provider_status === 'matched');
        $public_verified = (string) ($after_readback['public_rendered']['status'] ?? '') === 'checked'
            && ($meta_title === '' || $title_public_status === 'matched')
            && ($meta_description === '' || $description_public_status === 'matched');
        $metadata_verified = $provider_verified && $public_verified;
        $history_id = $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,
            'metadata_provider' => (string) ($previousMeta['backend'] ?? 'aysa'),
            'metadata_provider_mode' => (string) ($previousMeta['mode'] ?? 'aysa_owned'),
            'generator_type' => 'agent8_onpage_meta',
            'generation_method' => 'onpage_workspace_apply_meta',
            'status' => $metadata_verified ? 'applied' : 'provider_drift_needs_confirmation',
            'is_applied' => $metadata_verified ? 1 : 0,
            'confidence_score' => isset($meta_package['confidence_score']) && is_numeric($meta_package['confidence_score'])
                ? (float) $meta_package['confidence_score']
                : null,
            'notes' => $metadata_verified
                ? 'Applied generated on-page meta package from workspace.'
                : 'Provider or public readback did not match the expected on-page meta package after write.',
        ));

        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);
        }

        if ($metadata_verified) {
            $this->mark_entity_meta_applied($project_id, $entity_key);
        }

        return array(
            'ok' => $metadata_verified,
            'error' => $metadata_verified ? '' : ($provider_verified ? 'public_readback_mismatch' : 'provider_readback_mismatch'),
            'entity_key' => $entity_key,
            'meta_title' => $meta_title,
            'meta_description' => $meta_description,
            'previous_meta_title' => $previous_meta_title,
            'previous_meta_description' => $previous_meta_description,
            'provider_verified' => $provider_verified,
            'public_verified' => $public_verified,
            'metadata_readback' => $after_readback,
            'history_id' => $history_id,
        );
    }

    private function mark_entity_meta_applied(int $project_id, string $entity_key): void
    {
        global $wpdb;
        $this->ensure_schema();

        if ($project_id <= 0 || $entity_key === '') {
            return;
        }

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $existing = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $wpdb->prepare("SELECT id FROM {$this->inventory_table} WHERE project_id = %d AND entity_key = %s LIMIT 1", $project_id, $entity_key),
            ARRAY_A
        );
        if (!is_array($existing)) {
            return;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $wpdb->update(
            $this->inventory_table,
            array(
                'seo_state' => 'optimized',
                'meta_state' => 'applied',
                'output_state' => 'applied',
                'last_evaluated_at' => current_time('mysql'),
            ),
            array('id' => intval($existing['id'] ?? 0))
        );
    }

    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();
        return $this->content_rewrite_contract_blocked_result($entity_key, $inventory, $rewrite_package);

        $featured_image_alt = isset($rewrite_package['featured_image_alt']) && is_array($rewrite_package['featured_image_alt']) ? $rewrite_package['featured_image_alt'] : array();
        $faq_schema_jsonld = trim((string) ($rewrite_package['faq_schema_jsonld'] ?? ''));
        $rewritten_content = trim((string) ($rewrite_package['rewritten_content'] ?? ''));
        $content_title = trim((string) ($rewrite_package['content_title'] ?? ''));
        $content_description = trim((string) ($rewrite_package['content_description'] ?? ''));
        $short_description_html = trim((string) ($rewrite_package['short_description_html'] ?? ''));
        $long_description_html = trim((string) ($rewrite_package['long_description_html'] ?? ''));
        $category_description_html = trim((string) ($rewrite_package['category_description_html'] ?? ''));
        $seo_appendix_html = trim((string) ($rewrite_package['seo_appendix_html'] ?? ''));
        if ($rewritten_content === '' && $content_title === '' && $content_description === '' && $short_description_html === '' && $long_description_html === '' && $category_description_html === '' && $seo_appendix_html === '') {
            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');
        }
        $url_topic_mismatch = $this->detect_url_topic_mismatch($inventory, $rewrite_package);
        $url_topic_fix = array('status' => 'not_needed');
        if (!empty($url_topic_mismatch['mismatch'])) {
            $url_topic_fix = $this->auto_fix_url_topic_mismatch($inventory, $url_topic_mismatch);
            if (empty($url_topic_fix['ok'])) {
                return array(
                    'ok' => false,
                    'error' => 'url_topic_auto_fix_failed',
                    'url_topic_mismatch' => $url_topic_mismatch,
                    'url_topic_fix' => $url_topic_fix,
                );
            }
        }
        $entity_group = sanitize_key((string) ($inventory['entity_group'] ?? 'other'));
        $post_type = (string) ($inventory['post_type'] ?? '');
        $taxonomy = (string) ($inventory['taxonomy'] ?? '');
        $content_surface = (string) ($rewrite_package['content_surface'] ?? '');
        if ($content_surface === '') {
            $content_surface = $this->detect_content_surface($object_kind, $entity_id, '');
        }
        $content_targets = isset($rewrite_package['content_targets']) && is_array($rewrite_package['content_targets'])
            ? $rewrite_package['content_targets']
            : $this->resolve_content_targets($entity_group, $object_kind, $post_type, $taxonomy, $content_surface);
        $apply_targets = isset($rewrite_package['apply_targets']) && is_array($rewrite_package['apply_targets'])
            ? $rewrite_package['apply_targets']
            : $this->build_default_apply_targets($entity_group, $object_kind, $content_surface);
        $content_by_key = array(
            'content_description' => $content_description,
            'rewritten_content' => $rewritten_content,
            'short_description_html' => $short_description_html !== '' ? $short_description_html : $content_description,
            'long_description_html' => $long_description_html !== '' ? $long_description_html : $rewritten_content,
            'category_description_html' => $category_description_html !== '' ? $category_description_html : ($rewritten_content !== '' ? $rewritten_content : $content_description),
            'seo_appendix_html' => $seo_appendix_html !== '' ? $seo_appendix_html : $rewritten_content,
        );

        $previous_content_title = '';
        $previous_content_description = '';
        $previous_short_description = '';
        $applied_targets = array();
        $review_targets = array();
        $featured_image_alt_result = array('status' => 'not_available');
        $seo_appendix_applied = false;

        if ($object_kind === 'term') {
            $term = get_term($entity_id, $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();
            foreach ($apply_targets as $target) {
                if (!is_array($target)) {
                    continue;
                }
                $target_key = sanitize_key((string) ($target['target'] ?? ''));
                $safe = array_key_exists('safe_to_auto_apply', $target) ? (bool) $target['safe_to_auto_apply'] : true;
                if (!$safe || $target_key !== 'term_description') {
                    $review_targets[] = $target_key;
                    continue;
                }
                $content_key = (string) ($target['content_key'] ?? 'category_description_html');
                $target_content = trim((string) ($content_by_key[$content_key] ?? ''));
                if ($target_content === '') {
                    continue;
                }
                $term_update['description'] = $target_content;
                $applied_targets[] = $target_key;
            }
            if ($term_update !== array()) {
                wp_update_term($entity_id, $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,
                'short_description_html' => $short_description_html,
                'long_description_html' => $long_description_html,
                'category_description_html' => $category_description_html,
                'seo_appendix_html' => $seo_appendix_html,
                'content_surface' => $content_surface,
                'content_targets' => $content_targets,
                'applied_targets' => $applied_targets,
                'review_targets' => $review_targets,
                '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);
            foreach ($apply_targets as $target) {
                if (!is_array($target)) {
                    continue;
                }
                $target_key = sanitize_key((string) ($target['target'] ?? ''));
                $safe = array_key_exists('safe_to_auto_apply', $target) ? (bool) $target['safe_to_auto_apply'] : true;
                if ($entity_group === 'product' && $target_key === 'post_content') {
                    $safe = !$this->post_content_has_builder_markup((string) $post->post_content);
                }
                if (!$safe || $target_key === 'builder_content_review') {
                    $review_targets[] = $target_key;
                    continue;
                }
                $content_key = (string) ($target['content_key'] ?? ($target_key === 'post_excerpt' ? 'short_description_html' : 'rewritten_content'));
                $target_content = trim((string) ($content_by_key[$content_key] ?? ''));
                if ($target_content === '') {
                    continue;
                }
                if ($target_key === 'post_excerpt') {
                    $post_update['post_excerpt'] = $target_content;
                    $applied_targets[] = $target_key;
                } elseif ($target_key === 'seo_appendix') {
                    $appendix_hash = md5($target_content);
                    update_post_meta($entity_id, '_aysa_seo_appendix_html', wp_kses_post($target_content));
                    update_post_meta($entity_id, '_aysa_seo_appendix_generated_at', current_time('mysql'));
                    update_post_meta($entity_id, '_aysa_seo_appendix_applied_at', current_time('mysql'));
                    update_post_meta($entity_id, '_aysa_seo_appendix_source', 'onpage_content_creation');
                    update_post_meta($entity_id, '_aysa_seo_appendix_primary_keyword', sanitize_text_field((string) ($inventory['main_keyword'] ?? '')));
                    update_post_meta($entity_id, '_aysa_seo_appendix_content_hash', $appendix_hash);
                    if ($this->is_valid_faq_schema_jsonld($faq_schema_jsonld) && trim((string) ($rewrite_package['faq_html'] ?? '')) !== '') {
                        update_post_meta($entity_id, '_aysa_seo_appendix_faq_schema_jsonld', wp_json_encode(json_decode($faq_schema_jsonld, true), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
                    } else {
                        delete_post_meta($entity_id, '_aysa_seo_appendix_faq_schema_jsonld');
                    }
                    $seo_appendix_applied = true;
                    $applied_targets[] = $target_key;
                } elseif ($target_key === 'post_content' && (!$this->content_surface_requires_builder_review($content_surface) || ($entity_group === 'product' && !$this->post_content_has_builder_markup((string) $post->post_content)))) {
                    $post_update['post_content'] = $target_content;
                    $applied_targets[] = $target_key;
                } else {
                    $review_targets[] = $target_key;
                }
            }
            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,
                'short_description_html' => $short_description_html,
                'long_description_html' => $long_description_html,
                'category_description_html' => $category_description_html,
                'seo_appendix_html' => $seo_appendix_html,
                'faq_schema_jsonld' => $faq_schema_jsonld,
                'content_surface' => $content_surface,
                'content_targets' => $content_targets,
                'applied_targets' => $applied_targets,
                'review_targets' => $review_targets,
                'applied_at' => current_time('mysql'),
            ));
        }

        if ($featured_image_alt !== array()) {
            $featured_image_alt_result = $this->apply_featured_image_alt_target($inventory, $featured_image_alt);
            if (($featured_image_alt_result['status'] ?? '') === 'success' || ($featured_image_alt_result['status'] ?? '') === 'already_present') {
                $applied_targets[] = 'featured_image_alt';
            } elseif (($featured_image_alt_result['status'] ?? '') === 'manual_needed') {
                $review_targets[] = 'featured_image_alt';
            }
        }

        $this->log_onpage_history_entry($inventory, array(
            'content_title' => $content_title !== '' ? $content_title : $previous_content_title,
            'content_description' => $category_description_html !== '' ? $category_description_html : ($long_description_html !== '' ? $long_description_html : ($rewritten_content !== '' ? $rewritten_content : $content_description)),
            'short_description' => $short_description_html !== '' ? $short_description_html : ($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,
            'content_surface' => $content_surface,
            'applied_targets' => $applied_targets,
            'review_targets' => $review_targets,
            'featured_image_alt' => $featured_image_alt_result,
            'url_topic_fix' => $url_topic_fix,
            'seo_appendix_applied' => $seo_appendix_applied,
        );
    }

    private function content_rewrite_contract_blocked_result(string $entity_key, array $inventory = array(), array $rewrite_package = array()): array
    {
        $shell = $this->content_rewrite_apply_contract_shell($entity_key, $inventory, $rewrite_package);
        return array(
            'ok' => false,
            'entity_key' => $entity_key,
            'operation' => 'apply_rewrite',
            'write_class' => 'onpage_content_rewrite_apply',
            'status' => 'blocked_high_risk',
            'error' => 'content_rewrite_contract_needed',
            'writes_enabled' => false,
            'billing_status' => 'not_billable_contract_blocked',
            'billing_cost_cents' => 0,
            'actual_credits_cents' => 0,
            'execution_shell_available' => true,
            'apply_contract_shell' => $shell,
            'apply_gate_status' => 'off',
            'apply_contract_status' => 'dry_run_ready_no_write',
            'controlled_apply_status' => 'readiness_contract_ready_write_disabled',
            'controlled_apply_ready' => false,
            'controlled_apply_readiness' => $shell['controlled_apply_readiness'] ?? $this->content_rewrite_controlled_apply_readiness(0),
            'write_execution_status' => 'blocked_until_controlled_gate',
            'live_write_requires_separate_confirmation' => true,
            'required_contract' => array(
                'exact_diff',
                'before_hash',
                'after_hash',
                'supported_content_surface',
                'restore_hash_check',
                'staging_or_explicit_safe_target',
                'readback_verification',
                'verified_only_billing',
            ),
        );
    }

    private function content_rewrite_apply_contract_shell(string $entity_key, array $inventory, array $rewrite_package): array
    {
        $current = (string) ($inventory['current_content_html'] ?? $inventory['current_content_text'] ?? $inventory['content_excerpt'] ?? $inventory['content_snippet'] ?? '');
        $proposed = (string) ($rewrite_package['rewritten_content'] ?? $rewrite_package['long_description_html'] ?? $rewrite_package['short_description_html'] ?? $rewrite_package['category_description_html'] ?? '');
        $current_normalized = $this->normalize_content_rewrite_contract_text($current);
        $proposed_normalized = $this->normalize_content_rewrite_contract_text($proposed);
        $before_hash = $current_normalized !== '' ? hash('sha256', $current_normalized) : '';
        $after_hash = $proposed_normalized !== '' ? hash('sha256', $proposed_normalized) : '';
        $surface = sanitize_key((string) ($rewrite_package['content_surface'] ?? $inventory['content_surface'] ?? 'unknown'));
        $unsupported = in_array($surface, array('elementor', 'wpbakery', 'divi', 'unknown'), true);
        $targets = $this->content_rewrite_apply_contract_targets($inventory, $rewrite_package, !$unsupported);
        $selected_count = $entity_key !== '' && $before_hash !== '' && $after_hash !== '' && $before_hash !== $after_hash && !$unsupported ? 1 : 0;
        $controlled_readiness = $this->content_rewrite_controlled_apply_readiness($selected_count);
        $gate_config = $this->content_rewrite_controlled_gate_config($selected_count);
        $preflight_enforcement = $this->content_rewrite_preflight_enforcement($targets, $surface, $before_hash, $after_hash, $gate_config);
        $single_row_contract = $this->content_rewrite_single_row_contract($entity_key, $targets, $surface, $before_hash, $after_hash, $gate_config, $preflight_enforcement);
        $shell_seed = wp_json_encode(array(
            'onpage_content_rewrite_apply_shell_v1',
            $entity_key,
            $before_hash,
            $after_hash,
            array_map(static function (array $target): string {
                return (string) ($target['target'] ?? '');
            }, $targets),
        ));
        $shell_hash = hash('sha256', is_string($shell_seed) ? $shell_seed : $entity_key . '|' . $before_hash . '|' . $after_hash);

        return array(
            'available' => true,
            'status' => 'dry_run_ready_no_write',
            'write_class' => 'onpage_content_rewrite_apply',
            'confirmation_kind' => 'onpage_content_rewrite_apply',
            'selected_rows_required' => true,
            'selected_row_count' => $selected_count,
            'supported_targets' => $targets,
            'payload_hash' => $shell_hash,
            'idempotency_key' => 'agent8v3:onpage_content_rewrite_apply_shell:' . $shell_hash,
            'worker_task_group' => 'onpage_apply_content_rewrite_dry_run',
            'plugin_bridge' => 'onpage_content_rewrite_apply_dry_run_bridge',
            'apply_gate_status' => 'off',
            'controlled_apply_status' => 'readiness_contract_ready_write_disabled',
            'controlled_apply_ready' => false,
            'controlled_apply_readiness' => $controlled_readiness,
            'gate_config' => $gate_config,
            'preflight_enforcement' => $preflight_enforcement,
            'single_row_contract' => $single_row_contract,
            'history_readback_foundation' => $this->content_rewrite_history_readback_foundation($selected_count),
            'write_execution_status' => 'blocked_until_controlled_gate',
            'live_write_requires_separate_confirmation' => true,
            'dry_run' => true,
            'writes_enabled' => false,
            'billing_enabled' => false,
            'max_cost_cents' => 0,
            'billing_policy' => 'blocked_until_verified_changed_content_targets_only_cost_x5',
            'readback_model' => 'before_after_hash_readback_required',
            'history_model' => 'before_after_content_snapshot_required',
            'restore_model' => 'hash_conflict_safe_restore_required',
        );
    }

    private function content_rewrite_controlled_apply_readiness(int $selected_count): array
    {
        return array(
            'status' => 'gate_off_no_write',
            'ready' => false,
            'dry_run_only' => true,
            'writes_enabled' => false,
            'billing_enabled' => false,
            'selected_rows' => max(0, $selected_count),
            'allowed_write_targets' => array(
                'post_content',
                'post_excerpt',
                'term_description',
            ),
            'worker_task_group' => 'onpage_apply_content_rewrite_controlled_gate_off',
            'plugin_bridge' => 'onpage_content_rewrite_apply_controlled_bridge',
            'billing_model' => 'verified_changed_content_targets_only_cost_x5_gate_off',
            'executor_available' => true,
            'requires_separate_live_write_confirmation' => true,
            'gates' => array(
                'api_gate_on' => false,
                'plugin_site_gate_on' => false,
                'explicit_selected_rows' => $selected_count > 0,
                'supported_content_surface' => 'required',
                'target_allowlist' => array(
                    'post_content',
                    'post_excerpt',
                    'term_description',
                ),
                'before_hash_match' => 'required',
                'after_hash_present' => 'required',
                'history_snapshot' => 'required',
                'restore_available' => 'required',
                'readback_verification' => 'required',
                'verified_only_billing' => 'required',
                'controlled_live_write_approval' => 'required',
            ),
        );
    }

    private function content_rewrite_controlled_gate_config(int $selected_count): array
    {
        $selected_count = max(0, $selected_count);
        return array(
            'status' => 'gate_off',
            'api_gate_on' => false,
            'plugin_site_gate_on' => false,
            'kill_switch_off' => false,
            'writes_enabled' => false,
            'billing_enabled' => false,
            'max_selected_rows' => 1,
            'selected_rows' => $selected_count,
            'selected_rows_within_limit' => $selected_count > 0 && $selected_count <= 1,
            'required_confirmation_kind' => 'onpage_content_rewrite_apply',
            'confirmation_valid' => false,
            'target_allowlist' => array(
                'post_content',
                'post_excerpt',
                'term_description',
            ),
            'safe_target_required' => true,
            'staging_or_explicit_safe_target_required' => true,
            'controlled_live_write_approval_required' => true,
            'scoped_gate_model' => array(
                'status' => 'foundation_only_gate_off',
                'scope_required' => 'project_site_entity_target_idempotency',
                'gate_can_enable_only_after_preflight' => true,
                'default_gate_state' => 'off',
                'broad_gate_allowed' => false,
            ),
        );
    }

    private function content_rewrite_preflight_enforcement(array $targets, string $surface, string $before_hash, string $after_hash, array $gate_config): array
    {
        $blocking = array();
        if (empty($gate_config['api_gate_on'])) {
            $blocking[] = 'api_gate_off';
        }
        if (empty($gate_config['plugin_site_gate_on'])) {
            $blocking[] = 'plugin_site_gate_off';
        }
        if (empty($gate_config['kill_switch_off'])) {
            $blocking[] = 'kill_switch_not_confirmed_off';
        }
        if (empty($gate_config['confirmation_valid'])) {
            $blocking[] = 'confirmation_kind_required';
        }
        if (empty($gate_config['selected_rows_within_limit'])) {
            $blocking[] = 'selected_rows_outside_limit';
        }
        $allowlist = is_array($gate_config['target_allowlist'] ?? null) ? $gate_config['target_allowlist'] : array();
        foreach ($targets as $target) {
            $target_key = sanitize_key((string) ($target['target'] ?? ''));
            if ($target_key !== '' && !in_array($target_key, $allowlist, true)) {
                $blocking[] = 'target_not_allowlisted';
            }
        }
        if (in_array(sanitize_key($surface), array('elementor', 'wpbakery', 'divi', 'unknown'), true)) {
            $blocking[] = 'unsupported_content_surface';
        }
        if ($before_hash === '') {
            $blocking[] = 'before_hash_required';
        }
        if ($after_hash === '') {
            $blocking[] = 'after_hash_required';
        }
        return array(
            'status' => 'blocked_gate_off',
            'passed' => false,
            'writes_enabled' => false,
            'billing_enabled' => false,
            'blocking_reasons' => array_values(array_unique($blocking)),
            'requirements' => array(
                'api_gate_on',
                'plugin_site_gate_on',
                'kill_switch_off',
                'confirmation_kind',
                'selected_rows_within_limit',
                'target_allowlist',
                'supported_content_surface',
                'before_hash',
                'after_hash',
                'history_store_available',
                'readback_contract',
                'restore_store_available',
            ),
        );
    }

    private function content_rewrite_single_row_contract(string $entity_key, array $targets, string $surface, string $before_hash, string $after_hash, array $gate_config, array $preflight_enforcement): array
    {
        $target = sanitize_key((string) ($targets[0]['target'] ?? ''));
        $allowlist = is_array($gate_config['target_allowlist'] ?? null) ? $gate_config['target_allowlist'] : array();
        $non_gate_blocking = array();
        if (intval($gate_config['selected_rows'] ?? 0) !== 1) {
            $non_gate_blocking[] = 'exactly_one_selected_row_required';
        }
        if (sanitize_text_field($entity_key) === '') {
            $non_gate_blocking[] = 'entity_identity_required';
        }
        if ($target === '' || !in_array($target, $allowlist, true)) {
            $non_gate_blocking[] = 'target_allowlist_required';
        }
        if ($before_hash === '') {
            $non_gate_blocking[] = 'before_hash_required';
        }
        if ($after_hash === '') {
            $non_gate_blocking[] = 'after_hash_required';
        }
        if ($before_hash !== '' && $after_hash !== '' && hash_equals($before_hash, $after_hash)) {
            $non_gate_blocking[] = 'changed_after_hash_required';
        }
        if (in_array(sanitize_key($surface), array('elementor', 'wpbakery', 'divi', 'unknown'), true)) {
            $non_gate_blocking[] = 'supported_content_surface_required';
        }
        return array(
            'status' => 'gate_off_no_write',
            'candidate_ready_for_future_controlled_test' => $non_gate_blocking === array(),
            'apply_allowed' => false,
            'writes_enabled' => false,
            'billing_enabled' => false,
            'requires_final_write_confirmation' => true,
            'max_selected_rows' => 1,
            'max_cost_cents' => 5,
            'selected_rows' => intval($gate_config['selected_rows'] ?? 0),
            'required_confirmation_kind' => 'onpage_content_rewrite_apply',
            'required_payload_fields' => array(
                'entity_key',
                'target',
                'content_surface',
                'before_hash',
                'after_hash',
                'confirmation_kind',
                'idempotency_key',
            ),
            'required_target_allowlist' => array(
                'post_content',
                'post_excerpt',
                'term_description',
            ),
            'selected_row_identity' => array(
                'entity_key' => sanitize_text_field($entity_key),
                'target' => $target,
                'content_surface' => sanitize_key($surface),
                'before_hash' => $before_hash,
                'after_hash' => $after_hash,
            ),
            'restore_requirements' => array(
                'history_store_available',
                'restore_snapshot_before_write',
                'restore_store_key_before_enable',
                'hash_conflict_guard_before_restore',
            ),
            'readback_requirements' => array(
                'read_current_target_before_write',
                'verify_after_hash_after_write',
                'block_billing_until_verified_changed',
            ),
            'scoped_gate_model' => array(
                'status' => 'foundation_only_gate_off',
                'scope_required' => 'project_site_entity_target_idempotency',
                'gate_can_enable_only_after_preflight' => true,
                'default_gate_state' => 'off',
                'broad_gate_allowed' => false,
            ),
            'write_checkpoint' => array(
                'status' => 'requires_explicit_write_confirmation',
                'stop_before_write' => true,
                'final_confirmation_required' => true,
                'write_may_run_in_this_wave' => false,
            ),
            'non_gate_blocking_reasons' => array_values(array_unique($non_gate_blocking)),
            'gate_blocking_reasons' => is_array($preflight_enforcement['blocking_reasons'] ?? null) ? array_values($preflight_enforcement['blocking_reasons']) : array(),
            'next_enablement_step' => 'controlled_single_row_preflight_live_no_write_or_billing',
        );
    }

    private function content_rewrite_history_readback_foundation(int $selected_count): array
    {
        return array(
            'status' => 'contract_ready_gate_off',
            'selected_rows' => max(0, $selected_count),
            'history_snapshot_required' => true,
            'history_persisted' => false,
            'history_store_available' => true,
            'history_store_option' => 'aysa_agent8v3_onpage_content_rewrite_restore_store',
            'readback_required' => true,
            'readback_executed' => false,
            'restore_required' => true,
            'restore_available' => false,
            'restore_store_available' => true,
            'hash_conflict_guard_required' => true,
            'billing_candidate_ready' => false,
            'billing_enabled' => false,
            'verified_only_billing_model' => 'verified_changed_content_targets_only_cost_x5_gate_off',
            'write_executor_shell' => array(
                'status' => 'gate_off_no_write',
                'available' => true,
                'writes_enabled' => false,
                'readback_enabled' => false,
                'restore_enabled' => false,
                'billing_enabled' => false,
            ),
        );
    }

    private function content_rewrite_apply_contract_targets(array $inventory, array $rewrite_package, bool $surface_supported): array
    {
        if (!$surface_supported) {
            return array();
        }
        $targets = isset($rewrite_package['apply_targets']) && is_array($rewrite_package['apply_targets'])
            ? $rewrite_package['apply_targets']
            : array();
        if ($targets === array()) {
            $object_kind = sanitize_key((string) ($inventory['object_kind'] ?? 'post'));
            $targets = $object_kind === 'term'
                ? array(array('target' => 'term_description', 'content_key' => 'category_description_html'))
                : array(
                    array('target' => 'post_content', 'content_key' => 'rewritten_content'),
                    array('target' => 'post_excerpt', 'content_key' => 'short_description_html'),
                );
        }
        $allowed = array('post_content', 'post_excerpt', 'term_description');
        $safe_targets = array();
        foreach ($targets as $target) {
            if (!is_array($target)) {
                continue;
            }
            $target_key = sanitize_key((string) ($target['target'] ?? ''));
            if (!in_array($target_key, $allowed, true)) {
                continue;
            }
            $safe_targets[] = array(
                'target' => $target_key,
                'content_key' => sanitize_key((string) ($target['content_key'] ?? '')),
                'surface' => sanitize_key((string) ($rewrite_package['content_surface'] ?? $inventory['content_surface'] ?? '')),
                'write_enabled' => false,
                'requires_readback' => true,
                'requires_history' => true,
                'requires_restore' => true,
            );
        }
        return $safe_targets;
    }

    private function aeo_appendix_contract_blocked_result(string $entity_key): array
    {
        return array(
            'ok' => false,
            'entity_key' => $entity_key,
            'operation' => 'apply_rewrite',
            'write_class' => 'onpage_aeo_appendix_apply',
            'status' => 'contract_needed',
            'error' => 'onpage_aeo_appendix_contract_needed',
            'writes_enabled' => false,
            'billing_status' => 'not_billable_contract_blocked',
            'billing_cost_cents' => 0,
            'actual_credits_cents' => 0,
            'required_contract' => array(
                'field_level_diff',
                'current_value',
                'proposed_value',
                'selected_rows',
                'explicit_confirmation',
                'readback_verification',
                'history_entry',
                'restore',
                'verified_only_billing',
            ),
        );
    }

    public function revert_onpage_item(int $project_id, string $entity_key, string $operation = 'apply_meta'): array
    {
        $detail = $this->get_detail($project_id, $entity_key);
        $inventory = isset($detail['inventory']) && is_array($detail['inventory']) ? $detail['inventory'] : array();
        $history = isset($detail['seo_history']) && is_array($detail['seo_history']) ? $detail['seo_history'] : array();
        if ($inventory === array()) {
            return array('ok' => false, 'error' => 'entity_not_found');
        }
        $operation = sanitize_key($operation);
        if ($operation === 'apply_meta' && sanitize_key((string) ($history['status'] ?? '')) === 'reverted') {
            $readback = $this->read_entity_meta($inventory);
            return array(
                'ok' => true,
                'entity_key' => $entity_key,
                'operation' => $operation,
                'status' => 'already_reverted',
                'verified' => true,
                'billing_status' => 'not_billable_restore',
                'billing_cost_cents' => 0,
                'actual_credits_cents' => 0,
                'readback' => $readback,
            );
        }
        if (!$this->is_history_applied($history)) {
            return array('ok' => false, 'error' => 'no_applied_aysa_history');
        }

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

        if ($operation === 'apply_meta') {
            $previous_meta_title = trim((string) ($history['previous_meta_title'] ?? ''));
            $previous_meta_description = trim((string) ($history['previous_meta_description'] ?? ''));
            $before_meta = $this->read_entity_meta($inventory);
            $this->persist_entity_meta($inventory, $previous_meta_title, $previous_meta_description);
            $this->log_onpage_history_entry($inventory, array(
                'meta_title' => $previous_meta_title,
                'meta_description' => $previous_meta_description,
                'previous_meta_title' => (string) ($history['meta_title'] ?? ''),
                'previous_meta_description' => (string) ($history['meta_description'] ?? ''),
                'generator_type' => 'agent8_onpage_meta_revert',
                'generation_method' => 'onpage_workspace_revert_meta',
                'status' => 'reverted',
                'is_applied' => 0,
                'notes' => 'Reverted AYSA On Page meta after user rejection.',
            ));
            $this->refresh_entity($project_id, $entity_key);
            $after_meta = $this->read_entity_meta($inventory);
            $verified = $after_meta['meta_title'] === $previous_meta_title
                && $after_meta['meta_description'] === $previous_meta_description;
            return array(
                'ok' => true,
                'entity_key' => $entity_key,
                'operation' => $operation,
                'status' => $verified ? 'reverted_verified' : 'reverted_pending_verification',
                'verified' => $verified,
                'billing_status' => 'not_billable_restore',
                'billing_cost_cents' => 0,
                'actual_credits_cents' => 0,
                'before' => $before_meta,
                'restored' => array(
                    'meta_title' => $previous_meta_title,
                    'meta_description' => $previous_meta_description,
                ),
                'readback' => $after_meta,
            );
        }

        if ($operation === 'apply_rewrite') {
            return $this->content_rewrite_restore_contract_result($entity_key, $inventory, $history);
        }

        return array('ok' => false, 'error' => 'unsupported_revert_operation');
    }

    private function content_rewrite_restore_contract_result(string $entity_key, array $inventory, array $history): array
    {
        $previous_content = (string) ($history['previous_content_description'] ?? '');
        $previous_short = (string) ($history['previous_short_description'] ?? '');
        $applied_content = (string) ($history['content_description'] ?? '');
        $applied_short = (string) ($history['short_description'] ?? '');
        $current_content = (string) ($inventory['current_content_html'] ?? $inventory['current_content_text'] ?? $inventory['content_excerpt'] ?? $inventory['content_snippet'] ?? '');
        $previous_normalized = $this->normalize_content_rewrite_contract_text($previous_content . "\n" . $previous_short);
        $applied_normalized = $this->normalize_content_rewrite_contract_text($applied_content . "\n" . $applied_short);
        $current_normalized = $this->normalize_content_rewrite_contract_text($current_content);
        $before_hash = $previous_normalized !== '' ? hash('sha256', $previous_normalized) : '';
        $after_hash = $applied_normalized !== '' ? hash('sha256', $applied_normalized) : '';
        $current_hash = $current_normalized !== '' ? hash('sha256', $current_normalized) : '';

        return array(
            'ok' => false,
            'entity_key' => $entity_key,
            'operation' => 'apply_rewrite',
            'write_class' => 'onpage_content_rewrite_apply',
            'status' => 'content_rewrite_restore_contract_required',
            'error' => 'content_rewrite_restore_not_executable',
            'writes_enabled' => false,
            'billing_status' => 'not_billable_restore_contract_blocked',
            'billing_cost_cents' => 0,
            'actual_credits_cents' => 0,
            'restore_allowed' => false,
            'restore_preview_available' => $previous_normalized !== '',
            'restore_conflict_check' => array(
                'required' => true,
                'current_hash' => $current_hash,
                'history_before_hash' => $before_hash,
                'history_after_hash' => $after_hash,
                'status' => 'blocked_until_hash_verified',
            ),
            'restore_requirements' => array(
                'before_content_snapshot',
                'before_hash',
                'after_hash',
                'current_hash_readback',
                'hash_conflict_block',
                'readback_after_restore',
                'non_billable_restore',
            ),
            'message' => 'Content rewrite restore is preview-only until the high-risk content restore contract is implemented.',
        );
    }

    private function normalize_content_rewrite_contract_text(string $value): string
    {
        $value = trim(html_entity_decode(wp_strip_all_tags($value), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
        $value = preg_replace('/\s+/u', ' ', $value);
        return is_string($value) ? trim($value) : '';
    }

    private function hydrate_seo_appendix_status(array $inventory): array
    {
        $entity_id = intval($inventory['entity_id'] ?? 0);
        $object_kind = strtolower(trim((string) ($inventory['object_kind'] ?? 'post')));
        if ($entity_id <= 0 || $object_kind === 'term') {
            $inventory['seo_appendix_applied'] = false;
            $inventory['seo_appendix_applied_at'] = '';
            $inventory['seo_appendix_hash'] = '';
            return $inventory;
        }

        $html = trim((string) get_post_meta($entity_id, '_aysa_seo_appendix_html', true));
        $applied_at = trim((string) get_post_meta($entity_id, '_aysa_seo_appendix_applied_at', true));
        $generated_at = trim((string) get_post_meta($entity_id, '_aysa_seo_appendix_generated_at', true));
        $hash = trim((string) get_post_meta($entity_id, '_aysa_seo_appendix_content_hash', true));
        $inventory['seo_appendix_applied'] = $html !== '';
        $inventory['seo_appendix_applied_at'] = $applied_at !== '' ? $applied_at : $generated_at;
        $inventory['seo_appendix_hash'] = $hash !== '' ? $hash : ($html !== '' ? md5($html) : '');

        return $inventory;
    }

    private function is_valid_faq_schema_jsonld(string $jsonld): bool
    {
        if (trim($jsonld) === '') {
            return false;
        }
        $decoded = json_decode($jsonld, true);
        if (!is_array($decoded)) {
            return false;
        }
        $type = $decoded['@type'] ?? '';
        if (is_array($type)) {
            $type = implode(' ', array_map('strval', $type));
        }
        if (stripos((string) $type, 'FAQPage') === false) {
            return false;
        }
        return isset($decoded['mainEntity']) && is_array($decoded['mainEntity']) && $decoded['mainEntity'] !== array();
    }

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

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $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);
        if (sanitize_key($operation) === 'apply_rewrite') {
            return array(
                'selected' => count($entity_keys),
                'attempted_count' => count($entity_keys),
                'eligible' => 0,
                'applied' => 0,
                'applied_count' => 0,
                'failed' => 0,
                'failed_count' => 0,
                'skipped' => count($entity_keys),
                'skipped_count' => count($entity_keys),
                'remaining_eligible_count' => 0,
                'items' => array(),
                'operation' => 'apply_rewrite',
                'status' => 'blocked_high_risk',
                'error' => 'content_rewrite_contract_needed',
                'write_class' => 'onpage_content_rewrite_apply',
                'writes_enabled' => false,
                'billing_status' => 'not_billable_contract_blocked',
                'billing_cost_cents' => 0,
                'actual_credits_cents' => 0,
                'required_contract' => array(
                    'exact_diff',
                    'before_hash',
                    'after_hash',
                    'supported_content_surface',
                    'restore_hash_check',
                    'staging_or_explicit_safe_target',
                    'readback_verification',
                    'verified_only_billing',
                ),
            );
        }
        $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;
        }

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $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);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $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',
                // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Canonical entity inventory intentionally spans every installed language; IDs remain bounded to public post types and no request value enters the query.
                '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;
                }
                if ((string) $post->post_status === 'publish' && $this->should_exclude_redirected_public_url($url, 'post', intval($post_id))) {
                    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;
                }
                if ($this->should_exclude_redirected_public_url($url, 'term', intval($term->term_id))) {
                    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'] ?? ''));
        $providerMeta = $this->read_entity_meta(array(
            'entity_id' => $entity_id,
            'object_kind' => $object_kind,
        ));
        if (trim((string) ($providerMeta['meta_title'] ?? '')) !== '') {
            $current_meta_title = trim((string) $providerMeta['meta_title']);
        }
        if (trim((string) ($providerMeta['meta_description'] ?? '')) !== '') {
            $current_meta_description = trim((string) $providerMeta['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)) {
                $existingOptimizationScope = sanitize_key((string) ($existing_inventory['optimization_scope'] ?? ''));
                $rewrite_state = $existingRewriteState;
                if ($existingOptimizationScope === 'aeo_content_qa') {
                    $optimization_scope = 'aeo_content_qa';
                } elseif ($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 = $existingOptimizationScope === 'aeo_content_qa' ? 'Review AEO content QA' : 'Review content rewrite';
                    $output_state = $existingOptimizationScope === 'aeo_content_qa' ? 'aeo_review_ready' : '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 = $existingOptimizationScope === 'aeo_content_qa'
                    ? 'worker_aeo_generated'
                    : ($existingRewritePackage !== null ? 'worker_rewrite_generated' : 'worker_rewrite_decision');
            }
        }
        if (is_array($existing_inventory) && $history_applied) {
            $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 (in_array($existingRewriteState, array('recommended', 'review_required', 'generated'), true) && ($existingRewriteDecision !== null || $existingRewritePackage !== null)) {
                $existingOptimizationScope = sanitize_key((string) ($existing_inventory['optimization_scope'] ?? ''));
                $rewrite_state = $existingRewriteState;
                $optimization_scope = $existingOptimizationScope === 'aeo_content_qa' ? 'aeo_content_qa' : 'content_rewrite';
                $next_step = $existingRewriteState === 'recommended'
                    ? 'Generate content rewrite'
                    : ($existingOptimizationScope === 'aeo_content_qa' ? 'Review AEO content QA' : 'Review content rewrite');
                if (in_array($existingRewriteState, array('review_required', 'generated'), true)) {
                    $output_state = $existingOptimizationScope === 'aeo_content_qa' ? 'aeo_review_ready' : '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 = $existingOptimizationScope === 'aeo_content_qa'
                    ? 'worker_aeo_generated'
                    : ($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,
            'metadata_provider' => (string) ($providerMeta['backend'] ?? ''),
            'metadata_provider_mode' => (string) ($providerMeta['mode'] ?? ''),
            'metadata_route_evidence_available' => (string) ($providerMeta['backend'] ?? '') !== '',
            'metadata_old_value_captured' => (string) ($providerMeta['backend'] ?? '') !== '',
            '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();
        }

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $row = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $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 = esc_sql($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(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to 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
        );
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query: table identifiers and structure are internal, and request-derived values are prepared before execution. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $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'] ?? ''));
        $providerMeta = $this->read_entity_meta(array(
            'entity_id' => $entity_id,
            'object_kind' => $object_kind,
        ));
        if (trim((string) ($providerMeta['meta_title'] ?? '')) !== '') {
            $current_meta_title = trim((string) $providerMeta['meta_title']);
        }
        if (trim((string) ($providerMeta['meta_description'] ?? '')) !== '') {
            $current_meta_description = trim((string) $providerMeta['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);
        $content_surface = $this->detect_content_surface($object_kind, $entity_id, $current_content_html);
        $content_targets = $this->resolve_content_targets($entity_group, $object_kind, $post_type, $taxonomy, $content_surface);
        $featured_image_alt_target = $this->resolve_featured_image_alt_target($object_kind, $entity_id, $taxonomy, $main_keyword);

        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,
            'metadata_provider' => (string) ($providerMeta['backend'] ?? ''),
            'metadata_provider_mode' => (string) ($providerMeta['mode'] ?? ''),
            'metadata_route_evidence_available' => (string) ($providerMeta['backend'] ?? '') !== '',
            'metadata_old_value_captured' => (string) ($providerMeta['backend'] ?? '') !== '',
            '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),
            'content_surface' => $content_surface,
            'builder_detected' => $this->content_surface_requires_builder_review($content_surface),
            'safe_to_auto_apply' => !empty($content_targets['safe_to_auto_apply']),
            'requires_builder_review' => !empty($content_targets['requires_builder_review']),
            'content_targets' => $content_targets,
            'featured_image_alt_target' => $featured_image_alt_target,
            'category_data' => $commerce_context['category_data'],
            'product_data' => $commerce_context['product_data'],
        );
    }

    private function resolve_featured_image_alt_target(string $object_kind, int $entity_id, string $taxonomy, string $main_keyword): array
    {
        if ($entity_id <= 0) {
            return array();
        }

        $attachment_id = 0;
        if ($object_kind === 'post') {
            $attachment_id = intval(get_post_thumbnail_id($entity_id));
        } elseif ($taxonomy === 'product_cat' || $object_kind === 'term') {
            $attachment_id = intval(get_term_meta($entity_id, 'thumbnail_id', true));
        }
        if ($attachment_id <= 0) {
            return array(
                'state' => 'not_available',
                'attachment_id' => 0,
                'image_url' => '',
                'current_alt' => '',
                'primary_keyword' => $main_keyword,
                'safe_to_generate' => false,
            );
        }

        $image_url = wp_get_attachment_url($attachment_id);
        $current_alt = trim((string) get_post_meta($attachment_id, '_wp_attachment_image_alt', true));
        $generated_at = trim((string) get_post_meta($attachment_id, '_aysa_image_alt_generated_at', true));
        $source = trim((string) get_post_meta($attachment_id, '_aysa_image_alt_source', true));
        $state = $current_alt === '' ? 'missing' : ($generated_at !== '' ? 'aysa_generated' : 'present');

        return array(
            'state' => $state,
            'attachment_id' => $attachment_id,
            'image_url' => $image_url ? esc_url_raw((string) $image_url) : '',
            'image_filename' => $image_url ? sanitize_text_field(basename((string) wp_parse_url((string) $image_url, PHP_URL_PATH))) : '',
            'current_alt' => $current_alt,
            'primary_keyword' => $main_keyword,
            'source' => $source,
            'generated_at' => $generated_at,
            'safe_to_generate' => $state === 'missing',
        );
    }

    private function apply_featured_image_alt_target(array $inventory, array $featured_image_alt): array
    {
        return $this->featured_image_alt_routed_to_technical_seo_result($inventory, $featured_image_alt);
    }

    private function featured_image_alt_routed_to_technical_seo_result(array $inventory, array $featured_image_alt): array
    {
        $attachment_id = intval($featured_image_alt['attachment_id'] ?? 0);
        $proposed = sanitize_text_field((string) ($featured_image_alt['proposed_alt'] ?? $featured_image_alt['final_alt'] ?? ''));
        return array(
            'status' => 'routed_to_technical_seo_image_alt',
            'write_class' => 'onpage_featured_image_alt_apply',
            'reason' => 'owned_by_technical_seo_image_alt_executor',
            'attachment_id' => $attachment_id,
            'proposed_alt' => $proposed,
            'entity_key' => sanitize_text_field((string) ($inventory['entity_key'] ?? '')),
            'writes_enabled' => false,
            'billing_status' => 'not_billable_routed_to_technical_image_alt',
            'billing_cost_cents' => 0,
            'actual_credits_cents' => 0,
            'route_to_module' => 'technical_seo_image_alt',
            'required_executor' => 'technical_image_alt_apply_bridge',
            'required_contract' => array(
                'technical_image_alt_history',
                'technical_image_alt_restore',
                'technical_image_alt_readback',
                'technical_image_alt_verified_only_billing',
            ),
        );
    }

    private function detect_content_surface(string $object_kind, int $entity_id, string $content_html): string
    {
        if ($object_kind !== 'post') {
            return 'classic';
        }

        if ($entity_id <= 0) {
            return 'unknown';
        }
        if ($content_html === '') {
            $content_html = (string) get_post_field('post_content', $entity_id);
        }

        $elementor_data = get_post_meta($entity_id, '_elementor_data', true);
        $elementor_mode = get_post_meta($entity_id, '_elementor_edit_mode', true);
        if ((is_string($elementor_data) && trim($elementor_data) !== '') || trim((string) $elementor_mode) !== '') {
            return 'elementor';
        }

        if (strpos($content_html, '<!-- wp:') !== false) {
            return 'gutenberg';
        }

        if (preg_match('/\[(vc_row|vc_column|vc_[a-z0-9_-]+)/i', $content_html)) {
            return 'wpbakery';
        }

        if (preg_match('/\[et_pb_/i', $content_html)) {
            return 'divi';
        }

        return 'classic';
    }

    private function content_surface_requires_builder_review(string $content_surface): bool
    {
        return in_array($content_surface, array('elementor', 'wpbakery', 'divi', 'unknown'), true);
    }

    private function resolve_content_targets(string $entity_group, string $object_kind, string $post_type, string $taxonomy, string $content_surface): array
    {
        $requires_builder_review = $this->content_surface_requires_builder_review($content_surface);

        if ($object_kind === 'term') {
            return array(
                'primary_target' => 'term_description',
                'secondary_targets' => array(),
                'safe_to_auto_apply' => true,
                'requires_builder_review' => false,
                'apply_strategy' => 'auto_term_description',
            );
        }

        if ($entity_group === 'product' || $post_type === 'product') {
            return array(
                'primary_target' => 'post_excerpt',
                'secondary_targets' => array('post_content'),
                'safe_to_auto_apply' => true,
                'requires_builder_review' => $requires_builder_review,
                'apply_strategy' => 'auto_short_description_only',
            );
        }

        if (in_array($entity_group, array('page', 'article'), true)) {
            return array(
                'primary_target' => 'seo_appendix',
                'secondary_targets' => $requires_builder_review ? array('builder_content_review') : array(),
                'safe_to_auto_apply' => true,
                'requires_builder_review' => $requires_builder_review,
                'apply_strategy' => 'auto_seo_appendix',
            );
        }

        return array(
            'primary_target' => 'builder_content_review',
            'secondary_targets' => array(),
            'safe_to_auto_apply' => false,
            'requires_builder_review' => true,
            'apply_strategy' => 'manual_builder_review',
        );
    }

    private function build_default_apply_targets(string $entity_group, string $object_kind, string $content_surface): array
    {
        if ($object_kind === 'term') {
            return array(array(
                'target' => 'term_description',
                'content_key' => 'category_description_html',
                'label' => 'Category description',
                'safe_to_auto_apply' => true,
            ));
        }

        if ($entity_group === 'product') {
            return array(
                array(
                    'target' => 'post_excerpt',
                    'content_key' => 'short_description_html',
                    'label' => 'WooCommerce short description',
                    'safe_to_auto_apply' => true,
                ),
                array(
                    'target' => 'post_content',
                    'content_key' => 'long_description_html',
                    'label' => 'Long description',
                    'safe_to_auto_apply' => false,
                ),
            );
        }

        if (in_array($entity_group, array('page', 'article'), true)) {
            $targets = array(array(
                'target' => 'seo_appendix',
                'content_key' => 'seo_appendix_html',
                'label' => 'SEO appendix',
                'safe_to_auto_apply' => true,
            ));
            if ($this->content_surface_requires_builder_review($content_surface)) {
                $targets[] = array(
                    'target' => 'builder_content_review',
                    'content_key' => 'rewritten_content',
                    'label' => 'Builder content',
                    'safe_to_auto_apply' => false,
                );
            }
            return $targets;
        }

        if (!$this->content_surface_requires_builder_review($content_surface)) {
            return array(array(
                'target' => 'post_content',
                'content_key' => 'rewritten_content',
                'label' => 'Page content',
                'safe_to_auto_apply' => true,
            ));
        }

        return array(array(
            'target' => 'builder_content_review',
            'content_key' => 'rewritten_content',
            'label' => 'Builder content',
            'safe_to_auto_apply' => false,
        ));
    }

    private function post_content_has_builder_markup(string $content_html): bool
    {
        if ($content_html === '') {
            return false;
        }

        return preg_match('/data-elementor-type|elementor-section|elementor-widget|\\[(vc_row|vc_column|vc_[a-z0-9_-]+)|\\[et_pb_/i', $content_html) === 1;
    }

    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) {
            if ($this->entity_meta_value_has_content($get_meta($meta_key))) {
                return true;
            }
        }

        return false;
    }

    private function entity_meta_value_has_content($value): bool
    {
        if (is_array($value)) {
            foreach ($value as $item) {
                if ($this->entity_meta_value_has_content($item)) {
                    return true;
                }
            }

            return false;
        }

        return is_scalar($value) && trim((string) $value) !== '';
    }

    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;
        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $rows = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $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;
        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $row = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $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 latest_content_rewrite_inventory_run_id(array $rows): int
    {
        $run_id = 0;
        foreach ($rows as $row) {
            if (!is_array($row) || sanitize_key((string) ($row['optimization_scope'] ?? '')) !== 'content_rewrite') {
                continue;
            }
            $package = json_decode((string) ($row['rewrite_package_json'] ?? '{}'), true);
            if (is_array($package) && (string) ($package['generation_focus'] ?? '') === 'aeo_content_qa') {
                continue;
            }
            $run_id = max($run_id, intval($row['worker_run_id'] ?? 0));
        }
        return $run_id;
    }

    public function get_provider_output_reconciliation(int $project_id, int $run_id): array
    {
        $summary = $this->get_run_summary($project_id, $run_id);
        $reconciliation = isset($summary['provider_output_reconciliation']) && is_array($summary['provider_output_reconciliation'])
            ? $summary['provider_output_reconciliation']
            : array();
        if (intval($reconciliation['run_id'] ?? 0) !== $run_id) {
            return array();
        }
        return $reconciliation;
    }

    public function get_latest_provider_output_run_id(int $project_id): int
    {
        global $wpdb;
        if ($project_id <= 0) {
            return 0;
        }

        // A later non-metadata projection may be the newest run, so inspect a
        // bounded recent window and select the newest run that owns a durable
        // provider-output reconciliation snapshot.
        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from the fixed plugin-owned table name; current reconciliation state must not be cached.
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolation is limited to the plugin-owned table identifier; project_id is prepared.
                "SELECT run_id, summary_json FROM {$this->runs_table} WHERE project_id = %d ORDER BY updated_at DESC, id DESC LIMIT 20",
                $project_id
            ),
            ARRAY_A
        );
        foreach ((array) $rows as $row) {
            $run_id = intval($row['run_id'] ?? 0);
            $summary = json_decode((string) ($row['summary_json'] ?? '{}'), true);
            $reconciliation = is_array($summary) && isset($summary['provider_output_reconciliation']) && is_array($summary['provider_output_reconciliation'])
                ? $summary['provider_output_reconciliation']
                : array();
            if ($run_id > 0 && intval($reconciliation['run_id'] ?? 0) === $run_id) {
                return $run_id;
            }
        }

        return 0;
    }

    /**
     * Return recent durable metadata batches available for review.
     *
     * Each batch remains a separate apply boundary. Callers may switch the
     * visible batch, but must not combine provider outputs from different
     * source runs into one paid write plan.
     *
     * @return array<int,array<string,int|string>>
     */
    public function get_provider_output_run_options(int $project_id, int $limit = 20): array
    {
        global $wpdb;
        if ($project_id <= 0) {
            return array();
        }
        $limit = max(1, min(50, $limit));

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fixed plugin-owned table; current durable review batches must not be cached.
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Interpolation is limited to the plugin-owned table identifier; project_id and limit are prepared.
                "SELECT run_id, summary_json, updated_at FROM {$this->runs_table} WHERE project_id = %d ORDER BY updated_at DESC, id DESC LIMIT %d",
                $project_id,
                $limit
            ),
            ARRAY_A
        );
        $options = array();
        foreach ((array) $rows as $row) {
            $run_id = intval($row['run_id'] ?? 0);
            $summary = json_decode((string) ($row['summary_json'] ?? '{}'), true);
            $reconciliation = is_array($summary) && isset($summary['provider_output_reconciliation']) && is_array($summary['provider_output_reconciliation'])
                ? $summary['provider_output_reconciliation']
                : array();
            if ($run_id <= 0 || intval($reconciliation['run_id'] ?? 0) !== $run_id) {
                continue;
            }
            $reconciliation_rows = is_array($reconciliation['rows'] ?? null) ? $reconciliation['rows'] : array();
            $options[] = array(
                'run_id' => $run_id,
                'generated_count' => max(0, intval($reconciliation['generated_count'] ?? count($reconciliation_rows))),
                'updated_at' => sanitize_text_field((string) ($row['updated_at'] ?? '')),
            );
        }
        return $options;
    }

    /**
     * Read the current lifecycle state for a bounded durable review batch in
     * one query. The generated proposal remains owned by its reconciliation
     * snapshot; mutable apply/readback and current relevance inputs are
     * overlaid by the caller before the proposal is exposed as selectable.
     *
     * @return array<string,array<string,mixed>>
     */
    public function get_inventory_lifecycle_by_entity_keys(int $project_id, array $entity_keys): array
    {
        global $wpdb;
        $entity_keys = array_values(array_unique(array_slice(array_filter(array_map('sanitize_text_field', $entity_keys)), 0, 80)));
        if ($project_id <= 0 || $entity_keys === array()) {
            return array();
        }
        $placeholders = implode(', ', array_fill(0, count($entity_keys), '%s'));
        $args = array_merge(array($project_id), $entity_keys);
        $inventory_table = $this->inventory_table;
        $posts_table = esc_sql($wpdb->posts);
        $query = sprintf(
            "SELECT inventory.entity_key, inventory.entity_id, inventory.object_kind, inventory.post_type,
                    inventory.entity_group, inventory.publication_state, inventory.mapping_state,
                    inventory.main_keyword, inventory.url, inventory.title, inventory.meta_state,
                    inventory.output_state, posts.post_excerpt, posts.post_content
             FROM %s inventory
             LEFT JOIN %s posts
               ON inventory.object_kind = 'post' AND posts.ID = inventory.entity_id
             WHERE inventory.project_id = %%d AND inventory.entity_key IN (%s)",
            $inventory_table,
            $posts_table,
            $placeholders
        );
        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The query contains fixed escaped WordPress/plugin identifiers plus placeholders only; this bounded current-state read must not return a stale cached review.
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifiers are fixed escaped WordPress/plugin tables and the generated placeholder list contains only %s; all values are prepared here.
                $query,
                $args
            ),
            ARRAY_A
        );
        $by_entity = array();
        foreach ((array) $rows as $row) {
            $entity_key = sanitize_text_field((string) ($row['entity_key'] ?? ''));
            if ($entity_key !== '') {
                $by_entity[$entity_key] = array(
                    'entity_id' => intval($row['entity_id'] ?? 0),
                    'object_kind' => sanitize_key((string) ($row['object_kind'] ?? '')),
                    'post_type' => sanitize_key((string) ($row['post_type'] ?? '')),
                    'entity_group' => sanitize_key((string) ($row['entity_group'] ?? '')),
                    'publication_state' => sanitize_key((string) ($row['publication_state'] ?? '')),
                    'mapping_state' => sanitize_key((string) ($row['mapping_state'] ?? '')),
                    'main_keyword' => sanitize_text_field((string) ($row['main_keyword'] ?? '')),
                    'url' => esc_url_raw((string) ($row['url'] ?? '')),
                    'title' => sanitize_text_field((string) ($row['title'] ?? '')),
                    'content_excerpt' => wp_trim_words(
                        wp_strip_all_tags((string) (($row['post_excerpt'] ?? '') !== '' ? $row['post_excerpt'] : ($row['post_content'] ?? ''))),
                        80,
                        ''
                    ),
                    'meta_state' => sanitize_key((string) ($row['meta_state'] ?? '')),
                    'output_state' => sanitize_key((string) ($row['output_state'] ?? '')),
                );
            }
        }
        return $by_entity;
    }

    private function get_run_summary(int $project_id, int $run_id): array
    {
        global $wpdb;
        if ($project_id <= 0 || $run_id <= 0) {
            return array();
        }
        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $summary_json = $wpdb->get_var(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
                "SELECT summary_json FROM {$this->runs_table} WHERE project_id = %d AND run_id = %d LIMIT 1",
                $project_id,
                $run_id
            )
        );
        if (!is_string($summary_json) || $summary_json === '') {
            return array();
        }
        $summary = json_decode($summary_json, true);
        return is_array($summary) ? $summary : array();
    }

    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'] ?? '')));

        $worker_run_id = max(0, intval($filters['worker_run_id'] ?? 0));

        return array_values(array_filter($rows, function (array $row) use ($active_group, $filter, $search, $worker_run_id): 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',
                'content_rewrite_current' => $worker_run_id > 0
                    && (string) ($row['optimization_scope'] ?? '') === 'content_rewrite'
                    && intval($row['worker_run_id'] ?? 0) === $worker_run_id,
                '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;
        }

        // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table identifier is initialized from $wpdb->prefix plus a fixed plugin-owned literal; no request input reaches the identifier. Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
        $existing = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reviewed release SQL: interpolation is limited to escaped or allowlisted plugin-owned table identifiers and generated placeholder lists; all request-derived values are passed to prepare.
            $wpdb->prepare("SELECT * FROM {$this->inventory_table} WHERE project_id = %d AND entity_key = %s LIMIT 1", $project_id, $entity_key),
            ARRAY_A
        );
        $incoming_worker_run_id = isset($row['worker_run_id']) && is_numeric($row['worker_run_id']) ? intval($row['worker_run_id']) : 0;
        $existing_worker_run_id = is_array($existing) ? intval($existing['worker_run_id'] ?? 0) : 0;
        $preserved_meta_package = false;
        if (is_array($existing)
            && $this->nullable_string($row['meta_package_json'] ?? null) === null
            && $this->nullable_string($existing['meta_package_json'] ?? null) !== null) {
            $row['meta_package_json'] = $existing['meta_package_json'];
            $row['meta_state'] = $existing['meta_state'] ?? 'generated';
            $preserved_meta_package = true;
        }
        $preserved_rewrite_package = false;
        foreach (array('rewrite_decision_json', 'rewrite_package_json') as $rewrite_key) {
            if (is_array($existing)
                && $this->nullable_string($row[$rewrite_key] ?? null) === null
                && $this->nullable_string($existing[$rewrite_key] ?? null) !== null) {
                $row[$rewrite_key] = $existing[$rewrite_key];
                $preserved_rewrite_package = true;
            }
        }
        if ($preserved_rewrite_package) {
            foreach (array('rewrite_state', 'decision_reason', 'next_step', 'content_source') as $preserve_key) {
                if ((!array_key_exists($preserve_key, $row) || $this->nullable_string($row[$preserve_key] ?? null) === null)
                    && array_key_exists($preserve_key, $existing)) {
                    $row[$preserve_key] = $existing[$preserve_key];
                }
            }
        }
        if (($preserved_meta_package || $preserved_rewrite_package)
            && (!array_key_exists('optimization_scope', $row) || in_array(sanitize_key((string) ($row['optimization_scope'] ?? '')), array('', 'none'), true))) {
            $row['optimization_scope'] = $existing['optimization_scope'] ?? 'none';
        }
        $existing_has_protected_rewrite = is_array($existing)
            && $existing_worker_run_id > $incoming_worker_run_id
            && in_array(sanitize_key((string) ($existing['rewrite_state'] ?? '')), array('recommended', 'review_required', 'generated'), true)
            && $this->nullable_string($existing['rewrite_package_json'] ?? null) !== null;
        if ($existing_has_protected_rewrite) {
            foreach (array(
                'optimization_scope',
                'rewrite_state',
                'output_state',
                'decision_reason',
                'next_step',
                'content_source',
                'rewrite_decision_json',
                'rewrite_package_json',
                'worker_run_id',
                'last_evaluated_at',
            ) as $preserve_key) {
                if (array_key_exists($preserve_key, $existing)) {
                    $row[$preserve_key] = $existing[$preserve_key];
                }
            }
        }
        $incoming_rewrite_package = is_array($row['rewrite_package_json'] ?? null)
            ? $row['rewrite_package_json']
            : json_decode((string) ($row['rewrite_package_json'] ?? '{}'), true);
        $incoming_is_aeo_preview = sanitize_key((string) ($row['optimization_scope'] ?? '')) === 'aeo_content_qa'
            && is_array($incoming_rewrite_package)
            && (string) ($incoming_rewrite_package['generation_focus'] ?? '') === 'aeo_content_qa';
        $existing_is_content_cycle = is_array($existing)
            && sanitize_key((string) ($existing['optimization_scope'] ?? '')) === 'content_rewrite'
            && $this->nullable_string($existing['rewrite_package_json'] ?? null) !== null;
        if ($incoming_is_aeo_preview && $existing_is_content_cycle) {
            // AEO is a review overlay on the current Content cycle, not a
            // replacement inventory. Its durable package remains available
            // through the completed worker run while the Content row keeps
            // its original package, run identity and quality state.
            foreach (array(
                'optimization_scope',
                'rewrite_state',
                'output_state',
                'decision_reason',
                'next_step',
                'content_source',
                'rewrite_decision_json',
                'rewrite_package_json',
                'worker_run_id',
                'last_evaluated_at',
            ) as $preserve_key) {
                if (array_key_exists($preserve_key, $existing)) {
                    $row[$preserve_key] = $existing[$preserve_key];
                }
            }
        }

        $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')),
        );

        if ($existing) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $wpdb->update($this->inventory_table, $payload, array('id' => intval($existing['id'] ?? 0)));
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reviewed release query uses WordPress or plugin-owned operational tables; current job, billing and idempotency state must not be cached because stale reads can duplicate or mis-order work.
            $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' => false,
            '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'] ?? ''));
        $seo_appendix_html = trim((string) ($package['seo_appendix_html'] ?? ''));

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

    private function detect_url_topic_mismatch(array $inventory, array $rewrite_package): array
    {
        $url = trim((string) ($inventory['url'] ?? $inventory['page_url'] ?? $rewrite_package['url'] ?? ''));
        $keyword = trim((string) ($inventory['main_keyword'] ?? $inventory['primary_keyword'] ?? $rewrite_package['main_keyword'] ?? ''));
        $slug = $this->extract_generation_slug($url);
        $slug_tokens = $this->topic_tokens(str_replace('-', ' ', $slug));
        $keyword_tokens = $this->topic_tokens($keyword);
        if ($slug === '' || count($slug_tokens) < 1 || count($keyword_tokens) < 2) {
            return array('mismatch' => false);
        }

        $overlap = array_values(array_intersect($slug_tokens, $keyword_tokens));
        $ratio = count($overlap) / max(1, count($keyword_tokens));
        $mismatch = $ratio < 0.34;

        return array(
            'mismatch' => $mismatch,
            'current_slug' => $slug,
            'primary_keyword' => $keyword,
            'suggested_slug' => $mismatch ? $this->suggested_topic_slug($keyword) : '',
            'reason' => $mismatch ? 'Current slug is not aligned with the primary topic.' : '',
        );
    }

    private function auto_fix_url_topic_mismatch(array $inventory, array $mismatch): array
    {
        $entity_id = intval($inventory['entity_id'] ?? 0);
        $object_kind = (string) ($inventory['object_kind'] ?? 'post');
        $suggested_slug = sanitize_title((string) ($mismatch['suggested_slug'] ?? ''));
        $old_url = trim((string) ($inventory['url'] ?? $inventory['page_url'] ?? ''));
        if ($entity_id <= 0 || $suggested_slug === '' || $old_url === '') {
            return array('ok' => false, 'status' => 'missing_url_slug_or_entity');
        }

        if ($object_kind === 'term') {
            $taxonomy = (string) ($inventory['taxonomy'] ?? '');
            $term = $taxonomy !== '' ? get_term($entity_id, $taxonomy) : null;
            if (!$term || is_wp_error($term)) {
                return array('ok' => false, 'status' => 'term_not_found');
            }
            $updated = wp_update_term($entity_id, $taxonomy, array('slug' => $suggested_slug));
            if (is_wp_error($updated)) {
                return array('ok' => false, 'status' => 'term_slug_update_failed', 'message' => $updated->get_error_message());
            }
            $new_url = get_term_link($entity_id, $taxonomy);
            if (is_wp_error($new_url)) {
                return array('ok' => false, 'status' => 'term_url_unavailable', 'message' => $new_url->get_error_message());
            }
        } else {
            $post = get_post($entity_id);
            if (!$post instanceof WP_Post) {
                return array('ok' => false, 'status' => 'post_not_found');
            }
            $updated = wp_update_post(array('ID' => $entity_id, 'post_name' => $suggested_slug), true);
            if (is_wp_error($updated)) {
                return array('ok' => false, 'status' => 'post_slug_update_failed', 'message' => $updated->get_error_message());
            }
            $new_url = get_permalink($entity_id);
        }

        $redirect = $this->add_onpage_url_redirect($old_url, (string) $new_url);
        if (empty($redirect['ok'])) {
            return array('ok' => false, 'status' => 'redirect_add_failed', 'redirect' => $redirect);
        }

        return array(
            'ok' => true,
            'status' => 'fixed',
            'old_url' => $old_url,
            'new_url' => (string) $new_url,
            'suggested_slug' => $suggested_slug,
            'redirect' => $redirect,
        );
    }

    private function add_onpage_url_redirect(string $source_url, string $destination_url): array
    {
        $source = $this->normalize_onpage_redirect_path($source_url);
        $destination = $this->normalize_onpage_redirect_path($destination_url);
        if ($source === '' || $destination === '' || $source === $destination) {
            return array('ok' => false, 'status' => 'invalid_redirect_paths');
        }

        $htaccess_file = ABSPATH . '.htaccess';
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Admin-authorized WordPress-root file operations retain a fail-closed writability preflight before the guarded write.
        if (!file_exists($htaccess_file) || !is_writable($htaccess_file)) {
            return array('ok' => false, 'status' => 'htaccess_unavailable');
        }
        $htaccess_content = file_get_contents($htaccess_file);
        if (!is_string($htaccess_content)) {
            return array('ok' => false, 'status' => 'htaccess_read_failed');
        }

        $section_start = '# BEGIN Aysa Redirects';
        $section_end = '# END Aysa Redirects';
        $redirect_rule = 'RedirectMatch 301 ^' . preg_quote($source, '/') . '$ ' . $destination;
        if (strpos($htaccess_content, $redirect_rule) !== false) {
            return array('ok' => true, 'status' => 'redirect_rule_exists', 'redirect_rule' => $redirect_rule);
        }

        if (strpos($htaccess_content, $section_start) !== false) {
            $pattern = '/' . preg_quote($section_start, '/') . '(.*?)' . preg_quote($section_end, '/') . '/s';
            if (preg_match($pattern, $htaccess_content)) {
                $htaccess_content = (string) preg_replace_callback(
                    $pattern,
                    static function (array $matches) use ($section_start, $section_end, $redirect_rule): string {
                        return $section_start . "\n" . (string) ($matches[1] ?? '') . $redirect_rule . "\n" . $section_end;
                    },
                    $htaccess_content
                );
            }
        } else {
            $htaccess_content = $section_start . "\n" . $redirect_rule . "\n" . $section_end . "\n\n" . $htaccess_content;
        }

        // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected -- Admin-authorized redirect operation intentionally updates site-root .htaccess after nonce, capability, path and writability checks.
        if (file_put_contents($htaccess_file, $htaccess_content) === false) {
            return array('ok' => false, 'status' => 'htaccess_write_failed');
        }

        return array('ok' => true, 'status' => 'redirect_added', 'redirect_rule' => $redirect_rule);
    }

    private function normalize_onpage_redirect_path(string $url): string
    {
        $url = trim($url);
        if ($url === '') {
            return '';
        }
        if (preg_match('#^https?://#i', $url) === 1) {
            $path = (string) wp_parse_url($url, PHP_URL_PATH);
            $query = (string) wp_parse_url($url, PHP_URL_QUERY);
            $url = $path !== '' ? $path : '/';
            if ($query !== '') {
                $url .= '?' . $query;
            }
        }
        return '/' . ltrim($url, '/');
    }

    private function topic_tokens(string $text): array
    {
        $text = mb_strtolower($text);
        $parts = preg_split('/[^\p{L}\p{N}]+/u', $text) ?: array();
        $stopwords = array('de', 'cu', 'si', 'și', 'in', 'în', 'la', 'pe', 'pentru', 'un', 'o', 'online', 'florarie', 'floraria');
        $tokens = array();
        foreach ($parts as $part) {
            $part = trim((string) $part);
            if ($part === '' || mb_strlen($part) < 3 || in_array($part, $stopwords, true)) {
                continue;
            }
            $tokens[] = $part;
        }
        return array_values(array_unique($tokens));
    }

    private function suggested_topic_slug(string $keyword): string
    {
        $slug = implode('-', array_slice($this->topic_tokens($keyword), 0, 7));
        $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $slug);
        if (is_string($ascii) && $ascii !== '') {
            $slug = $ascii;
        }
        $slug = strtolower((string) preg_replace('/[^a-z0-9]+/', '-', $slug));
        return trim($slug, '-');
    }

    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 (function_exists('aysa_seo_backend_adapter')) {
            aysa_seo_backend_adapter()->apply_metadata(
                $entity_id,
                (string) ($inventory['object_kind'] ?? 'post') === 'term' ? 'term' : 'post',
                array(
                    'title' => $meta_title,
                    'description' => $meta_description,
                )
            );
            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 read_entity_meta(array $inventory): array
    {
        $entity_id = intval($inventory['entity_id'] ?? 0);
        if ($entity_id <= 0) {
            return array('backend' => '', 'mode' => '', 'meta_title' => '', 'meta_description' => '');
        }

        $is_term = (string) ($inventory['object_kind'] ?? 'post') === 'term';
        if (function_exists('aysa_seo_backend_adapter')) {
            $providerMeta = aysa_seo_backend_adapter()->get_metadata($entity_id, $is_term ? 'term' : 'post');
            if (is_array($providerMeta)) {
                return array(
                    'backend' => (string) ($providerMeta['backend'] ?? 'aysa'),
                    'mode' => (string) ($providerMeta['mode'] ?? 'aysa_owned'),
                    'meta_title' => trim((string) ($providerMeta['meta_title'] ?? '')),
                    'meta_description' => trim((string) ($providerMeta['meta_description'] ?? '')),
                );
            }
        }
        $read = static function($key) use ($entity_id, $is_term) {
            return trim((string) ($is_term ? get_term_meta($entity_id, $key, true) : get_post_meta($entity_id, $key, true)));
        };
        $first = static function(array $keys) use ($read) {
            foreach ($keys as $key) {
                $value = $read($key);
                if ($value !== '') {
                    return $value;
                }
            }
            return '';
        };

        return array(
            'backend' => 'aysa',
            'mode' => 'aysa_owned',
            'meta_title' => $first(array('_aysa_seo_title', '_aysa_meta_title', '_yoast_wpseo_title', 'rank_math_title')),
            'meta_description' => $first(array('_aysa_seo_description', '_aysa_meta_description', '_yoast_wpseo_metadesc', '_yoast_wpseo_desc', 'rank_math_description')),
        );
    }

    private function build_metadata_readback(array $inventory, array $history, array $meta_package, bool $include_public = false): array
    {
        $history_applied = $this->is_history_applied($history);
        $expected_title = trim((string) ($history['meta_title'] ?? ''));
        $expected_description = trim((string) ($history['meta_description'] ?? ''));
        if ($expected_title === '') {
            $expected_title = trim((string) ($meta_package['meta_title'] ?? $meta_package['title'] ?? ''));
        }
        if ($expected_description === '') {
            $expected_description = trim((string) ($meta_package['meta_description'] ?? $meta_package['description'] ?? ''));
        }

        $live = $this->read_entity_meta($inventory);
        $public = $include_public ? $this->read_public_metadata($inventory) : array(
            'status' => 'not_checked',
            'title' => '',
            'meta_description' => '',
        );

        $field_status = array(
            'title' => $this->metadata_field_status($expected_title, (string) ($live['meta_title'] ?? ''), (string) ($public['title'] ?? '')),
            'meta_description' => $this->metadata_field_status($expected_description, (string) ($live['meta_description'] ?? ''), (string) ($public['meta_description'] ?? '')),
        );
        $drift_fields = array();
        foreach ($field_status as $field => $status) {
            if (($status['provider_status'] ?? '') === 'drift' || ($status['public_status'] ?? '') === 'drift') {
                $drift_fields[] = $field;
            }
        }

        $status = 'not_applicable';
        if ($history_applied && ($expected_title !== '' || $expected_description !== '')) {
            $status = $drift_fields !== array() ? 'provider_drift_needs_confirmation' : 'matched';
        } elseif ($expected_title !== '' || $expected_description !== '') {
            $status = 'pending_apply';
        }

        return array(
            'status' => $status,
            'provider' => (string) ($live['backend'] ?? ''),
            'provider_mode' => (string) ($live['mode'] ?? ''),
            'expected' => array(
                'meta_title' => $expected_title,
                'meta_description' => $expected_description,
                'source' => $history_applied ? 'latest_applied_history' : 'meta_package',
                'history_id' => intval($history['id'] ?? 0),
            ),
            'live_provider' => array(
                'meta_title' => (string) ($live['meta_title'] ?? ''),
                'meta_description' => (string) ($live['meta_description'] ?? ''),
            ),
            'public_rendered' => array(
                'status' => (string) ($public['status'] ?? 'not_checked'),
                'title' => (string) ($public['title'] ?? ''),
                'meta_description' => (string) ($public['meta_description'] ?? ''),
                'http_code' => intval($public['http_code'] ?? 0),
            ),
            'field_status' => $field_status,
            'drift_fields' => $drift_fields,
            'client_message' => $status === 'provider_drift_needs_confirmation'
                ? 'AYSA a găsit valori salvate diferit față de ce s-a aplicat anterior. Nu reaplic automat.'
                : '',
            'safe_next_action' => $status === 'provider_drift_needs_confirmation'
                ? 'Reaplică doar rândul selectat, cu confirmare explicită.'
                : '',
            'billing_allowed' => false,
            'readback_only' => true,
        );
    }

    private function metadata_field_status(string $expected, string $provider, string $public): array
    {
        $expected = trim($expected);
        $provider = trim($provider);
        $public = trim($public);
        $provider_status = $expected === '' ? 'not_expected' : ($provider === $expected ? 'matched' : 'drift');
        $public_status = 'not_checked';
        if ($expected !== '' && $public !== '') {
            $public_status = $public === $expected ? 'matched' : 'drift';
        }

        return array(
            'expected' => $expected,
            'provider' => $provider,
            'public' => $public,
            'provider_status' => $provider_status,
            'public_status' => $public_status,
        );
    }

    private function apply_metadata_readback_projection(array $inventory, array $metadata_readback): array
    {
        if ((string) ($metadata_readback['status'] ?? '') !== 'provider_drift_needs_confirmation') {
            if (is_array($metadata_readback) && $metadata_readback !== array()) {
                $inventory['metadata_readback'] = $metadata_readback;
            }
            return $inventory;
        }

        $inventory['metadata_readback'] = $metadata_readback;
        $inventory['meta_state'] = 'provider_drift_needs_confirmation';
        $inventory['output_state'] = 'provider_drift_needs_confirmation';
        $inventory['decision_reason'] = (string) ($metadata_readback['client_message'] ?? 'AYSA a găsit valori salvate diferit față de ce s-a aplicat anterior. Nu reaplic automat.');
        $inventory['next_step'] = 'Confirmă reaplicarea doar pentru acest rând.';
        return $inventory;
    }

    private function read_public_metadata(array $inventory): array
    {
        $url = esc_url_raw((string) ($inventory['url'] ?? ''));
        if ($url === '') {
            return array('status' => 'missing_url', 'title' => '', 'meta_description' => '', 'http_code' => 0);
        }

        $response = wp_remote_get(add_query_arg('aysa_readback', 'onpage_metadata_guard', $url), array(
            'timeout' => 4,
            'redirection' => 3,
            'headers' => array(
                'Cache-Control' => 'no-cache',
                'Pragma' => 'no-cache',
                'User-Agent' => 'AYSA On Page metadata readback',
            ),
        ));
        if (is_wp_error($response)) {
            return array('status' => 'readback_error', 'title' => '', 'meta_description' => '', 'http_code' => 0);
        }

        $code = intval(wp_remote_retrieve_response_code($response));
        $html = (string) wp_remote_retrieve_body($response);
        if ($code < 200 || $code >= 400 || $html === '') {
            return array('status' => 'unavailable', 'title' => '', 'meta_description' => '', 'http_code' => $code);
        }

        $title = '';
        if (preg_match('/<title[^>]*>(.*?)<\/title>/is', $html, $matches)) {
            $title = trim(wp_strip_all_tags(html_entity_decode((string) $matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8')));
        }
        $description = '';
        if (preg_match('/<meta\s+name=["\']description["\']\s+content=["\'](.*?)["\']/is', $html, $matches)) {
            $description = trim(html_entity_decode((string) $matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
        }

        return array(
            'status' => 'checked',
            'title' => $title,
            'meta_description' => $description,
            'http_code' => $code,
        );
    }

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

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

        $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);
        }

        return intval($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;
            }
            $secondary_keywords = isset($update['secondary_keywords']) && is_array($update['secondary_keywords'])
                ? array_values(array_filter(array_map('strval', $update['secondary_keywords'])))
                : array();
            $keyword_family = isset($update['keyword_family']) && is_array($update['keyword_family'])
                ? array_values(array_filter(array_map('strval', $update['keyword_family'])))
                : array();
            if ($keyword_family === array()) {
                $keyword_family = array_values(array_unique(array_merge(array($keyword), $secondary_keywords)));
            }
            $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,
                'secondaryKeywords' => $secondary_keywords,
                'keywordFamily' => $keyword_family,
                'entityKey' => sanitize_text_field((string) ($update['entity_key'] ?? '')),
                'mappingState' => sanitize_key((string) ($update['mapping_state'] ?? $update['mapping_status'] ?? '')),
                'mappingStatus' => sanitize_key((string) ($update['mapping_status'] ?? $update['mapping_state'] ?? '')),
                'reviewStatus' => sanitize_key((string) ($update['review_status'] ?? 'accepted')),
                'mappingSource' => sanitize_key((string) ($update['source'] ?? $update['keyword_source'] ?? 'agent8_v3_keyword_mapping_ai')),
                'confidenceScore' => isset($update['confidence_score']) && is_numeric($update['confidence_score']) ? floatval($update['confidence_score']) : null,
                'targetUrl' => $this->mapping_update_target_url($associated_on, $id_intern),
                'updatedAt' => current_time('mysql'),
            );

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

        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, array $secondary_keywords = array(), array $keyword_family = array()): void
    {
        if ($id_intern <= 0 || $keyword === '') {
            return;
        }

        $assoc = strtolower(trim($associated_on));
        $payload = array(
            'keyword' => $keyword,
            'aysa_keyword' => $keyword,
            'primary_keyword' => $keyword,
            'secondary_keywords' => array_values(array_filter(array_map('strval', $secondary_keywords))),
            'keyword_family' => array_values(array_filter(array_map('strval', $keyword_family))),
        );

        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 remove_local_mapping_for_inventory_row(int $project_id, array $row): void
    {
        if ($project_id <= 0) {
            return;
        }

        $entity_key = trim((string) ($row['entity_key'] ?? ''));
        $associated_on = strtolower(trim($this->resolve_association_type_from_inventory($row)));
        $entity_id = intval($row['entity_id'] ?? 0);
        $option_key = 'aysa_keyword_discovery_local_associations_' . intval($project_id);
        $rows = get_option($option_key, array());
        if (!is_array($rows)) {
            $rows = array();
        }

        $rows = array_values(array_filter($rows, static function ($association) use ($entity_key, $associated_on, $entity_id): bool {
            if (!is_array($association)) {
                return false;
            }
            $association_entity_key = trim((string) ($association['entityKey'] ?? $association['entity_key'] ?? ''));
            $association_on = strtolower(trim((string) ($association['associatedOn'] ?? $association['associated_on'] ?? '')));
            $association_id = intval($association['idIntern'] ?? $association['id_intern'] ?? 0);
            if ($entity_key !== '' && $association_entity_key === $entity_key) {
                return false;
            }
            if ($associated_on !== '' && $entity_id > 0 && $association_on === $associated_on && $association_id === $entity_id) {
                return false;
            }
            return true;
        }));
        update_option($option_key, $rows, false);

        if ($entity_id <= 0) {
            return;
        }
        if (in_array($associated_on, array('term', 'category', 'product_cat', 'product_tag', 'tag'), true)) {
            delete_term_meta($entity_id, 'aysa_keyword_data');
            return;
        }
        delete_post_meta($entity_id, 'aysa_keyword_data');
    }

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

        $assoc = strtolower(trim($associated_on));
        if (in_array($assoc, array('term', 'category', 'product_cat', 'product_tag', 'tag'), true)) {
            $term = get_term($id_intern);
            if ($term instanceof WP_Term) {
                return $this->safe_term_link($term);
            }
            return '';
        }

        $url = get_permalink($id_intern);
        return is_string($url) ? $url : '';
    }

    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) wp_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 should_exclude_redirected_public_url(string $url, string $object_kind, int $entity_id): bool
    {
        $target_url = $this->resolve_public_redirect_target($url);
        if ($target_url === '') {
            return false;
        }

        if ($this->normalize_public_url_for_compare($url) === $this->normalize_public_url_for_compare($target_url)) {
            return false;
        }

        if ($object_kind === 'post') {
            $target_post_id = function_exists('url_to_postid') ? intval(url_to_postid($target_url)) : 0;
            if ($target_post_id > 0 && $target_post_id === $entity_id) {
                return false;
            }
        }

        return true;
    }

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

        $cache_key = 'aysa_onpage_redirect_' . md5($url);
        $cached = get_transient($cache_key);
        if (is_string($cached)) {
            return $cached;
        }

        $current = $url;
        $redirected = false;
        for ($i = 0; $i < 4; $i++) {
            $response = wp_safe_remote_head($current, array(
                'timeout' => 3,
                'redirection' => 0,
                'user-agent' => 'AYSA OnPage Inventory/1.0',
            ));
            if (is_wp_error($response)) {
                set_transient($cache_key, '', HOUR_IN_SECONDS);
                return '';
            }

            $code = intval(wp_remote_retrieve_response_code($response));
            if ($code < 300 || $code >= 400) {
                $target = $redirected ? $current : '';
                set_transient($cache_key, $target, 6 * HOUR_IN_SECONDS);
                return $target;
            }

            $location = trim((string) wp_remote_retrieve_header($response, 'location'));
            if ($location === '') {
                set_transient($cache_key, '', HOUR_IN_SECONDS);
                return '';
            }

            $next = $this->absolute_redirect_url($location, $current);
            if ($next === '' || $next === $current) {
                set_transient($cache_key, '', HOUR_IN_SECONDS);
                return '';
            }
            $current = $next;
            $redirected = true;
        }

        set_transient($cache_key, $current, 6 * HOUR_IN_SECONDS);
        return $current;
    }

    private function absolute_redirect_url(string $location, string $base_url): string
    {
        $location = trim($location);
        if ($location === '') {
            return '';
        }
        if (filter_var($location, FILTER_VALIDATE_URL)) {
            return $location;
        }

        $scheme = (string) wp_parse_url($base_url, PHP_URL_SCHEME);
        $host = (string) wp_parse_url($base_url, PHP_URL_HOST);
        if ($scheme === '' || $host === '') {
            return '';
        }
        if (str_starts_with($location, '//')) {
            return $scheme . ':' . $location;
        }
        if (str_starts_with($location, '/')) {
            return $scheme . '://' . $host . $location;
        }

        $path = (string) wp_parse_url($base_url, PHP_URL_PATH);
        $base_path = preg_replace('~/[^/]*$~', '/', $path);
        return $scheme . '://' . $host . (string) $base_path . $location;
    }

    private function normalize_public_url_for_compare(string $url): string
    {
        $url = trim($url);
        $scheme = strtolower((string) wp_parse_url($url, PHP_URL_SCHEME));
        $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        $path = (string) wp_parse_url($url, PHP_URL_PATH);
        if ($scheme === '' || $host === '') {
            return rtrim($url, '/');
        }
        $path = '/' . ltrim($path, '/');
        return $scheme . '://' . $host . rtrim($path, '/');
    }

    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) wp_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,
        );
    }
}
