<?php

if (!defined('WPINC')) {
    die;
}

class Aysa_Google_OAuth_Manager {
    const NONCE_ACTION = 'aysa_google_oauth_nonce';
    const CONTENT_GEN_QUEUE_HOOK = 'aysa_content_generation_queue_tick';
    const CONTENT_GEN_QUEUE_OPTION_PREFIX = 'aysa_content_gen_queue_';

    private static $instance = null;

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

        return self::$instance;
    }

    private function __construct() {
        add_action('wp_ajax_aysa_google_get_auth_url', array($this, 'ajax_get_auth_url'));
        add_action('wp_ajax_aysa_google_status', array($this, 'ajax_status'));
        add_action('wp_ajax_aysa_google_disconnect', array($this, 'ajax_disconnect'));
        add_action('wp_ajax_aysa_google_save_properties', array($this, 'ajax_save_properties'));
        add_action('wp_ajax_aysa_google_keyword_mappings', array($this, 'ajax_keyword_mappings'));
        add_action('wp_ajax_aysa_keyword_suggestions_get_settings', array($this, 'ajax_keyword_suggestions_get_settings'));
        add_action('wp_ajax_aysa_keyword_suggestions_save_settings', array($this, 'ajax_keyword_suggestions_save_settings'));
        add_action('wp_ajax_aysa_keyword_suggestions_batch', array($this, 'ajax_keyword_suggestions_batch'));
        add_action('wp_ajax_aysa_content_generation_batch', array($this, 'ajax_content_generation_batch'));
        add_action('wp_ajax_aysa_content_generation_enqueue', array($this, 'ajax_content_generation_enqueue'));
        add_action('wp_ajax_aysa_content_generation_status', array($this, 'ajax_content_generation_status'));
        add_action(self::CONTENT_GEN_QUEUE_HOOK, array($this, 'process_content_generation_queue'), 10, 1);
    }

    private function resolve_project_id($posted_project_id = null) {
        $requested_project_id = intval($posted_project_id);
        $bound_project_id = intval(get_option('aysa_project_id', 0));
        if ($bound_project_id <= 0 && defined('AYSA_LOCAL_DEV_PROJECT_ID')) {
            $bound_project_id = intval(AYSA_LOCAL_DEV_PROJECT_ID);
            if ($bound_project_id > 0) {
                update_option('aysa_project_id', $bound_project_id);
            }
        }
        if ($bound_project_id <= 0 || ($requested_project_id > 0 && $requested_project_id !== $bound_project_id)) {
            return 0;
        }
        return $bound_project_id;
    }

    public function ajax_get_auth_url() {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }

        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }

        $return_url = isset($_POST['return_url']) ? esc_url_raw(wp_unslash($_POST['return_url'])) : '';
        $return_url = wp_validate_redirect($return_url, admin_url('admin.php?page=aysa-setup-seo-profile&step=integrations'));
        $payload = array(
            'project_id' => $project_id,
            'wp_user_id' => get_current_user_id(),
            'site_host' => $this->get_site_host(),
            'return_url' => $return_url,
            'scope_profile' => 'seo_readonly',
        );

        $result = $this->broker_request('POST', '/v1/oauth/start', $payload);
        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
            return;
        }

        if (empty($result['auth_url'])) {
            wp_send_json_error('OAuth broker did not return auth URL.');
            return;
        }

        wp_send_json_success(array('auth_url' => esc_url_raw($result['auth_url'])));
    }

    public function ajax_status() {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }

        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }

        $result = $this->broker_request('POST', '/v1/oauth/status', array(
            'project_id' => $project_id,
            'site_host' => $this->get_site_host(),
        ));
        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
            return;
        }

        wp_send_json_success($result);
    }

    /**
     * Server-side status helper used by setup gating logic.
     *
     * @param int $project_id
     * @return array|WP_Error
     */
    public function get_status_for_project($project_id) {
        $project_id = $this->resolve_project_id($project_id);
        if ($project_id <= 0) {
            return new WP_Error('missing_project_id', 'Missing project ID');
        }

        return $this->broker_request('POST', '/v1/oauth/status', array(
            'project_id' => $project_id,
            'site_host' => $this->get_site_host(),
        ));
    }

    public function ajax_save_properties() {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }

        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }

        $selected_gsc_site = isset($_POST['selected_gsc_site']) ? sanitize_text_field(wp_unslash($_POST['selected_gsc_site'])) : '';
        $selected_ga4_property = isset($_POST['selected_ga4_property']) ? sanitize_text_field(wp_unslash($_POST['selected_ga4_property'])) : '';

        $result = $this->broker_request('POST', '/v1/oauth/select-properties', array(
            'project_id' => $project_id,
            'selected_gsc_site' => $selected_gsc_site,
            'selected_ga4_property' => $selected_ga4_property,
        ));
        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
            return;
        }

        wp_send_json_success($result);
    }

    public function ajax_disconnect() {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }

        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }

        $result = $this->broker_request('POST', '/v1/oauth/disconnect', array('project_id' => $project_id));
        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
            return;
        }

        $this->clear_local_integration_state($project_id);
        wp_send_json_success($result);
    }

    private function clear_local_integration_state($project_id) {
        $project_id = intval($project_id);
        if ($project_id > 0) {
            delete_option('aysa_wizard_integrations_project_' . $project_id);
        }

        $wp_user_id = get_current_user_id();
        if ($wp_user_id > 0) {
            delete_option('aysa_wizard_integrations_' . $wp_user_id);
        }

        if (class_exists('Aysa_Auth_Manager')) {
            $auth_status = Aysa_Auth_Manager::get_instance()->is_authenticated();
            $aysa_user_id = isset($auth_status['aysa_user_id']) ? intval($auth_status['aysa_user_id']) : 0;
            if ($aysa_user_id > 0 && $aysa_user_id !== $wp_user_id) {
                delete_option('aysa_wizard_integrations_' . $aysa_user_id);
            }
        }
    }

    public function ajax_keyword_mappings() {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }

        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }

        $days = isset($_POST['days']) ? intval(wp_unslash($_POST['days'])) : 90;
        $limit = isset($_POST['limit']) ? intval(wp_unslash($_POST['limit'])) : 1000;
        $days = max(1, min(90, $days));
        $limit = max(20, min(1000, $limit));

        $site_host = $this->get_site_host();
        $result = $this->broker_request('POST', '/v1/google/keyword-mappings', array(
            'project_id' => $project_id,
            'site_host' => $site_host,
            'days' => $days,
            'limit' => $limit,
        ));
        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
            return;
        }

        wp_send_json_success($result);
    }

    public function ajax_keyword_suggestions_get_settings() {
        check_ajax_referer('aysa_metabox_nonce', 'nonce');
        if (!current_user_can('edit_posts')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }
        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }
        $result = $this->broker_request('POST', '/v1/keywords/settings/get', array(
            'project_id' => $project_id,
        ));
        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
            return;
        }
        wp_send_json_success($result);
    }

    public function ajax_keyword_suggestions_save_settings() {
        check_ajax_referer('aysa_metabox_nonce', 'nonce');
        if (!current_user_can('edit_posts')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }
        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON is decoded, shape-checked and recursively sanitized before use.
        $raw = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '';
        $settings = is_string($raw) ? json_decode($raw, true) : array();
        if (!is_array($settings)) {
            wp_send_json_error('Invalid settings payload');
            return;
        }
        $settings = $this->sanitize_nested_payload($settings);
        $result = $this->broker_request('POST', '/v1/keywords/settings/save', array(
            'project_id' => $project_id,
            'settings' => $settings,
        ));
        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
            return;
        }
        wp_send_json_success($result);
    }

    public function ajax_keyword_suggestions_batch() {
        check_ajax_referer('aysa_metabox_nonce', 'nonce');
        if (!current_user_can('edit_posts')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }

        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded entities are reduced to an explicit typed allowlist by sanitize_entities_for_suggestions.
        $entities_raw = isset($_POST['entities']) ? wp_unslash($_POST['entities']) : '';
        $entities = is_string($entities_raw) ? json_decode($entities_raw, true) : array();
        if (!is_array($entities) || empty($entities)) {
            wp_send_json_error('No entities provided');
            return;
        }

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded keywords are sanitized individually by sanitize_keywords_for_suggestions.
        $existing_keywords_raw = isset($_POST['existing_keywords']) ? wp_unslash($_POST['existing_keywords']) : '';
        $existing_keywords = is_string($existing_keywords_raw) ? json_decode($existing_keywords_raw, true) : array();
        if (!is_array($existing_keywords)) {
            $existing_keywords = array();
        }

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON is decoded, shape-checked and recursively sanitized before use.
        $settings_raw = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '';
        $settings = is_string($settings_raw) ? json_decode($settings_raw, true) : array();
        if (!is_array($settings)) {
            $settings = array();
        }
        $settings = $this->sanitize_nested_payload($settings);

        $payload = array(
            'project_id' => $project_id,
            'site_host' => $this->get_site_host(),
            'entities' => $this->sanitize_entities_for_suggestions($entities),
            'existing_keywords' => $this->sanitize_keywords_for_suggestions($existing_keywords),
            'settings' => $settings,
        );

        $result = $this->broker_request('POST', '/v1/keywords/suggest-unlinked', $payload);
        if (is_wp_error($result)) {
            $fallback = $this->build_local_keyword_suggestions_fallback(
                $project_id,
                $payload,
                $result->get_error_message()
            );
            wp_send_json_success($fallback);
            return;
        }
        // Hybrid safety-net: if broker response contains empty suggestions for some entities,
        // backfill only those entities with local fallback so UI never stalls on "No suggestion above threshold".
        $data = is_array($result) ? $result : array();
        $rows = isset($data['results']) && is_array($data['results']) ? $data['results'] : array();
        if (!empty($rows)) {
            $needs_backfill = false;
            foreach ($rows as $row) {
                if (!is_array($row)) {
                    continue;
                }
                $suggestions = isset($row['suggestions']) && is_array($row['suggestions']) ? $row['suggestions'] : array();
                if (empty($suggestions)) {
                    $needs_backfill = true;
                    break;
                }
            }

            if ($needs_backfill) {
                $fallback = $this->build_local_keyword_suggestions_fallback($project_id, $payload, 'broker_empty_suggestions');
                $fallback_rows = isset($fallback['results']) && is_array($fallback['results']) ? $fallback['results'] : array();
                $fallback_map = array();
                foreach ($fallback_rows as $frow) {
                    if (!is_array($frow)) {
                        continue;
                    }
                    $f_id = (string) ($frow['entity_id'] ?? '');
                    $f_type = $this->normalize_suggestion_entity_group($frow['entity_type'] ?? '');
                    if ($f_id === '' || $f_type === '') {
                        continue;
                    }
                    $fallback_map[$f_type . ':' . $f_id] = $frow;
                }

                foreach ($rows as $idx => $row) {
                    if (!is_array($row)) {
                        continue;
                    }
                    $suggestions = isset($row['suggestions']) && is_array($row['suggestions']) ? $row['suggestions'] : array();
                    if (!empty($suggestions)) {
                        continue;
                    }
                    $r_id = (string) ($row['entity_id'] ?? '');
                    $r_type = $this->normalize_suggestion_entity_group($row['entity_type'] ?? '');
                    $sig = $r_type . ':' . $r_id;
                    if (isset($fallback_map[$sig])) {
                        $rows[$idx]['suggestions'] = isset($fallback_map[$sig]['suggestions']) && is_array($fallback_map[$sig]['suggestions'])
                            ? $fallback_map[$sig]['suggestions']
                            : array();
                        if (empty($rows[$idx]['costs']) && !empty($fallback_map[$sig]['costs'])) {
                            $rows[$idx]['costs'] = $fallback_map[$sig]['costs'];
                        }
                        $rows[$idx]['source'] = 'hybrid_fallback';
                    }
                }

                // Last-resort guarantee: never return a row with empty suggestions.
                // Build a minimal keyword from the row title when both broker + local fallback are empty.
                foreach ($rows as $idx => $row) {
                    if (!is_array($row)) {
                        continue;
                    }
                    $suggestions = isset($row['suggestions']) && is_array($row['suggestions']) ? $row['suggestions'] : array();
                    if (!empty($suggestions)) {
                        continue;
                    }
                    $title = sanitize_text_field((string) ($row['entity_title'] ?? ''));
                    if ($title === '') {
                        continue;
                    }
                    $title_kw = $this->clean_local_keyword_display($title);
                    if ($title_kw === '') {
                        continue;
                    }
                    $rows[$idx]['suggestions'] = array(
                        array(
                            'keyword' => $title_kw,
                            'score' => 15.0,
                            'source' => 'local_title_emergency_fallback',
                        ),
                    );
                    $rows[$idx]['source'] = 'hybrid_fallback';
                }

                $data['results'] = $rows;
                $data['hybrid_fallback'] = true;
            }
        }

        wp_send_json_success($data);
    }

    private function sanitize_entities_for_suggestions(array $entities) {
        $out = array();
        foreach ($entities as $entity) {
            if (!is_array($entity)) {
                continue;
            }
            $id = isset($entity['id']) ? intval($entity['id']) : 0;
            $type = isset($entity['type']) ? sanitize_key((string) $entity['type']) : '';
            if ($id <= 0 || $type === '') {
                continue;
            }

            $content = isset($entity['content']) ? wp_kses_post((string) $entity['content']) : '';
            $excerpt = isset($entity['excerpt']) ? sanitize_textarea_field((string) $entity['excerpt']) : '';

            $categories = array();
            if (isset($entity['categories']) && is_array($entity['categories'])) {
                foreach ($entity['categories'] as $cat) {
                    $cat = sanitize_text_field((string) $cat);
                    if ($cat !== '') {
                        $categories[] = $cat;
                    }
                }
            }

            $out[] = array(
                'id' => $id,
                'type' => $type,
                'title' => sanitize_text_field((string) ($entity['title'] ?? '')),
                'slug' => sanitize_title((string) ($entity['slug'] ?? '')),
                'excerpt' => $excerpt,
                'content' => $content,
                'categories' => $categories,
                'seo_profile' => $this->get_seo_profile_context(),
            );
        }
        return $out;
    }

    /**
     * Sanitize arbitrary settings while preserving numeric and boolean types.
     *
     * @param mixed $value Decoded JSON value.
     * @return mixed
     */
    private function sanitize_nested_payload($value) {
        if (is_array($value)) {
            $sanitized = array();
            foreach ($value as $key => $item) {
                $safe_key = is_int($key) ? $key : sanitize_key((string) $key);
                $sanitized[$safe_key] = $this->sanitize_nested_payload($item);
            }
            return $sanitized;
        }
        if (is_string($value)) {
            return sanitize_textarea_field($value);
        }
        return is_scalar($value) || null === $value ? $value : null;
    }

    private function normalize_suggestion_entity_group($entity_type_raw) {
        $type = sanitize_key((string) $entity_type_raw);
        if ($type === '') {
            return 'other';
        }
        if (strpos($type, 'product') !== false) {
            return 'product';
        }
        if (strpos($type, 'category') !== false || strpos($type, 'cat') !== false) {
            return 'category';
        }
        if ($type === 'post' || strpos($type, 'article') !== false) {
            return 'article';
        }
        if (strpos($type, 'page') !== false) {
            return 'other';
        }
        return 'other';
    }

    private function sanitize_keywords_for_suggestions(array $keywords) {
        $out = array();
        foreach ($keywords as $kw) {
            $kw = sanitize_text_field((string) $kw);
            if ($kw !== '') {
                $out[] = $kw;
            }
        }
        return array_values(array_unique($out));
    }

    private function get_seo_profile_context() {
        return array(
            'industry' => (string) get_option('aysa_industry', ''),
            'project_description' => (string) get_option('aysa_company_description', ''),
            'language' => (string) get_option('aysa_language', get_locale()),
            'location' => (string) get_option('aysa_city', ''),
        );
    }

    private function build_local_keyword_suggestions_fallback($project_id, array $payload, $error_message = '') {
        $entities = isset($payload['entities']) && is_array($payload['entities']) ? $payload['entities'] : array();
        $existing_keywords = isset($payload['existing_keywords']) && is_array($payload['existing_keywords']) ? $payload['existing_keywords'] : array();
        $settings = isset($payload['settings']) && is_array($payload['settings']) ? $payload['settings'] : array();

        $max_keywords = max(1, min(10, intval($settings['max_keywords'] ?? 3)));
        $min_words = max(1, min(6, intval($settings['min_keyword_words'] ?? 2)));
        $max_words = max($min_words, min(8, intval($settings['max_keyword_words'] ?? 4)));
        $min_score = max(0.0, min(100.0, floatval($settings['min_score'] ?? 20.0)));
        $multiplier = max(1.0, min(50.0, floatval($settings['billing_multiplier'] ?? 5.0)));

        $existing_set = array();
        foreach ($existing_keywords as $kw) {
            $k = sanitize_text_field((string) $kw);
            if ($k !== '') {
                $existing_set[mb_strtolower($k, 'UTF-8')] = true;
            }
        }

        $results = array();
        $total_ai = 0.0;
        $total_billable = 0.0;
        $request_id = wp_generate_password(20, false, false);

        foreach ($entities as $entity) {
            if (!is_array($entity)) {
                continue;
            }
            $title = sanitize_text_field((string) ($entity['title'] ?? ''));
            $slug = sanitize_title((string) ($entity['slug'] ?? ''));
            $excerpt = sanitize_textarea_field((string) ($entity['excerpt'] ?? ''));
            $content = wp_strip_all_tags((string) ($entity['content'] ?? ''));
            $categories = isset($entity['categories']) && is_array($entity['categories']) ? $entity['categories'] : array();

            $seed_text = trim(implode(' ', array_filter(array(
                $title,
                str_replace('-', ' ', $slug),
                $excerpt,
                implode(' ', array_map('sanitize_text_field', $categories)),
                $content
            ))));

            $candidates = $this->extract_local_keyword_candidates($seed_text, $min_words, $max_words, 140);
            arsort($candidates);

            $suggestions = array();
            $dedup = array();
            foreach ($candidates as $keyword => $score) {
                $clean_display = $this->clean_local_keyword_display((string) $keyword);
                $norm_existing = mb_strtolower(trim((string) $clean_display), 'UTF-8');
                if ($norm_existing === '' || isset($existing_set[$norm_existing])) {
                    continue;
                }
                if ($score < $min_score) {
                    continue;
                }
                $dedup_key = $this->normalize_local_keyword_for_dedup($clean_display);
                if ($dedup_key === '') {
                    continue;
                }
                $candidate = array(
                    'keyword' => $clean_display,
                    'score' => round($score, 1),
                    'source' => 'local_fallback',
                );
                if (!isset($dedup[$dedup_key])) {
                    $dedup[$dedup_key] = $candidate;
                } else {
                    $dedup[$dedup_key] = $this->pick_better_local_keyword_variant($dedup[$dedup_key], $candidate);
                }
                if (count($dedup) >= ($max_keywords * 3)) {
                    break;
                }
            }

            if (!empty($dedup)) {
                usort($dedup, function ($a, $b) {
                    $sa = floatval($a['score'] ?? 0);
                    $sb = floatval($b['score'] ?? 0);
                    if ($sa === $sb) {
                        return mb_strlen((string) ($a['keyword'] ?? ''), 'UTF-8') <=> mb_strlen((string) ($b['keyword'] ?? ''), 'UTF-8');
                    }
                    return ($sb <=> $sa);
                });
                $suggestions = array_slice(array_values($dedup), 0, $max_keywords);
            } else {
                // Final safeguard: build minimal suggestions from title/slug so rows can still be associated.
                $title_slug_text = trim(implode(' ', array_filter(array(
                    $title,
                    str_replace('-', ' ', $slug),
                ))));
                $fallback_candidates = $this->extract_local_keyword_candidates($title_slug_text, 1, max(1, $max_words), 40);
                arsort($fallback_candidates);
                foreach ($fallback_candidates as $keyword => $score) {
                    $clean_display = $this->clean_local_keyword_display((string) $keyword);
                    $norm_existing = mb_strtolower(trim((string) $clean_display), 'UTF-8');
                    if ($norm_existing === '' || isset($existing_set[$norm_existing])) {
                        continue;
                    }
                    $dedup_key = $this->normalize_local_keyword_for_dedup($clean_display);
                    if ($dedup_key === '') {
                        continue;
                    }
                    $suggestions[] = array(
                        'keyword' => $clean_display,
                        'score' => round(max(15.0, min(95.0, (float) $score)), 1),
                        'source' => 'local_title_slug_fallback',
                    );
                    if (count($suggestions) >= $max_keywords) {
                        break;
                    }
                }

                // Absolute last-resort: a compact keyword from entity title.
                if (empty($suggestions) && $title !== '') {
                    $title_kw = $this->clean_local_keyword_display($title);
                    if ($title_kw !== '') {
                        $suggestions[] = array(
                            'keyword' => $title_kw,
                            'score' => 15.0,
                            'source' => 'local_title_last_resort',
                        );
                    }
                }
            }

            $ai_cost = $this->estimate_local_suggestion_cost($seed_text, count($suggestions));
            $billable_cost = $ai_cost * $multiplier;
            $total_ai += $ai_cost;
            $total_billable += $billable_cost;

            $results[] = array(
                'entity_id' => (string) intval($entity['id'] ?? 0),
                'entity_type' => sanitize_key((string) ($entity['type'] ?? 'other')),
                'entity_title' => $title,
                'suggestions' => $suggestions,
                'costs' => array(
                    'ai_cost_usd' => round($ai_cost, 6),
                    'billable_cost_usd' => round($billable_cost, 6),
                    'multiplier' => $multiplier,
                    'model' => 'local_fallback',
                ),
            );
        }

        $response = array(
            'ok' => true,
            'generated_at' => gmdate('c'),
            'request_id' => $request_id,
            'settings_used' => array(
                'max_keywords' => $max_keywords,
                'min_keyword_words' => $min_words,
                'max_keyword_words' => $max_words,
                'min_score' => $min_score,
                'billing_multiplier' => $multiplier,
            ),
            'totals' => array(
                'ai_cost_usd' => round($total_ai, 6),
                'billable_cost_usd' => round($total_billable, 6),
                'multiplier' => $multiplier,
            ),
            'results' => $results,
            'fallback' => true,
            'fallback_reason' => sanitize_text_field((string) $error_message),
            'fallback_reason_debug' => (string) $error_message,
        );

        $this->record_local_keyword_suggestion_usage(intval($project_id), $response);
        return $response;
    }

    private function extract_local_keyword_candidates($text, $min_words, $max_words, $limit = 120) {
        $raw = mb_strtolower((string) $text, 'UTF-8');
        $raw = preg_replace('/[^\p{L}\p{N}\s]+/u', ' ', $raw);
        $raw = preg_replace('/\s+/u', ' ', $raw);
        $raw = trim((string) $raw);
        if ($raw === '') {
            return array();
        }

        $stop = array_flip(array(
            'si','sau','de','la','din','cu','in','pe','pentru','catre','prin','este','sunt','fara','the','for','and','with','from','this','that','your','our'
        ));
        $tokens = preg_split('/\s+/u', $raw, -1, PREG_SPLIT_NO_EMPTY);
        if (!is_array($tokens) || empty($tokens)) {
            return array();
        }

        $scores = array();
        $total = count($tokens);
        for ($n = $min_words; $n <= $max_words; $n++) {
            if ($n > $total) {
                break;
            }
            for ($i = 0; $i <= $total - $n; $i++) {
                $slice = array_slice($tokens, $i, $n);
                $skip = false;
                foreach ($slice as $tok) {
                    if (mb_strlen($tok, 'UTF-8') <= 1) {
                        $skip = true;
                        break;
                    }
                }
                if ($skip) {
                    continue;
                }
                if (isset($stop[$slice[0]]) || isset($stop[$slice[$n - 1]])) {
                    continue;
                }
                $phrase = trim(implode(' ', $slice));
                if ($phrase === '' || mb_strlen($phrase, 'UTF-8') < 4) {
                    continue;
                }
                $base = 18 + ($n * 7);
                $position_bonus = max(0, 10 - (int) floor($i / 6));
                if (!isset($scores[$phrase])) {
                    $scores[$phrase] = 0.0;
                }
                $scores[$phrase] += $base + $position_bonus;
            }
        }

        if (count($scores) > $limit) {
            arsort($scores);
            $scores = array_slice($scores, 0, $limit, true);
        }
        foreach ($scores as $k => $v) {
            $scores[$k] = min(95.0, (float) $v / 2.0);
        }
        return $scores;
    }

    private function estimate_local_suggestion_cost($text, $keywords_count) {
        $chars = max(0, mb_strlen((string) $text, 'UTF-8'));
        $base = 0.00006 + ($chars / 100000.0) * 0.0024;
        $kw_component = max(0, intval($keywords_count)) * 0.00002;
        return round($base + $kw_component, 6);
    }

    private function clean_local_keyword_display($keyword) {
        $keyword = mb_strtolower(trim((string) $keyword), 'UTF-8');
        $keyword = preg_replace('/\s+/u', ' ', $keyword);
        $tokens = preg_split('/\s+/u', $keyword, -1, PREG_SPLIT_NO_EMPTY);
        if (!is_array($tokens) || empty($tokens)) {
            return '';
        }
        $leading_noise = array_flip(array('un', 'o', 'the', 'a', 'an', 'niste'));
        while (!empty($tokens) && isset($leading_noise[$tokens[0]])) {
            array_shift($tokens);
        }
        if (!empty($tokens)) {
            $tokens[0] = $this->normalize_local_token_base($tokens[0]);
        }
        $out = trim(implode(' ', $tokens));
        return $out;
    }

    private function normalize_local_keyword_for_dedup($keyword) {
        $keyword = $this->clean_local_keyword_display($keyword);
        if ($keyword === '') {
            return '';
        }
        $tokens = preg_split('/\s+/u', $keyword, -1, PREG_SPLIT_NO_EMPTY);
        if (!is_array($tokens) || empty($tokens)) {
            return '';
        }
        $normalized = array();
        foreach ($tokens as $tok) {
            $normalized[] = $this->normalize_local_token_base($tok);
        }
        return trim(implode(' ', $normalized));
    }

    private function normalize_local_token_base($token) {
        $t = mb_strtolower(trim((string) $token), 'UTF-8');
        if (mb_strlen($t, 'UTF-8') < 4) {
            return $t;
        }
        $suffixes = array('ului', 'elor', 'ilor', 'lele', 'lor', 'ile', 'ul', 'le');
        foreach ($suffixes as $suffix) {
            $len = mb_strlen($suffix, 'UTF-8');
            if (mb_substr($t, -$len, null, 'UTF-8') === $suffix && mb_strlen($t, 'UTF-8') > ($len + 2)) {
                $t = mb_substr($t, 0, mb_strlen($t, 'UTF-8') - $len, 'UTF-8');
                break;
            }
        }
        return $t;
    }

    private function pick_better_local_keyword_variant(array $current, array $candidate) {
        $score_current = floatval($current['score'] ?? 0);
        $score_candidate = floatval($candidate['score'] ?? 0);
        if ($score_candidate > $score_current) {
            return $candidate;
        }
        if ($score_candidate < $score_current) {
            return $current;
        }
        $len_current = mb_strlen((string) ($current['keyword'] ?? ''), 'UTF-8');
        $len_candidate = mb_strlen((string) ($candidate['keyword'] ?? ''), 'UTF-8');
        if ($len_candidate < $len_current) {
            return $candidate;
        }
        return $current;
    }

    private function record_local_keyword_suggestion_usage($project_id, array $response) {
        $log = get_option('aysa_keyword_suggestion_usage_local', array());
        if (!is_array($log)) {
            $log = array();
        }
        $log[] = array(
            'timestamp' => gmdate('c'),
            'project_id' => intval($project_id),
            'request_id' => sanitize_text_field((string) ($response['request_id'] ?? '')),
            'totals' => isset($response['totals']) && is_array($response['totals']) ? $response['totals'] : array(),
            'results_count' => isset($response['results']) && is_array($response['results']) ? count($response['results']) : 0,
            'fallback' => !empty($response['fallback']),
        );
        if (count($log) > 500) {
            $log = array_slice($log, -500);
        }
        update_option('aysa_keyword_suggestion_usage_local', $log, false);
    }

    public function ajax_content_generation_batch() {
        try {
            check_ajax_referer('aysa_metabox_nonce', 'nonce');
            if (!current_user_can('edit_posts')) {
                wp_send_json_error('Insufficient permissions');
                return;
            }

            $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
            if ($project_id <= 0) {
                wp_send_json_error('Missing project ID');
                return;
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded entities are reduced to an explicit typed allowlist by sanitize_entities_for_content_generation.
            $entities_raw = isset($_POST['entities']) ? wp_unslash($_POST['entities']) : '';
            $entities = is_string($entities_raw) ? json_decode($entities_raw, true) : array();
            if (!is_array($entities) || empty($entities)) {
                wp_send_json_error('No entities provided');
                return;
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded keywords are sanitized individually by sanitize_keywords_for_suggestions.
            $existing_keywords_raw = isset($_POST['existing_keywords']) ? wp_unslash($_POST['existing_keywords']) : '';
            $existing_keywords = is_string($existing_keywords_raw) ? json_decode($existing_keywords_raw, true) : array();
            if (!is_array($existing_keywords)) {
                $existing_keywords = array();
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON is decoded, shape-checked and recursively sanitized before use.
            $settings_raw = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '';
            $settings = is_string($settings_raw) ? json_decode($settings_raw, true) : array();
            if (!is_array($settings)) {
                $settings = array();
            }
            $settings = $this->sanitize_nested_payload($settings);

            $payload = array(
                'project_id' => $project_id,
                'entities' => array_slice($this->sanitize_entities_for_content_generation($entities), 0, 3),
                'existing_keywords' => $this->sanitize_keywords_for_suggestions($existing_keywords),
                'settings' => $settings,
            );

            $result = $this->build_local_content_generation_batch($project_id, $payload);
            wp_send_json_success($result);
        } catch (\Throwable $e) {
            aysa_debug_log('[AYSA CONTENT GEN] ajax_content_generation_batch fatal: ' . $e->getMessage());
            wp_send_json_error('Content generation failed: ' . $e->getMessage());
        }
    }

    public function ajax_content_generation_enqueue() {
        try {
            check_ajax_referer('aysa_metabox_nonce', 'nonce');
            if (!current_user_can('edit_posts')) {
                wp_send_json_error('Insufficient permissions');
                return;
            }

            $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
            if ($project_id <= 0) {
                wp_send_json_error('Missing project ID');
                return;
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded entities are reduced to an explicit typed allowlist by sanitize_entities_for_content_generation.
            $entities_raw = isset($_POST['entities']) ? wp_unslash($_POST['entities']) : '';
            $entities = is_string($entities_raw) ? json_decode($entities_raw, true) : array();
            if (!is_array($entities) || empty($entities)) {
                wp_send_json_error('No entities provided');
                return;
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded keywords are sanitized individually by sanitize_keywords_for_suggestions.
            $existing_keywords_raw = isset($_POST['existing_keywords']) ? wp_unslash($_POST['existing_keywords']) : '';
            $existing_keywords = is_string($existing_keywords_raw) ? json_decode($existing_keywords_raw, true) : array();
            if (!is_array($existing_keywords)) {
                $existing_keywords = array();
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON is decoded, shape-checked and recursively sanitized before use.
            $settings_raw = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '';
            $settings = is_string($settings_raw) ? json_decode($settings_raw, true) : array();
            if (!is_array($settings)) {
                $settings = array();
            }
            $settings = $this->sanitize_nested_payload($settings);

            $sanitized_entities = array_slice($this->sanitize_entities_for_content_generation($entities), 0, 3);
            if (empty($sanitized_entities)) {
                wp_send_json_error('No valid entities provided');
                return;
            }

            $state = array(
                'job_id' => wp_generate_password(20, false, false),
                'project_id' => $project_id,
                'status' => 'running',
                'created_at' => gmdate('c'),
                'updated_at' => gmdate('c'),
                'finished_at' => '',
                'settings' => $settings,
                'existing_keywords' => $this->sanitize_keywords_for_suggestions($existing_keywords),
                'queue' => $sanitized_entities,
                'total' => count($sanitized_entities),
                'processed' => 0,
                'results' => array(),
                'last_error' => '',
            );

            $this->save_content_generation_queue_state($project_id, $state);
            if (!wp_next_scheduled(self::CONTENT_GEN_QUEUE_HOOK, array($project_id))) {
                wp_schedule_single_event(time() + 1, self::CONTENT_GEN_QUEUE_HOOK, array($project_id));
            }
            if (function_exists('spawn_cron')) {
                spawn_cron();
            }

            wp_send_json_success($this->build_content_generation_status_summary($state));
        } catch (\Throwable $e) {
            aysa_debug_log('[AYSA CONTENT GEN] enqueue fatal: ' . $e->getMessage());
            wp_send_json_error('Content generation enqueue failed: ' . $e->getMessage());
        }
    }

    public function ajax_content_generation_status() {
        check_ajax_referer('aysa_metabox_nonce', 'nonce');
        if (!current_user_can('edit_posts')) {
            wp_send_json_error('Insufficient permissions');
            return;
        }

        $project_id = $this->resolve_project_id(isset($_POST['project_id']) ? absint(wp_unslash($_POST['project_id'])) : null);
        if ($project_id <= 0) {
            wp_send_json_error('Missing project ID');
            return;
        }

        $state = $this->get_content_generation_queue_state($project_id);
        if (!is_array($state)) {
            wp_send_json_error('No active content generation job');
            return;
        }

        $fallback_tick = false;
        $status = sanitize_text_field((string) ($state['status'] ?? ''));
        if ($status === 'running') {
            $updated_at = sanitize_text_field((string) ($state['updated_at'] ?? ''));
            $updated_ts = $updated_at !== '' ? strtotime($updated_at) : 0;
            $stale_seconds = ($updated_ts > 0) ? max(0, time() - intval($updated_ts)) : 9999;
            $next = wp_next_scheduled(self::CONTENT_GEN_QUEUE_HOOK, array($project_id));
            $needs_kick = (!$next) || ($stale_seconds >= 8);

            if ($needs_kick) {
                $fallback_tick = true;
                $this->process_content_generation_queue_internal($project_id, true);
                $state = $this->get_content_generation_queue_state($project_id);
                if (!is_array($state)) {
                    wp_send_json_error('Queue state lost during processing');
                    return;
                }
                if (($state['status'] ?? '') === 'running' && !wp_next_scheduled(self::CONTENT_GEN_QUEUE_HOOK, array($project_id))) {
                    wp_schedule_single_event(time() + 2, self::CONTENT_GEN_QUEUE_HOOK, array($project_id));
                    if (function_exists('spawn_cron')) {
                        spawn_cron();
                    }
                }
            }
        }

        $summary = $this->build_content_generation_status_summary($state);
        $summary['fallback_tick'] = $fallback_tick;
        wp_send_json_success($summary);
    }

    public function process_content_generation_queue($project_id) {
        $this->process_content_generation_queue_internal($project_id, false);
    }

    private function process_content_generation_queue_internal($project_id, $force = false) {
        $project_id = $this->resolve_project_id($project_id);
        if ($project_id <= 0) {
            return;
        }

        $lock_key = 'aysa_content_gen_lock_' . $project_id;
        $lock_value = get_transient($lock_key);
        if (!$force && $lock_value) {
            return;
        }
        if ($force && $lock_value) {
            delete_transient($lock_key);
        }
        set_transient($lock_key, time(), 120);

        try {
            $state = $this->get_content_generation_queue_state($project_id);
            if (!is_array($state) || ($state['status'] ?? '') !== 'running') {
                return;
            }

            $queue = isset($state['queue']) && is_array($state['queue']) ? $state['queue'] : array();
            if (empty($queue)) {
                $state['status'] = 'completed';
                $state['finished_at'] = gmdate('c');
                $state['updated_at'] = gmdate('c');
                $this->save_content_generation_queue_state($project_id, $state);
                return;
            }

            $entity = array_shift($queue);
            $payload = array(
                'project_id' => $project_id,
                'entities' => array($entity),
                'existing_keywords' => isset($state['existing_keywords']) && is_array($state['existing_keywords']) ? $state['existing_keywords'] : array(),
                'settings' => isset($state['settings']) && is_array($state['settings']) ? $state['settings'] : array(),
            );

            $result = $this->build_local_content_generation_batch($project_id, $payload);
            $row_result = array();
            if (is_array($result) && !empty($result['results']) && is_array($result['results'])) {
                $row_result = $result['results'][0] ?? array();
            }
            if (empty($row_result)) {
                $row_result = array(
                    'entity_id' => (string) intval($entity['id'] ?? 0),
                    'entity_type' => sanitize_key((string) ($entity['type'] ?? 'other')),
                    'content_generated' => false,
                    'suggestions' => array(),
                    'message' => 'No result returned for this row.',
                    'costs' => array(
                        'ai_cost_usd' => 0,
                        'billable_cost_usd' => 0,
                        'multiplier' => max(1.0, floatval(($state['settings']['billing_multiplier'] ?? 5))),
                        'model' => 'queue_empty_result',
                    ),
                );
            }

            $state['results'][] = $row_result;
            $state['processed'] = intval($state['processed'] ?? 0) + 1;
            $state['queue'] = $queue;
            $state['updated_at'] = gmdate('c');

            if (!empty($row_result['suggestions']) && is_array($row_result['suggestions'])) {
                $kw_set = array();
                foreach ((array) ($state['existing_keywords'] ?? array()) as $kw) {
                    $kw = sanitize_text_field((string) $kw);
                    if ($kw !== '') {
                        $kw_set[mb_strtolower($kw, 'UTF-8')] = $kw;
                    }
                }
                foreach ($row_result['suggestions'] as $s) {
                    $kw = sanitize_text_field((string) ($s['keyword'] ?? ''));
                    if ($kw !== '') {
                        $kw_set[mb_strtolower($kw, 'UTF-8')] = $kw;
                    }
                }
                $state['existing_keywords'] = array_values($kw_set);
            }

            if (empty($queue) || intval($state['processed']) >= intval($state['total'] ?? 0)) {
                $state['status'] = 'completed';
                $state['finished_at'] = gmdate('c');
            }

            $this->save_content_generation_queue_state($project_id, $state);

            if (($state['status'] ?? '') === 'running') {
                wp_schedule_single_event(time() + 2, self::CONTENT_GEN_QUEUE_HOOK, array($project_id));
            }
        } catch (\Throwable $e) {
            aysa_debug_log('[AYSA CONTENT GEN] queue fatal: ' . $e->getMessage());
            $state = $this->get_content_generation_queue_state($project_id);
            if (is_array($state)) {
                $state['status'] = 'failed';
                $state['last_error'] = $e->getMessage();
                $state['updated_at'] = gmdate('c');
                $state['finished_at'] = gmdate('c');
                $this->save_content_generation_queue_state($project_id, $state);
            }
        } finally {
            delete_transient($lock_key);
        }
    }

    private function get_content_generation_queue_option_name($project_id) {
        return self::CONTENT_GEN_QUEUE_OPTION_PREFIX . intval($project_id);
    }

    private function get_content_generation_queue_state($project_id) {
        $state = get_option($this->get_content_generation_queue_option_name($project_id), null);
        return is_array($state) ? $state : null;
    }

    private function save_content_generation_queue_state($project_id, array $state) {
        update_option($this->get_content_generation_queue_option_name($project_id), $state, false);
    }

    private function build_content_generation_status_summary(array $state) {
        $results = isset($state['results']) && is_array($state['results']) ? $state['results'] : array();
        $totals = array(
            'ai_cost_usd' => 0.0,
            'billable_cost_usd' => 0.0,
            'generated' => 0,
        );
        foreach ($results as $r) {
            if (!empty($r['content_generated'])) {
                $totals['generated'] += 1;
            }
            $cost_ai = isset($r['costs']['ai_cost_usd']) ? floatval($r['costs']['ai_cost_usd']) : 0.0;
            $cost_billable = isset($r['costs']['billable_cost_usd']) ? floatval($r['costs']['billable_cost_usd']) : 0.0;
            $totals['ai_cost_usd'] += $cost_ai;
            $totals['billable_cost_usd'] += $cost_billable;
        }

        return array(
            'job_id' => sanitize_text_field((string) ($state['job_id'] ?? '')),
            'status' => sanitize_text_field((string) ($state['status'] ?? 'unknown')),
            'total' => intval($state['total'] ?? 0),
            'processed' => intval($state['processed'] ?? 0),
            'pending' => max(0, intval($state['total'] ?? 0) - intval($state['processed'] ?? 0)),
            'created_at' => sanitize_text_field((string) ($state['created_at'] ?? '')),
            'updated_at' => sanitize_text_field((string) ($state['updated_at'] ?? '')),
            'finished_at' => sanitize_text_field((string) ($state['finished_at'] ?? '')),
            'last_error' => sanitize_text_field((string) ($state['last_error'] ?? '')),
            'results' => $results,
            'totals' => array(
                'ai_cost_usd' => round($totals['ai_cost_usd'], 6),
                'billable_cost_usd' => round($totals['billable_cost_usd'], 6),
                'generated' => intval($totals['generated']),
            ),
        );
    }

    private function sanitize_entities_for_content_generation(array $entities) {
        $out = array();
        foreach ($entities as $entity) {
            if (!is_array($entity)) {
                continue;
            }
            $id = isset($entity['id']) ? intval($entity['id']) : 0;
            $type = isset($entity['type']) ? sanitize_key((string) $entity['type']) : '';
            if ($id <= 0 || $type === '') {
                continue;
            }
            $content = isset($entity['content']) ? wp_kses_post((string) $entity['content']) : '';
            $excerpt = isset($entity['excerpt']) ? sanitize_textarea_field((string) $entity['excerpt']) : '';

            $categories = array();
            if (isset($entity['categories']) && is_array($entity['categories'])) {
                foreach ($entity['categories'] as $cat) {
                    $cat = sanitize_text_field((string) $cat);
                    if ($cat !== '') {
                        $categories[] = $cat;
                    }
                }
            }

            $preferred_keywords = array();
            if (isset($entity['preferred_keywords']) && is_array($entity['preferred_keywords'])) {
                foreach ($entity['preferred_keywords'] as $kw) {
                    $kw = sanitize_text_field((string) $kw);
                    if ($kw !== '') {
                        $preferred_keywords[] = $kw;
                    }
                }
            }

            $out[] = array(
                'id' => $id,
                'type' => $type,
                'source_type' => sanitize_key((string) ($entity['source_type'] ?? '')),
                'title' => sanitize_text_field((string) ($entity['title'] ?? '')),
                'slug' => sanitize_title((string) ($entity['slug'] ?? '')),
                'url' => esc_url_raw((string) ($entity['url'] ?? '')),
                'excerpt' => $excerpt,
                'content' => $content,
                'categories' => $categories,
                'preferred_keywords' => array_values(array_unique($preferred_keywords)),
                'seo_profile' => $this->get_seo_profile_context(),
            );
        }
        return $out;
    }

    private function build_local_content_generation_batch($project_id, array $payload) {
        $entities = isset($payload['entities']) && is_array($payload['entities']) ? $payload['entities'] : array();
        $existing_keywords = isset($payload['existing_keywords']) && is_array($payload['existing_keywords']) ? $payload['existing_keywords'] : array();
        $settings = isset($payload['settings']) && is_array($payload['settings']) ? $payload['settings'] : array();

        $existing_set = array();
        foreach ($existing_keywords as $kw) {
            $k = sanitize_text_field((string) $kw);
            if ($k !== '') {
                $existing_set[mb_strtolower($k, 'UTF-8')] = true;
            }
        }

        $multiplier = max(1.0, min(50.0, floatval($settings['billing_multiplier'] ?? 5.0)));
        $max_keywords = max(1, min(6, intval($settings['max_keywords'] ?? 3)));
        $min_words = max(1, min(6, intval($settings['min_keyword_words'] ?? 2)));
        $max_words = max($min_words, min(8, intval($settings['max_keyword_words'] ?? 4)));
        $location = sanitize_text_field((string) get_option('aysa_city', ''));
        $brand = sanitize_text_field((string) get_option('aysa_company_name', get_bloginfo('name')));
        if ($brand === '') {
            $brand = sanitize_text_field((string) get_bloginfo('name'));
        }

        $results = array();
        $request_id = wp_generate_password(20, false, false);
        $total_ai = 0.0;
        $total_billable = 0.0;
        $batch_started_at = microtime(true);
        $batch_time_budget_seconds = 65.0;

        foreach ($entities as $entity) {
            if ((microtime(true) - $batch_started_at) >= $batch_time_budget_seconds) {
                $results[] = array(
                    'entity_id' => (string) intval($entity['id'] ?? 0),
                    'entity_type' => sanitize_key((string) ($entity['type'] ?? 'other')),
                    'content_generated' => false,
                    'suggestions' => array(),
                    'costs' => array(
                        'ai_cost_usd' => 0,
                        'billable_cost_usd' => 0,
                        'multiplier' => $multiplier,
                        'model' => 'local_content_skip',
                    ),
                    'message' => 'Skipped: batch time budget reached. Retry for remaining rows.',
                );
                continue;
            }
            $entity_type = sanitize_key((string) ($entity['type'] ?? 'other'));
            $entity_id = intval($entity['id'] ?? 0);
            if ($entity_id <= 0 || $entity_type === 'article') {
                continue;
            }
            if (!$this->is_supported_entity_for_ai_content_generation($entity)) {
                $results[] = array(
                    'entity_id' => (string) $entity_id,
                    'entity_type' => $entity_type,
                    'content_generated' => false,
                    'suggestions' => array(),
                    'costs' => array(
                        'ai_cost_usd' => 0,
                        'billable_cost_usd' => 0,
                        'multiplier' => $multiplier,
                        'model' => 'local_content_skip',
                    ),
                    'message' => 'Skipped: only products, categories and pages are supported for this flow.',
                );
                continue;
            }
            if ($this->should_exclude_entity_from_content_generation($entity)) {
                $results[] = array(
                    'entity_id' => (string) $entity_id,
                    'entity_type' => $entity_type,
                    'content_generated' => false,
                    'suggestions' => array(),
                    'costs' => array(
                        'ai_cost_usd' => 0,
                        'billable_cost_usd' => 0,
                        'multiplier' => $multiplier,
                        'model' => 'local_content_skip',
                    ),
                    'message' => 'Skipped: system/internal page is excluded from content generation.',
                );
                continue;
            }

            $content_raw = (string) ($entity['content'] ?? '');
            $excerpt_raw = (string) ($entity['excerpt'] ?? '');
            $content_text = trim(preg_replace('/\s+/u', ' ', wp_strip_all_tags($content_raw . ' ' . $excerpt_raw)));

            $title = sanitize_text_field((string) ($entity['title'] ?? ''));
            $slug = sanitize_title((string) ($entity['slug'] ?? ''));
            $seed_text = trim(implode(' ', array_filter(array(
                $title,
                str_replace('-', ' ', $slug),
                implode(' ', array_map('sanitize_text_field', (array) ($entity['categories'] ?? array()))),
                $content_text
            ))));

            $suggestions = array();
            $seen = array();
            $preferred_keywords = array();
            if (isset($entity['preferred_keywords']) && is_array($entity['preferred_keywords'])) {
                foreach ($entity['preferred_keywords'] as $kw) {
                    $kw = $this->clean_local_keyword_display((string) $kw);
                    $norm = $this->normalize_local_keyword_for_dedup($kw);
                    if ($kw === '' || $norm === '' || isset($seen[$norm])) {
                        continue;
                    }
                    $seen[$norm] = true;
                    $preferred_keywords[] = $kw;
                    $suggestions[] = array(
                        'keyword' => $kw,
                        'score' => 100.0,
                        'source' => 'existing_association',
                    );
                    if (count($suggestions) >= $max_keywords) {
                        break;
                    }
                }
            }

            if (empty($suggestions)) {
                $fallback_kw = $this->clean_local_keyword_display((string) $title);
                $fallback_norm = $this->normalize_local_keyword_for_dedup($fallback_kw);
                if ($fallback_kw !== '' && $fallback_norm !== '' && !isset($existing_set[mb_strtolower($fallback_kw, 'UTF-8')])) {
                    $suggestions[] = array(
                        'keyword' => $fallback_kw,
                        'score' => 60.0,
                        'source' => 'title_seed',
                    );
                }
            }

            $primary_keyword = !empty($suggestions) ? (string) ($suggestions[0]['keyword'] ?? '') : '';
            $secondary_keywords = array();
            foreach (array_slice($suggestions, 1) as $s) {
                $kw = sanitize_text_field((string) ($s['keyword'] ?? ''));
                if ($kw !== '') {
                    $secondary_keywords[] = $kw;
                }
            }

            $entity_payload = $this->build_ai_content_generation_payload(
                $project_id,
                $entity,
                $title,
                $primary_keyword,
                $secondary_keywords,
                $brand,
                $location,
                $entity_type
            );
            if (!is_array($entity_payload)) {
                $results[] = array(
                    'entity_id' => (string) $entity_id,
                    'entity_type' => $entity_type,
                    'content_generated' => false,
                    'suggestions' => $suggestions,
                    'message' => 'External AI endpoints returned empty or invalid content payload.',
                    'costs' => array(
                        'ai_cost_usd' => 0,
                        'billable_cost_usd' => 0,
                        'multiplier' => $multiplier,
                        'model' => 'ai_endpoints_failed',
                    ),
                );
                continue;
            }

            $save_result = $this->apply_generated_content_to_entity($entity, $entity_payload);
            $alt_result = $this->apply_generated_alt_text_to_entity($entity, $primary_keyword, $title, $brand);

            $ai_cost = $this->estimate_local_suggestion_cost($seed_text, count($suggestions)) + 0.0008;
            $billable_cost = $ai_cost * $multiplier;
            $total_ai += $ai_cost;
            $total_billable += $billable_cost;

            $results[] = array(
                'entity_id' => (string) $entity_id,
                'entity_type' => $entity_type,
                'content_generated' => !is_wp_error($save_result),
                'suggestions' => $suggestions,
                'message' => is_wp_error($save_result) ? $save_result->get_error_message() : 'Content generated and saved.',
                'image_message' => is_wp_error($alt_result) ? $alt_result->get_error_message() : 'Image alt updated.',
                'costs' => array(
                    'ai_cost_usd' => round($ai_cost, 6),
                    'billable_cost_usd' => round($billable_cost, 6),
                    'multiplier' => $multiplier,
                    'model' => (string) ($entity_payload['model'] ?? 'ai_endpoints'),
                ),
            );
        }

        return array(
            'ok' => true,
            'request_id' => $request_id,
            'generated_at' => gmdate('c'),
            'results' => $results,
            'totals' => array(
                'ai_cost_usd' => round($total_ai, 6),
                'billable_cost_usd' => round($total_billable, 6),
                'multiplier' => $multiplier,
            ),
            'settings_used' => array(
                'max_keywords' => $max_keywords,
                'min_keyword_words' => $min_words,
                'max_keyword_words' => $max_words,
                'billing_multiplier' => $multiplier,
            ),
            'fallback' => false,
        );
    }

    private function is_supported_entity_for_ai_content_generation(array $entity) {
        $group = sanitize_key((string) ($entity['type'] ?? ''));
        $source_type = sanitize_key((string) ($entity['source_type'] ?? ''));

        if ($group === 'product') {
            return true;
        }
        if ($group === 'category') {
            return in_array($source_type, array('category', 'product_cat', ''), true);
        }
        if ($group === 'other') {
            return $source_type === 'page';
        }
        return false;
    }

    private function should_exclude_entity_from_content_generation(array $entity) {
        $source_type = sanitize_key((string) ($entity['source_type'] ?? ''));
        $title = mb_strtolower(sanitize_text_field((string) ($entity['title'] ?? '')), 'UTF-8');
        $slug = mb_strtolower(sanitize_title((string) ($entity['slug'] ?? '')), 'UTF-8');
        $url = mb_strtolower(esc_url_raw((string) ($entity['url'] ?? '')), 'UTF-8');

        $title_plain = trim(preg_replace('/\s+/u', ' ', html_entity_decode($title, ENT_QUOTES | ENT_HTML5, 'UTF-8')));
        $slug_plain = trim(str_replace('_', '-', $slug));

        $blocked_slugs = array(
            'cart',
            'mycart',
            'checkout',
            'my-account',
            'myaccount',
            'wishlist',
            'order-pay',
            'order-received',
            'lost-password',
            'reset-password',
        );
        $blocked_titles = array(
            'cart',
            'cos',
            'checkout',
            'my account',
            'contul meu',
            'wishlist',
        );
        $blocked_url_parts = array(
            '/cart',
            '/checkout',
            '/my-account',
            '/myaccount',
            '/wishlist',
            'add-to-cart=',
            '/order-pay/',
            '/order-received/',
            '/lost-password/',
            '/reset-password/',
        );

        if (in_array($slug_plain, $blocked_slugs, true) || in_array($title_plain, $blocked_titles, true)) {
            return true;
        }

        foreach ($blocked_url_parts as $part) {
            if ($part !== '' && strpos($url, $part) !== false) {
                return true;
            }
        }

        if ($source_type === 'other' && ($slug_plain === '' || $title_plain === '')) {
            return true;
        }

        return false;
    }

    private function build_ai_content_generation_payload($project_id, array $entity, $title, $primary_keyword, array $secondary_keywords, $brand, $location, $entity_type) {
        $title = sanitize_text_field((string) $title);
        $primary_keyword = sanitize_text_field((string) $primary_keyword);
        $secondary_keywords = array_values(array_filter(array_map('sanitize_text_field', $secondary_keywords)));
        if ($primary_keyword === '') {
            return null;
        }

        $page_type = $this->map_entity_type_for_ai($entity_type);
        $language = $this->normalize_generation_language((string) get_option('aysa_language', get_locale()));
        $domain = home_url('/');
        $entity_url = esc_url_raw((string) ($entity['url'] ?? ''));
        if ($entity_url === '') {
            $entity_url = $domain;
        }

        $meta_base = array(
            'language' => $language,
            'pageType' => $page_type,
            'domain' => $domain,
            'primaryKeyword' => $primary_keyword,
            'secondaryKeywords' => $secondary_keywords,
            'relatedKeywords' => array(),
            'targetAudience' => (string) get_option('aysa_targeted_se', ''),
            'searchIntent' => (string) get_option('aysa_update_frequency', ''),
            'city' => $location,
            'brandName' => $brand,
            'projectId' => intval($project_id),
        );

        $meta_title_resp = $this->call_ai_generation_api('POST', '/api/chatgpt/seo-meta-title', $meta_base, 8);
        $meta_keywords_resp = $this->call_ai_generation_api('POST', '/api/chatgpt/seo-meta-keywords', array(
            'language' => $language,
            'pageType' => $page_type,
            'domain' => $domain,
            'primaryKeyword' => $primary_keyword,
            'secondaryKeywords' => $secondary_keywords,
            'relatedKeywords' => array(),
            'projectId' => intval($project_id),
        ), 8);
        $meta_description_resp = $this->call_ai_generation_api('POST', '/api/chatgpt/seo-meta-description', $meta_base, 8);

        $title_resp = $this->call_ai_generation_api('GET', '/api/chatgpt/product-or-category-title', array(
            'target_keyword' => $primary_keyword,
            'language' => $language,
            'page_type' => ($page_type === 'blog') ? 'page' : $page_type,
            'projectId' => intval($project_id),
        ), 10);
        $content_request_payload = array(
            'language' => $language,
            'pageType' => $page_type,
            'domain' => $domain,
            'primaryKeyword' => $primary_keyword,
            'secondaryKeywords' => $secondary_keywords,
            'relatedKeywords' => array(),
            'minimumWordCount' => 300,
            'documentOutline' => array(),
            'includeTldr' => false,
            'numberOfImages' => 0,
            'city' => $location,
            'paymentMethods' => array(),
            'deliveryMethods' => array(),
            'tone' => (string) get_option('aysa_tone_of_voice', ''),
            'targetAudience' => (string) get_option('aysa_targeted_se', ''),
            'searchIntent' => (string) get_option('aysa_update_frequency', ''),
            'existingPageContent' => false,
            'pageUrl' => $entity_url,
            'websiteContext' => (string) get_option('aysa_company_description', ''),
            'brandName' => $brand,
            'includeFaq' => true,
            'includeFaqSchema' => true,
            'competitorUrls' => array(),
            'projectId' => intval($project_id),
        );
        $content_resp = $this->call_ai_generation_api('POST', '/api/chatgpt/seo-optimized-content', $content_request_payload, 90);

        $meta_title = $this->extract_ai_meta_value($meta_title_resp, array('seoMetaTitle', 'metaTitle', 'title'));
        $meta_keywords = $this->extract_ai_meta_value($meta_keywords_resp, array('seoMetaKeywords', 'metaKeywords', 'keywords'));
        $meta_description = $this->extract_ai_meta_value($meta_description_resp, array('seoMetaDescription', 'metaDescription', 'description'));
        $entity_title = $this->extract_ai_meta_value($title_resp, array('title', 'productTitle', 'categoryTitle', 'name'));
        $content_html = $this->extract_ai_meta_value($content_resp, array('seoOptimizedContent', 'content', 'htmlContent'));

        if ($content_html === '' && $meta_description === '' && $meta_title === '') {
            return null;
        }

        $sanitized_html = $this->sanitize_ai_content_html((string) $content_html);
        $used_retry_content_generation = false;
        if (!$this->is_ai_content_quality_acceptable($sanitized_html, $entity_type)) {
            $retry_payload = $content_request_payload;
            $retry_payload['minimumWordCount'] = 900;
            $retry_payload['includeTldr'] = true;
            $retry_payload['documentOutline'] = array(
                'Introducere',
                'Beneficii principale',
                'Cum alegi corect',
                'Recomandari practice',
                'Intrebari frecvente',
            );
            $content_resp_retry = $this->call_ai_generation_api('POST', '/api/chatgpt/seo-optimized-content', $retry_payload, 120);
            $content_html_retry = $this->extract_ai_meta_value($content_resp_retry, array('seoOptimizedContent', 'content', 'htmlContent'));
            $sanitized_retry = $this->sanitize_ai_content_html((string) $content_html_retry);
            if ($this->is_ai_content_quality_acceptable($sanitized_retry, $entity_type)) {
                $sanitized_html = $sanitized_retry;
                $used_retry_content_generation = true;
            }
        }
        if ($sanitized_html === '') {
            return null;
        }

        if ($entity_title === '') {
            $entity_title = $title;
        }
        if ($meta_title === '') {
            $meta_title = $entity_title !== '' ? $entity_title : $primary_keyword;
        }
        if ($meta_description === '') {
            $meta_description = sanitize_textarea_field(wp_trim_words(wp_strip_all_tags($sanitized_html), 28, '...'));
        }
        if ($meta_keywords === '') {
            $meta_keywords = implode(', ', array_values(array_filter(array_unique(array_merge(array($primary_keyword), $secondary_keywords)))));
        }

        $meta_title = $this->uppercase_first_utf8($meta_title);
        $meta_description = $this->uppercase_first_utf8($meta_description);

        return array(
            'keyword' => $primary_keyword,
            'second_keyword' => implode(', ', $secondary_keywords),
            'meta_title' => sanitize_text_field($meta_title),
            'meta_description' => sanitize_textarea_field($meta_description),
            'meta_keywords' => sanitize_text_field($meta_keywords),
            'content_html' => $sanitized_html,
            'excerpt' => sanitize_textarea_field(wp_trim_words(wp_strip_all_tags($sanitized_html), 30, '...')),
            'model' => 'chatgpt_seo_optimized_content',
            'generation_trace' => array(
                'provider' => 'ai_endpoints',
                'has_meta_title' => ($meta_title !== ''),
                'has_meta_keywords' => ($meta_keywords !== ''),
                'has_meta_description' => ($meta_description !== ''),
                'has_content' => ($sanitized_html !== ''),
                'has_faq_hint' => (stripos($sanitized_html, 'faq') !== false || stripos($sanitized_html, 'intreb') !== false),
                'content_quality_ok' => $this->is_ai_content_quality_acceptable($sanitized_html, $entity_type),
                'used_retry_content_generation' => $used_retry_content_generation,
            ),
        );
    }

    private function map_entity_type_for_ai($entity_type) {
        $type = sanitize_key((string) $entity_type);
        if ($type === 'product') {
            return 'product';
        }
        if ($type === 'category') {
            return 'category';
        }
        return 'page';
    }

    private function normalize_generation_language($language_raw) {
        $raw = sanitize_text_field((string) $language_raw);
        if ($raw === '') {
            return 'ro';
        }
        if (strpos($raw, '_') !== false) {
            $raw = explode('_', $raw)[0];
        } elseif (strpos($raw, '-') !== false) {
            $raw = explode('-', $raw)[0];
        }
        $raw = strtolower(trim($raw));
        return $raw !== '' ? $raw : 'ro';
    }

    private function get_ai_generation_base_url() {
        if (class_exists('Aysa_API_Config')) {
            $base = Aysa_API_Config::get_base_url();
            if (is_string($base) && trim($base) !== '') {
                return rtrim($base, '/');
            }
        }
        if (defined('AYSA_API_BASE_URL') && is_string(AYSA_API_BASE_URL) && trim(AYSA_API_BASE_URL) !== '') {
            return rtrim((string) AYSA_API_BASE_URL, '/');
        }
        return 'https://api.aysa.ai';
    }

    private function call_ai_generation_api($method, $path, array $payload = array(), $timeout = 8) {
        $method = strtoupper((string) $method);
        $path = '/' . ltrim((string) $path, '/');
        $base_url = $this->get_ai_generation_base_url() . $path;

        $max_attempts = 4;
        $attempt = 0;
        while ($attempt < $max_attempts) {
            $attempt++;
            $url = $base_url;
            $args = array(
                'method' => $method,
                'timeout' => max(2, min(120, intval($timeout))),
                'headers' => array(
                    'Accept' => 'application/json',
                ),
            );

            if ($method === 'GET') {
                if (!empty($payload)) {
                    $url = add_query_arg($payload, $url);
                }
            } else {
                $args['headers']['Content-Type'] = 'application/json';
                $args['body'] = wp_json_encode($payload);
            }

            $resp = wp_remote_request($url, $args);
            if (is_wp_error($resp)) {
                if ($attempt < $max_attempts) {
                    usleep($attempt * 350000);
                    continue;
                }
                return null;
            }

            $code = intval(wp_remote_retrieve_response_code($resp));
            $body = (string) wp_remote_retrieve_body($resp);
            if ($code >= 200 && $code < 300 && trim($body) !== '') {
                $json = json_decode($body, true);
                if (json_last_error() === JSON_ERROR_NONE && is_array($json)) {
                    return $json;
                }
                return $body;
            }

            if ($code === 429 && $attempt < $max_attempts) {
                $retry_delay = 1.0 * $attempt;
                $retry_after = wp_remote_retrieve_header($resp, 'retry-after');
                if ($retry_after !== '') {
                    $retry_after_int = intval($retry_after);
                    if ($retry_after_int > 0) {
                        $retry_delay = min(12.0, max($retry_delay, floatval($retry_after_int)));
                    }
                }
                usleep((int) round($retry_delay * 1000000));
                continue;
            }

            if (($code >= 500 && $code <= 599) && $attempt < $max_attempts) {
                usleep($attempt * 500000);
                continue;
            }

            return null;
        }
        return null;
    }

    private function extract_ai_meta_value($response, array $keys) {
        if (is_string($response)) {
            return $this->normalize_ai_generated_text($response);
        }
        if (!is_array($response)) {
            return '';
        }

        foreach ($keys as $key) {
            if (isset($response[$key])) {
                $direct = $this->extract_text_from_ai_value($response[$key], false);
                if ($direct !== '') {
                    return $direct;
                }
            }
        }

        // Fallback: recursive lookup in nested response structures (e.g. data/result wrappers)
        $recursive = $this->extract_text_from_ai_value($response, true, $keys, 0, 4);
        if ($recursive !== '') {
            return $recursive;
        }
        return '';
    }

    private function extract_text_from_ai_value($value, $allow_recursive = false, array $target_keys = array(), $depth = 0, $max_depth = 4) {
        if (is_string($value)) {
            return $this->normalize_ai_generated_text($value);
        }

        if (is_scalar($value)) {
            $normalized = $this->normalize_ai_generated_text((string) $value);
            return $normalized;
        }

        if (!is_array($value)) {
            return '';
        }

        if (!$allow_recursive || $depth >= $max_depth) {
            $flat = array();
            foreach ($value as $item) {
                if (is_scalar($item)) {
                    $flat[] = (string) $item;
                }
            }
            return $this->normalize_ai_generated_text(implode(', ', array_filter($flat)));
        }

        foreach ($target_keys as $tk) {
            if (isset($value[$tk])) {
                $candidate = $this->extract_text_from_ai_value($value[$tk], true, $target_keys, $depth + 1, $max_depth);
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        foreach ($value as $k => $nested) {
            if (is_string($k) && in_array($k, array('data', 'result', 'payload', 'response', 'output'), true)) {
                $candidate = $this->extract_text_from_ai_value($nested, true, $target_keys, $depth + 1, $max_depth);
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        foreach ($value as $nested) {
            if (is_array($nested)) {
                $candidate = $this->extract_text_from_ai_value($nested, true, $target_keys, $depth + 1, $max_depth);
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        return '';
    }

    private function sanitize_ai_content_html($content_html) {
        $raw = trim((string) $content_html);
        if ($raw === '') {
            return '';
        }

        $normalized = $this->normalize_ai_generated_text($raw);
        if ($normalized === '') {
            return '';
        }

        // If endpoint returned JSON-ish string/object, try to extract content-bearing fields.
        if ((str_starts_with($normalized, '{') && str_ends_with($normalized, '}')) || (str_starts_with($normalized, '[') && str_ends_with($normalized, ']'))) {
            $decoded = json_decode($normalized, true);
            if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
                $nested_content = $this->extract_text_from_ai_value($decoded, true, array('seoOptimizedContent', 'content', 'htmlContent', 'entityContent'), 0, 5);
                if ($nested_content !== '') {
                    $normalized = $nested_content;
                }
            }
        }

        if (strpos($normalized, '<') === false || strpos($normalized, '>') === false) {
            $normalized = wpautop(esc_html($normalized));
        }

        $jsonld_blocks = array();
        $without_jsonld = preg_replace_callback(
            '/<script\b[^>]*type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is',
            function ($matches) use (&$jsonld_blocks) {
                $raw_json = isset($matches[1]) ? trim((string) $matches[1]) : '';
                if ($raw_json === '') {
                    return '';
                }
                $decoded = json_decode($raw_json, true);
                if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
                    return '';
                }
                $jsonld_blocks[] = wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                return '';
            },
            (string) $normalized
        );

        $sanitized = wp_kses_post((string) $without_jsonld);
        if (!empty($jsonld_blocks)) {
            foreach ($jsonld_blocks as $jsonld) {
                if (!is_string($jsonld) || trim($jsonld) === '') {
                    continue;
                }
                $sanitized .= "\n<script type=\"application/ld+json\">" . $jsonld . "</script>";
            }
        }

        return $this->ensure_faq_jsonld_script($sanitized);
    }

    private function ensure_faq_jsonld_script($html) {
        $html = trim((string) $html);
        if ($html === '') {
            return '';
        }
        if (stripos($html, 'application/ld+json') !== false) {
            return $html;
        }

        $faq_items = $this->extract_faq_items_from_html($html);
        if (count($faq_items) < 2) {
            return $html;
        }

        $faq_schema = array(
            '@context' => 'https://schema.org',
            '@type' => 'FAQPage',
            'mainEntity' => array_map(function ($item) {
                return array(
                    '@type' => 'Question',
                    'name' => (string) ($item['question'] ?? ''),
                    'acceptedAnswer' => array(
                        '@type' => 'Answer',
                        'text' => (string) ($item['answer'] ?? ''),
                    ),
                );
            }, $faq_items),
        );

        return rtrim($html) . "\n<script type=\"application/ld+json\">" .
            wp_json_encode($faq_schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) .
            "</script>";
    }

    private function extract_faq_items_from_html($html) {
        $html = (string) $html;
        $items = array();
        $seen = array();

        if (preg_match_all('/<h3[^>]*>(.*?)<\/h3>\s*([\s\S]*?)(?=<h3\b|$)/iu', $html, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $m) {
                $question = trim(wp_strip_all_tags((string) ($m[1] ?? '')));
                $answer_raw = trim((string) ($m[2] ?? ''));
                $answer = trim(preg_replace('/\s+/u', ' ', wp_strip_all_tags($answer_raw)));
                $question = preg_replace('/^\d+[\.\)\-:]\s*/u', '', $question);
                if ($question === '' || $answer === '') {
                    continue;
                }
                $norm = mb_strtolower($question, 'UTF-8');
                if (isset($seen[$norm])) {
                    continue;
                }
                $seen[$norm] = true;
                $items[] = array('question' => $question, 'answer' => $answer);
            }
        }

        // Fallback parser for "<strong>1. Question?</strong> - Answer" style.
        if (count($items) < 2 && preg_match_all('/<strong[^>]*>\s*(?:\d+[\.\)\-:]?\s*)?(.*?)<\/strong>\s*[-–:]\s*([^<]+(?:<(?!\/strong)[^>]*>[^<]*)*)/iu', $html, $strong_matches, PREG_SET_ORDER)) {
            foreach ($strong_matches as $m) {
                $question = trim(preg_replace('/\s+/u', ' ', wp_strip_all_tags((string) ($m[1] ?? ''))));
                $answer = trim(preg_replace('/\s+/u', ' ', wp_strip_all_tags((string) ($m[2] ?? ''))));
                if ($question === '' || $answer === '') {
                    continue;
                }
                $norm = mb_strtolower($question, 'UTF-8');
                if (isset($seen[$norm])) {
                    continue;
                }
                $seen[$norm] = true;
                $items[] = array('question' => $question, 'answer' => $answer);
            }
        }

        return array_slice($items, 0, 12);
    }

    private function is_ai_content_quality_acceptable($html, $entity_type = '') {
        $html = trim((string) $html);
        if ($html === '') {
            return false;
        }

        $plain = trim(preg_replace('/\s+/u', ' ', wp_strip_all_tags($html)));
        $plain_len = mb_strlen($plain, 'UTF-8');
        $paragraph_count = preg_match_all('/<p[\s>]/i', $html);
        $has_heading = (bool) preg_match('/<h[2-4][\s>]/i', $html);

        $type = sanitize_key((string) $entity_type);
        $min_len = 650;
        if ($type === 'category' || $type === 'other' || $type === 'page') {
            $min_len = 800;
        }

        if ($plain_len < $min_len) {
            return false;
        }
        if (!$has_heading && intval($paragraph_count) < 3) {
            return false;
        }
        return true;
    }

    private function normalize_ai_generated_text($value) {
        $text = trim((string) $value);
        if ($text === '') {
            return '';
        }

        // Decode JSON-encoded string payloads, e.g. "\"\\nTitle\\n...\""
        if ((str_starts_with($text, '"') && str_ends_with($text, '"')) ||
            (str_starts_with($text, "'") && str_ends_with($text, "'"))) {
            $decoded_string = json_decode($text, true);
            if (json_last_error() === JSON_ERROR_NONE && is_string($decoded_string)) {
                $text = $decoded_string;
            } else {
                $text = trim($text, "\"'");
            }
        }

        $replacements = array(
            "\\r\\n" => "\n",
            "\\n\\r" => "\n",
            "\\n" => "\n",
            "\\r" => "\n",
            "\\t" => ' ',
            '{{attributes}}' => '',
            '{{ attributes }}' => '',
            '{{attributes }}' => '',
            '{{ attributes}}' => '',
        );
        $text = strtr($text, $replacements);
        $text = str_replace(array("\r\n", "\r"), "\n", $text);

        // Remove orphan lines containing only n/nn left from escaped newlines.
        $text = preg_replace('/(^|\n)\s*n+\s*(?=\n|$)/iu', '$1', $text);

        // Normalize spacing.
        $text = preg_replace('/[ \t]+/u', ' ', $text);
        $text = preg_replace('/\n{3,}/u', "\n\n", $text);

        return trim($text);
    }

    private function uppercase_first_utf8($value) {
        $value = trim((string) $value);
        if ($value === '') {
            return '';
        }
        $first = mb_substr($value, 0, 1, 'UTF-8');
        $rest = mb_substr($value, 1, null, 'UTF-8');
        return mb_strtoupper($first, 'UTF-8') . $rest;
    }

    private function apply_generated_content_to_entity(array $entity, array $payload) {
        $entity_id = intval($entity['id'] ?? 0);
        $group = sanitize_key((string) ($entity['type'] ?? 'other'));
        $source_type = sanitize_key((string) ($entity['source_type'] ?? ''));
        if ($entity_id <= 0) {
            return new WP_Error('invalid_entity', 'Invalid entity id.');
        }

        $is_term_entity = ($group === 'category' || $source_type === 'category' || $source_type === 'product_cat');
        if ($is_term_entity) {
            $taxonomy = ($source_type === 'category') ? 'category' : (($source_type === 'product_cat') ? 'product_cat' : '');
            $term = null;
            if ($taxonomy !== '') {
                $term = get_term($entity_id, $taxonomy);
            }
            if (!$term || is_wp_error($term)) {
                $term = get_term($entity_id);
            }
            if (!$term || is_wp_error($term)) {
                return new WP_Error('term_not_found', 'Term not found for content generation.');
            }

            $term_content = trim((string) ($payload['content_html'] ?? ''));
            if ($term_content === '') {
                $term_content = nl2br(esc_html((string) ($payload['excerpt'] ?? '')));
            }
            $term_content = $this->sanitize_ai_content_html($term_content);
            $term_res = wp_update_term($entity_id, $term->taxonomy, array(
                'description' => $term_content,
            ));
            if (is_wp_error($term_res)) {
                return $term_res;
            }

            $keyword_data = get_term_meta($entity_id, 'aysa_keyword_data', true);
            if (!is_array($keyword_data)) {
                $keyword_data = array();
            }
            $keyword_data = array_merge($keyword_data, array(
                'keyword' => sanitize_text_field((string) ($payload['keyword'] ?? '')),
                'second_keyword' => sanitize_text_field((string) ($payload['second_keyword'] ?? '')),
                'meta_title' => sanitize_text_field((string) ($payload['meta_title'] ?? '')),
                'meta_description' => sanitize_textarea_field((string) ($payload['meta_description'] ?? '')),
                'meta_keywords' => sanitize_text_field((string) ($payload['meta_keywords'] ?? '')),
                'track' => 1,
                'last_updated' => current_time('mysql'),
                'saved_via' => 'content_gen_batch',
            ));
            update_term_meta($entity_id, 'aysa_keyword_data', $keyword_data);
            update_term_meta($entity_id, '_aysa_meta_title', sanitize_text_field((string) ($payload['meta_title'] ?? '')));
            update_term_meta($entity_id, '_aysa_meta_description', sanitize_textarea_field((string) ($payload['meta_description'] ?? '')));
            update_term_meta($entity_id, '_aysa_meta_keywords', sanitize_text_field((string) ($payload['meta_keywords'] ?? '')));
            update_term_meta($entity_id, '_aysa_generated_content', array(
                'source' => 'content_gen_batch',
                'generated_at' => current_time('mysql'),
                'keyword' => sanitize_text_field((string) ($payload['keyword'] ?? '')),
                'trace' => isset($payload['generation_trace']) && is_array($payload['generation_trace']) ? $payload['generation_trace'] : array(),
            ));
            return true;
        }

        $post_obj = get_post($entity_id);
        if ($post_obj && !is_wp_error($post_obj)) {
            $generated_content = trim((string) ($payload['content_html'] ?? ''));
            if ($generated_content === '') {
                $generated_content = '<p>' . esc_html((string) ($payload['excerpt'] ?? '')) . '</p>';
            }
            $generated_content = $this->sanitize_ai_content_html($generated_content);
            $update_data = array(
                'ID' => $entity_id,
                'post_content' => $generated_content,
                'post_excerpt' => sanitize_textarea_field((string) ($payload['excerpt'] ?? '')),
            );
            $res = wp_update_post($update_data, true);
            if (is_wp_error($res)) {
                return $res;
            }

            $elementor_synced = $this->sync_generated_content_to_elementor(
                $entity_id,
                $generated_content,
                sanitize_textarea_field((string) ($payload['excerpt'] ?? ''))
            );
            $keyword_data = get_post_meta($entity_id, 'aysa_keyword_data', true);
            if (!is_array($keyword_data)) {
                $keyword_data = array();
            }
            $keyword_data = array_merge($keyword_data, array(
                'keyword' => sanitize_text_field((string) ($payload['keyword'] ?? '')),
                'second_keyword' => sanitize_text_field((string) ($payload['second_keyword'] ?? '')),
                'meta_title' => sanitize_text_field((string) ($payload['meta_title'] ?? '')),
                'meta_description' => sanitize_textarea_field((string) ($payload['meta_description'] ?? '')),
                'meta_keywords' => sanitize_text_field((string) ($payload['meta_keywords'] ?? '')),
                'track' => 1,
                'last_updated' => current_time('mysql'),
                'saved_via' => 'content_gen_batch',
            ));
            update_post_meta($entity_id, 'aysa_keyword_data', $keyword_data);
            update_post_meta($entity_id, '_aysa_meta_title', sanitize_text_field((string) ($payload['meta_title'] ?? '')));
            update_post_meta($entity_id, '_aysa_meta_description', sanitize_textarea_field((string) ($payload['meta_description'] ?? '')));
            update_post_meta($entity_id, '_aysa_meta_keywords', sanitize_text_field((string) ($payload['meta_keywords'] ?? '')));
            update_post_meta($entity_id, '_aysa_generated_content', array(
                'source' => 'content_gen_batch',
                'generated_at' => current_time('mysql'),
                'keyword' => sanitize_text_field((string) ($payload['keyword'] ?? '')),
                'trace' => isset($payload['generation_trace']) && is_array($payload['generation_trace']) ? $payload['generation_trace'] : array(),
                'elementor_synced' => (bool) $elementor_synced,
            ));
            return true;
        }

        return new WP_Error('unsupported_entity', 'Unsupported entity type for content generation.');
    }

    private function apply_generated_alt_text_to_entity(array $entity, $keyword, $title, $brand) {
        $entity_id = intval($entity['id'] ?? 0);
        if ($entity_id <= 0) {
            return new WP_Error('invalid_entity', 'Invalid entity id.');
        }

        $attachment_id = 0;
        $post_obj = get_post($entity_id);
        if ($post_obj && !is_wp_error($post_obj)) {
            $attachment_id = intval(get_post_thumbnail_id($entity_id));
        } else {
            $source_type = sanitize_key((string) ($entity['source_type'] ?? ''));
            if ($source_type === 'category' || $source_type === 'product_cat' || $source_type === '') {
                $attachment_id = intval(get_term_meta($entity_id, 'thumbnail_id', true));
            }
        }

        if ($attachment_id <= 0) {
            return new WP_Error('no_image', 'No image found for entity.');
        }

        $existing_alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
        if (trim((string) $existing_alt) !== '') {
            return true;
        }

        $keyword = sanitize_text_field((string) $keyword);
        $title = sanitize_text_field((string) $title);
        $brand = sanitize_text_field((string) $brand);

        $attachment_url = wp_get_attachment_url($attachment_id);
        $language = $this->normalize_generation_language((string) get_option('aysa_language', get_locale()));
        $tone = sanitize_text_field((string) get_option('aysa_tone_of_voice', 'professional'));
        $target_audience = sanitize_text_field((string) get_option('aysa_targeted_se', 'general'));

        $secondary_keywords = array();
        $entity_second = sanitize_text_field((string) ($entity['second_keyword'] ?? ''));
        if ($entity_second !== '') {
            foreach (explode(',', $entity_second) as $part) {
                $part = sanitize_text_field(trim((string) $part));
                if ($part !== '') {
                    $secondary_keywords[] = $part;
                }
            }
        }

        $api_payload = array(
            'language' => $language,
            'primaryKeyword' => ($keyword !== '' ? $keyword : $title),
            'secondaryKeywords' => array_values(array_unique($secondary_keywords)),
            'tone' => $tone !== '' ? $tone : 'professional',
            'targetAudience' => $target_audience !== '' ? $target_audience : 'general',
            'imageContext' => $attachment_url ? esc_url_raw($attachment_url) : '',
            'projectId' => intval(get_option('aysa_project_id', 0)),
        );

        $ai_response = null;
        if (!empty($api_payload['imageContext']) && !empty($api_payload['primaryKeyword'])) {
            $ai_response = $this->call_ai_generation_api('POST', '/api/chatgpt/alternative-text-images', $api_payload, 20);
        }

        $parsed = $this->extract_alt_generation_fields($ai_response);
        $alt = sanitize_text_field((string) ($parsed['alt_text'] ?? ''));
        $img_title = sanitize_text_field((string) ($parsed['title'] ?? ''));
        $img_desc = sanitize_textarea_field((string) ($parsed['description'] ?? ''));
        $img_caption = sanitize_textarea_field((string) ($parsed['caption'] ?? ''));

        if ($alt === '') {
            $alt = trim(implode(' ', array_filter(array(
                $keyword !== '' ? $keyword : $title,
                $brand !== '' ? ('- ' . $brand) : ''
            ))));
            if ($alt === '') {
                $alt = 'Imagine optimizata SEO';
            }
        }

        update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt);
        if ($img_title !== '' || $img_desc !== '' || $img_caption !== '') {
            $attachment_update = array('ID' => $attachment_id);
            if ($img_title !== '') {
                $attachment_update['post_title'] = $img_title;
            }
            if ($img_desc !== '') {
                $attachment_update['post_content'] = $img_desc;
            }
            if ($img_caption !== '') {
                $attachment_update['post_excerpt'] = $img_caption;
            }
            if (count($attachment_update) > 1) {
                wp_update_post($attachment_update);
            }
        }
        return true;
    }

    private function extract_alt_generation_fields($response) {
        $result = array(
            'alt_text' => '',
            'title' => '',
            'description' => '',
            'caption' => '',
        );

        if ($response === null || $response === '') {
            return $result;
        }

        if (is_string($response)) {
            $trimmed = trim($response);
            $normalized = $trimmed;
            $decoded_array = null;
            for ($i = 0; $i < 3; $i++) {
                $decoded = json_decode($normalized, true);
                if (json_last_error() === JSON_ERROR_NONE) {
                    if (is_array($decoded)) {
                        $decoded_array = $decoded;
                        break;
                    }
                    if (is_string($decoded)) {
                        $normalized = trim($decoded);
                        continue;
                    }
                }

                $jsonish = $this->normalize_jsonish_string($normalized);
                if ($jsonish !== $normalized) {
                    $normalized = $jsonish;
                    $decoded = json_decode($normalized, true);
                    if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
                        $decoded_array = $decoded;
                        break;
                    }
                }

                $stripped = stripslashes($normalized);
                if ($stripped !== $normalized) {
                    $normalized = $stripped;
                    continue;
                }
                break;
            }

            if (is_array($decoded_array)) {
                $response = $decoded_array;
            } else {
                $raw_extracted = $this->extract_alt_fields_from_raw_string($normalized);
                if (!empty($raw_extracted['alt_text']) || !empty($raw_extracted['title']) || !empty($raw_extracted['description']) || !empty($raw_extracted['caption'])) {
                    return $raw_extracted;
                }
                $result['alt_text'] = $normalized;
                return $result;
            }
        }

        if (!is_array($response)) {
            return $result;
        }

        $pick = function(array $keys) use ($response) {
            foreach ($keys as $key) {
                if (isset($response[$key]) && is_scalar($response[$key])) {
                    $value = trim((string) $response[$key]);
                    if ($value !== '') {
                        return $value;
                    }
                }
            }
            return '';
        };

        $result['alt_text'] = $pick(array('AltText', 'altText', 'alt_text', 'alternativeText', 'alternative_text', 'imageAltText', 'image_alt_text', 'alt', 'text'));
        $result['title'] = $pick(array('Title', 'title', 'imageTitle', 'image_title'));
        $result['description'] = $pick(array('Description', 'description', 'desc', 'imageDescription', 'image_description', 'content'));
        $result['caption'] = $pick(array('AssociatedText', 'associatedText', 'associated_text', 'caption', 'excerpt'));

        if ($result['alt_text'] === '' && isset($response[0])) {
            $first = $response[0];
            if (is_string($first)) {
                $result['alt_text'] = trim($first);
            } elseif (is_array($first)) {
                $nested = $this->extract_alt_generation_fields($first);
                foreach ($nested as $k => $v) {
                    if ($v !== '') {
                        $result[$k] = $v;
                    }
                }
            }
        }

        return $result;
    }

    private function extract_alt_fields_from_raw_string($raw) {
        $result = array(
            'alt_text' => '',
            'title' => '',
            'description' => '',
            'caption' => '',
        );
        $raw = trim((string) $raw);
        if ($raw === '') {
            return $result;
        }

        $patterns = array(
            'alt_text' => '/"(?:alt_text|altText|AltText|alternative_text|alternativeText|image_alt_text|imageAltText|alt|text)"\s*:\s*"((?:\\\\.|[^"\\\\])*)"/i',
            'title' => '/"(?:title|Title|image_title|imageTitle)"\s*:\s*"((?:\\\\.|[^"\\\\])*)"/i',
            'description' => '/"(?:description|Description|desc|image_description|imageDescription|content)"\s*:\s*"((?:\\\\.|[^"\\\\])*)"/i',
            'caption' => '/"(?:associated_text|associatedText|AssociatedText|caption|excerpt)"\s*:\s*"((?:\\\\.|[^"\\\\])*)"/i',
        );

        foreach ($patterns as $field => $pattern) {
            if (preg_match($pattern, $raw, $m)) {
                $value = isset($m[1]) ? stripcslashes((string) $m[1]) : '';
                $value = trim($value);
                if ($value !== '') {
                    $result[$field] = $value;
                }
            }
        }

        return $result;
    }

    private function normalize_jsonish_string($value) {
        $s = trim((string) $value);
        if ($s === '') {
            return '';
        }

        // Unwrap quoted JSON-like payloads.
        if (strlen($s) >= 2 && (($s[0] === '"' && substr($s, -1) === '"') || ($s[0] === "'" && substr($s, -1) === "'"))) {
            $s = substr($s, 1, -1);
        }

        // Convert escaped quotes/newlines and noisy n separators to valid JSON.
        $s = str_replace(array('\\"', "\\'"), array('"', "'"), $s);
        $s = str_replace(array("\\r\\n", "\\n", "\\r"), "\n", $s);
        $s = preg_replace('/\{\s*n\s*/', '{', $s);
        $s = preg_replace('/,\s*n\s*/', ',', $s);
        $s = preg_replace('/\s+n\s*\}/', '}', $s);

        return trim((string) $s);
    }

    private function sync_generated_content_to_elementor($post_id, $content_html, $excerpt = '') {
        $post_id = intval($post_id);
        if ($post_id <= 0 || !class_exists('AYSA_Elementor_Handler')) {
            return false;
        }
        try {
            $handler = AYSA_Elementor_Handler::get_instance();
            if (!$handler || !method_exists($handler, 'is_elementor_active_on_post') || !method_exists($handler, 'update_elementor_content')) {
                return false;
            }
            if (!$handler->is_elementor_active_on_post($post_id)) {
                return false;
            }

            $updated = (bool) $handler->update_elementor_content($post_id, $content_html, 'description');
            if ($excerpt !== '') {
                $handler->update_elementor_content($post_id, $excerpt, 'short_description');
            }
            if (!$updated) {
                $updated = $this->force_sync_elementor_data($post_id, $content_html);
            }
            return $updated;
        } catch (\Throwable $e) {
            aysa_debug_log('[AYSA CONTENT GEN] Elementor sync error for post ' . $post_id . ': ' . $e->getMessage());
            return false;
        }
    }

    private function force_sync_elementor_data($post_id, $content_html) {
        $raw = get_post_meta($post_id, '_elementor_data', true);
        if (!is_string($raw) || trim($raw) === '') {
            return false;
        }

        $data = json_decode($raw, true);
        if (!is_array($data)) {
            return false;
        }

        $updated = false;
        $walker = function (&$elements) use (&$walker, $content_html, &$updated) {
            if (!is_array($elements)) {
                return;
            }
            foreach ($elements as &$element) {
                if (!is_array($element)) {
                    continue;
                }
                $widget = isset($element['widgetType']) ? (string) $element['widgetType'] : '';
                if (!isset($element['settings']) || !is_array($element['settings'])) {
                    $element['settings'] = array();
                }
                if ($widget === 'text-editor') {
                    $element['settings']['editor'] = $content_html;
                    $updated = true;
                } elseif ($widget === 'theme-post-content' || $widget === 'woocommerce-product-content') {
                    $element['settings']['content'] = $content_html;
                    $updated = true;
                } elseif (!$updated) {
                    // Last-resort: first widget with rich text-like key.
                    foreach (array('editor', 'content', 'text') as $candidateKey) {
                        if (array_key_exists($candidateKey, $element['settings'])) {
                            $element['settings'][$candidateKey] = $content_html;
                            $updated = true;
                            break;
                        }
                    }
                }
                if (isset($element['elements']) && is_array($element['elements'])) {
                    $walker($element['elements']);
                }
            }
        };

        $walker($data);
        if (!$updated) {
            return false;
        }

        update_post_meta($post_id, '_elementor_data', wp_slash(wp_json_encode($data)));
        if (defined('ELEMENTOR_VERSION')) {
            update_post_meta($post_id, '_elementor_version', ELEMENTOR_VERSION);
        }
        if (class_exists('\Elementor\Plugin')) {
            try {
                \Elementor\Plugin::$instance->files_manager->clear_cache();
                \Elementor\Plugin::$instance->posts_css_manager->clear_cache();
            } catch (\Throwable $e) {
                aysa_debug_log('[AYSA CONTENT GEN] Elementor cache clear warning for post ' . $post_id . ': ' . $e->getMessage());
            }
        }

        return true;
    }

    private function get_broker_config() {
        $base_url = defined('AYSA_OAUTH_BROKER_BASE_URL')
            ? AYSA_OAUTH_BROKER_BASE_URL
            : (class_exists('Aysa_API_Config') ? Aysa_API_Config::get_base_url() : 'https://api.aysa.ai');
        $key_id = defined('AYSA_OAUTH_BROKER_KEY_ID') ? AYSA_OAUTH_BROKER_KEY_ID : 'wp_plugin';
        $key_secret = defined('AYSA_OAUTH_BROKER_KEY_SECRET') ? AYSA_OAUTH_BROKER_KEY_SECRET : '';

        $base_url = rtrim((string) $base_url, '/');
        $key_id = trim((string) $key_id);
        $key_secret = trim((string) $key_secret);

        if ($base_url === '' || $key_id === '' || $key_secret === '') {
            return new WP_Error('broker_config_missing', 'OAuth broker is not configured on this site.');
        }

        return array(
            'base_url' => $base_url,
            'key_id' => $key_id,
            'key_secret' => $key_secret,
        );
    }

    private function broker_request($method, $path, $payload = array()) {
        $cfg = $this->get_broker_config();
        if (is_wp_error($cfg)) {
            return $cfg;
        }

        $method = strtoupper((string) $method);
        $path = '/' . ltrim((string) $path, '/');
        $url = $cfg['base_url'] . $path;
        $body = ($method === 'POST') ? wp_json_encode($payload) : '';
        if ($body === false) {
            $body = '{}';
        }

        $timestamp = (string) time();
        try {
            $nonce = bin2hex(random_bytes(16));
        } catch (Exception $e) {
            $nonce = wp_generate_password(32, false, false);
        }

        $body_hash = hash('sha256', $body);
        $canonical = implode("\n", array($method, $path, $body_hash, $timestamp, $nonce));
        $signature = hash_hmac('sha256', $canonical, $cfg['key_secret']);

        $headers = array(
            'Content-Type' => 'application/json',
            'X-Aysa-Key' => $cfg['key_id'],
            'X-Aysa-Timestamp' => $timestamp,
            'X-Aysa-Nonce' => $nonce,
            'X-Aysa-Signature' => $signature,
        );

        $args = array(
            'timeout' => 20,
            'method' => $method,
            'headers' => $headers,
        );

        if ($method === 'POST') {
            $args['body'] = $body;
        }

        $resp = wp_remote_request($url, $args);
        if (is_wp_error($resp)) {
            if (function_exists('aysa_report_operational_error')) {
                aysa_report_operational_error(array(
                    'source' => 'plugin',
                    'module' => 'google',
                    'severity' => 'error',
                    'error_code' => 'google_oauth_broker_http_failed',
                    'message_safe' => 'Google OAuth broker request failed.',
                    'action_key' => 'google_oauth_broker_request',
                    'technical_context' => array(
                        'method' => $method,
                        'path' => $path,
                        'wp_error_code' => $resp->get_error_code(),
                        'wp_error_message' => $resp->get_error_message(),
                    ),
                ));
            }
            return new WP_Error('broker_http_failed', 'OAuth broker request failed: ' . $resp->get_error_message());
        }

        $code = wp_remote_retrieve_response_code($resp);
        $data = json_decode((string) wp_remote_retrieve_body($resp), true);
        if (!is_array($data)) {
            if (function_exists('aysa_report_operational_error')) {
                aysa_report_operational_error(array(
                    'source' => 'plugin',
                    'module' => 'google',
                    'severity' => 'error',
                    'error_code' => 'google_oauth_broker_invalid_response',
                    'message_safe' => 'Google OAuth broker returned invalid JSON.',
                    'action_key' => 'google_oauth_broker_request',
                    'technical_context' => array(
                        'method' => $method,
                        'path' => $path,
                        'http_code' => intval($code),
                    ),
                ));
            }
            return new WP_Error('broker_invalid_response', 'OAuth broker returned invalid response.');
        }

        if ($code < 200 || $code >= 300 || (isset($data['ok']) && !$data['ok'])) {
            $msg = isset($data['error']) ? (string) $data['error'] : 'OAuth broker returned error.';
            if (function_exists('aysa_report_operational_error')) {
                aysa_report_operational_error(array(
                    'source' => 'plugin',
                    'module' => 'google',
                    'severity' => 'error',
                    'error_code' => 'google_oauth_broker_error',
                    'message_safe' => 'Google OAuth broker returned an error.',
                    'action_key' => 'google_oauth_broker_request',
                    'technical_context' => array(
                        'method' => $method,
                        'path' => $path,
                        'http_code' => intval($code),
                        'broker_error' => $msg,
                    ),
                ));
            }
            return new WP_Error('broker_error', $msg . ' [code=' . intval($code) . ' url=' . $url . ']');
        }

        return $data;
    }

    private function get_site_host() {
        $host = wp_parse_url(home_url(), PHP_URL_HOST);
        $host = is_string($host) ? $host : '';
        return $host;
    }
}
