<?php

namespace HaoZiTeam\AIPost\Service\Features;

use HaoZiTeam\AIPost\Service\Features\ApiClientService;
use HaoZiTeam\AIPost\Service\Features\Logger;

defined('ABSPATH') || exit;

class EditorGenerateEndpoint
{
    const OPTION_JOBS = 'aipost_editor_generate_jobs';
    const CRON_HOOK = 'aipost_editor_generate_run';

    public static function register(): void
    {
        add_action('rest_api_init', [__CLASS__, 'register_routes']);
        add_action(self::CRON_HOOK, [__CLASS__, 'run_job'], 10, 1);
    }

    private static function detect_language(string $text): string
    {
        $text = trim($text);
        if ($text === '') {
            return 'zh';
        }

        // Devanagari (Hindi, Sanskrit, etc.)
        if (preg_match('/[\x{0900}-\x{097F}]/u', $text)) {
            return 'hi';
        }

        // Spanish: ¿¡ and common accented letters
        if (preg_match('/[¿¡ñÑáÁéÉíÍóÓúÚüÜ]/u', $text)) {
            return 'es';
        }

        // Japanese: Hiragana / Katakana
        if (preg_match('/[\x{3040}-\x{30FF}]/u', $text)) {
            return 'ja';
        }

        // Chinese: CJK Unified Ideographs
        if (preg_match('/[\x{4E00}-\x{9FFF}]/u', $text)) {
            return 'zh';
        }

        return 'en';
    }

    private static function lang_to_task_config(string $lang): array
    {
        return [
            // Editor generate: disable language guard, let AI infer from title
            'disable-language-guard' => true,
            'post-tags-language' => '',
            'custom-language-description' => '',
        ];
    }

    private static function ensure_html_article(string $content): string
    {
        $content = trim((string)$content);
        if ($content === '') {
            return '';
        }

        // Already HTML
        if (stripos($content, '<h2') !== false || stripos($content, '<p') !== false) {
            return $content;
        }

        // Convert Markdown headings to HTML <h2>
        $lines = preg_split('/\r\n|\r|\n/u', $content);
        $out = [];
        foreach ($lines as $line) {
            $l = trim((string)$line);
            if ($l === '') {
                $out[] = '';
                continue;
            }
            if (preg_match('/^#{2,6}\s*(.+)$/u', $l, $m)) {
                $out[] = '<h2>' . esc_html(trim((string)$m[1])) . '</h2>';
                continue;
            }
            $out[] = esc_html($l);
        }

        $html = implode("\n", $out);
        if (function_exists('wpautop')) {
            $html = wpautop($html);
        }
        return trim((string)$html);
    }

    private static function build_keywords_input(string $title, string $content): string
    {
        $plain = trim((string)wp_strip_all_tags((string)$content));

        // Prefer headings as signal; keeps tokens low.
        $headings = [];
        if (preg_match_all('/<h2[^>]*>(.*?)<\/h2>/is', (string)$content, $m)) {
            foreach ($m[1] as $h) {
                $t = trim((string)wp_strip_all_tags((string)$h));
                if ($t !== '') {
                    $headings[] = $t;
                }
            }
        } elseif (preg_match_all('/^#{2,6}\s*(.+)$/mu', (string)$content, $m2)) {
            foreach ($m2[1] as $h) {
                $t = trim((string)$h);
                if ($t !== '') {
                    $headings[] = $t;
                }
            }
        }
        $headings = array_slice(array_values(array_unique($headings)), 0, 10);

        $snippet = mb_substr($plain, 0, 800, 'UTF-8');
        $input = "Title: \"{$title}\"\n";
        if (!empty($headings)) {
            $input .= "Headings: " . implode(' | ', $headings) . "\n";
        }
        $input .= "Snippet: " . $snippet;
        return $input;
    }

    public static function register_routes(): void
    {
        register_rest_route('aipost/v1', '/editor-generate', [
            'methods' => 'POST',
            'callback' => [__CLASS__, 'handle_create'],
            'permission_callback' => [__CLASS__, 'permission_check'],
        ]);

        register_rest_route('aipost/v1', '/editor-title-image', [
            'methods' => 'POST',
            'callback' => [__CLASS__, 'handle_title_image'],
            'permission_callback' => [__CLASS__, 'permission_check'],
        ]);

        register_rest_route('aipost/v1', '/editor-text-to-image', [
            'methods' => 'POST',
            'callback' => [__CLASS__, 'handle_text_to_image'],
            'permission_callback' => [__CLASS__, 'permission_check'],
        ]);

        register_rest_route('aipost/v1', '/editor-generate/status', [
            'methods' => 'GET',
            'callback' => [__CLASS__, 'handle_status'],
            'permission_callback' => [__CLASS__, 'permission_check'],
        ]);

        register_rest_route('aipost/v1', '/editor-generate/cancel', [
            'methods' => 'POST',
            'callback' => [__CLASS__, 'handle_cancel'],
            'permission_callback' => [__CLASS__, 'permission_check'],
        ]);
    }

    public static function handle_title_image(\WP_REST_Request $req)
    {
        $post_id = intval($req->get_param('post_id'));
        $title = trim((string)$req->get_param('title'));
        $w = intval($req->get_param('width'));
        $h = intval($req->get_param('height'));
        if ($post_id <= 0) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'invalid_post_id'], 400);
        }
        if ($title === '') {
            return new \WP_REST_Response(['ok' => false, 'error' => 'empty_title'], 400);
        }

        try {
            // 方式二（标题图）：优先复用任务管理/全局配置。
            // ImageProcessor::generate_thumbnail_with_settings 内部已支持从 get_option('ai-post') 兜底读取。
            $task_settings = [];
            if ($w > 0 && $h > 0) {
                $task_settings['title-image-width'] = $w;
                $task_settings['title-image-height'] = $h;
            }

            $callback = function ($t) {
                $ts = gmdate('YmdHis');
                $rand = '';
                try {
                    $rand = substr(bin2hex(random_bytes(4)), 0, 8);
                } catch (\Throwable $e) {
                    $rand = (string) \wp_rand(10000000, 99999999);
                }
                return $ts . '-' . $rand;
            };

            $attachment = ImageProcessor::generate_thumbnail_with_settings($title, $task_settings, AIPOST_PLUGIN_PATH, $callback);
            if (!is_array($attachment) || empty($attachment['guid'])) {
                return new \WP_REST_Response(['ok' => false, 'error' => 'generate_failed'], 500);
            }

            $url = (string)$attachment['guid'];
            $html = '<figure class="wp-block-image size-large"><img src="' . \esc_url($url) . '" alt="' . \esc_attr($title) . '" /></figure>';

            return new \WP_REST_Response([
                'ok' => true,
                'data' => [
                    'attachment_id' => (int)($attachment['id'] ?? 0),
                    'url' => $url,
                    'html' => $html,
                ],
            ], 200);
        } catch (\Throwable $e) {
            return new \WP_REST_Response(['ok' => false, 'error' => $e->getMessage()], 500);
        }
    }

    /**
     * 编辑器文生图：根据标题生成AI图片并插入正文
     */
    public static function handle_text_to_image(\WP_REST_Request $req)
    {
        $post_id = intval($req->get_param('post_id'));
        $title = trim((string)$req->get_param('title'));
        if ($post_id <= 0) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'invalid_post_id'], 400);
        }
        if ($title === '') {
            return new \WP_REST_Response(['ok' => false, 'error' => 'empty_title'], 400);
        }

        // 延长执行时间
        if (function_exists('set_time_limit')) {
            @set_time_limit(180);
        }

        try {
            $settings = get_option('ai-post', []);
            if (!is_array($settings)) {
                $settings = [];
            }

            // 校验是否至少配置了一个文生图 provider 的 API Key（智谱/豆包/阿里百炼）
            $has_any_key = false;
            foreach (['zhipu-accounts', 'doubao-accounts', 'bai-lian-accounts'] as $_acct_key) {
                $accts = $settings[$_acct_key] ?? [];
                if (is_array($accts)) {
                    foreach ($accts as $_acc) {
                        if (is_array($_acc) && trim((string)($_acc['account-secret-key'] ?? '')) !== '') {
                            $has_any_key = true;
                            break 2;
                        }
                    }
                }
            }
            if (!$has_any_key) {
                return new \WP_REST_Response([
                    'ok' => false,
                    'error' => '请先配置文生图模型的秘钥：当前支持豆包、智谱、阿里百炼，至少配置一个',
                ], 400);
            }

            // 优先使用前端传递的 provider，回退到任务设置或默认值
            $req_provider = trim((string)$req->get_param('provider'));
            $allowed_providers = ['zhipu', 'doubao', 'aliyun'];
            $provider = in_array($req_provider, $allowed_providers, true) ? $req_provider : '';

            // 查找与 provider 匹配的任务设置（用于传递模型/尺寸等配置）
            $task_settings = [];
            $tasks = $settings['tasks'] ?? [];
            if (is_array($tasks)) {
                foreach ($tasks as $task) {
                    if (!is_array($task)) continue;
                    $tp = (string)($task['imagegen-provider'] ?? '');
                    if ($provider !== '' && $tp === $provider) {
                        $task_settings = $task;
                        break;
                    }
                }
            }

            // 如果前端未指定 provider，回退到第一个已配置秘钥的 provider
            if ($provider === '') {
                $fallback_map = [
                    'zhipu'  => 'zhipu-accounts',
                    'doubao' => 'doubao-accounts',
                    'aliyun' => 'bai-lian-accounts',
                ];
                foreach ($fallback_map as $pv => $acct_key) {
                    $accts = $settings[$acct_key] ?? [];
                    if (is_array($accts)) {
                        foreach ($accts as $acc) {
                            if (is_array($acc) && trim((string)($acc['account-secret-key'] ?? '')) !== '') {
                                $provider = $pv;
                                break 2;
                            }
                        }
                    }
                }
            }

            // 生成图片提示词
            $prompt = ImageProcessor::generate_image_prompt_from_title($title, $settings);
            Logger::debug('EditorT2I: provider=' . $provider . ' prompt_len=' . strlen($prompt), 'ImageGen');

            // 根据提供商调用对应的文生图 API
            if ($provider === 'aliyun') {
                $gen = ImageProcessor::generate_image_via_aliyun_dashscope($prompt, $settings);
            } elseif ($provider === 'zhipu') {
                $gen = ImageProcessor::generate_image_via_zhipu($prompt, $settings, $task_settings ?: null);
            } else {
                $gen = ImageProcessor::generate_image_via_volcengine($prompt, $settings, $task_settings ?: null);
            }

            if (is_wp_error($gen)) {
                Logger::warning('EditorT2I: gen failed: ' . $gen->get_error_message(), 'ImageGen');
                return new \WP_REST_Response(['ok' => false, 'error' => '文生图失败：' . $gen->get_error_message()], 500);
            }

            $remote_url = (string)($gen['url'] ?? '');
            if ($remote_url === '') {
                return new \WP_REST_Response(['ok' => false, 'error' => '文生图返回空URL'], 500);
            }

            // 下载图片到媒体库
            $attach = ImageProcessor::attach_remote_image_to_post($post_id, $remote_url, false, $title, 120);
            if (!$attach['ok'] || empty($attach['url'])) {
                // 侧载失败时直接使用远程URL
                Logger::warning('EditorT2I: sideload failed, using remote url', 'ImageSideload');
                $final_url = $remote_url;
            } else {
                $final_url = (string)$attach['url'];
            }

            $html = '<figure class="wp-block-image size-large"><img src="' . \esc_url($final_url) . '" alt="' . \esc_attr($title) . '" /></figure>';

            return new \WP_REST_Response([
                'ok' => true,
                'data' => [
                    'attachment_id' => (int)($attach['attachment_id'] ?? 0),
                    'url' => $final_url,
                    'html' => $html,
                    'provider' => $provider,
                    'model' => (string)($gen['model'] ?? ''),
                ],
            ], 200);
        } catch (\Throwable $e) {
            Logger::error('EditorT2I: exception: ' . $e->getMessage(), 'ImageGen');
            return new \WP_REST_Response(['ok' => false, 'error' => $e->getMessage()], 500);
        }
    }

    public static function permission_check(): bool
    {
        return is_user_logged_in() && current_user_can('edit_posts');
    }

    public static function handle_create(\WP_REST_Request $req)
    {
        // 编辑器同步生成可能较慢（例如 Spark X1 / Pro / Max）。
        // 避免在请求过程中触发 PHP 默认 max_execution_time 导致“此站点遇到了致命错误”。
        if (function_exists('ignore_user_abort')) {
            @ignore_user_abort(true);
        }
        if (function_exists('set_time_limit')) {
            @set_time_limit(0);
        }

        $post_id = intval($req->get_param('post_id'));
        $title = trim((string)$req->get_param('title'));
        $force = (bool)$req->get_param('force');
        $model = trim((string)$req->get_param('model'));
        $sync = $req->get_param('sync');
        $sync = ($sync === null) ? true : (bool)$sync;

        if ($post_id <= 0) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'post_id_required'], 400);
        }
        if (!current_user_can('edit_post', $post_id)) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'forbidden'], 403);
        }

        $post = get_post($post_id);
        if (!$post) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'post_not_found'], 404);
        }

        if ($title === '') {
            $title = (string)$post->post_title;
        }
        if ($title === '') {
            return new \WP_REST_Response(['ok' => false, 'error' => 'title_required'], 400);
        }

        $lock_key = 'aipost_editor_generate_lock_' . $post_id;
        if (get_transient($lock_key)) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'job_already_running'], 409);
        }
        set_transient($lock_key, 1, 10 * MINUTE_IN_SECONDS);

        $job_id = self::new_job_id();
        $job = [
            'job_id' => $job_id,
            'post_id' => $post_id,
            'user_id' => get_current_user_id(),
            'status' => 'queued',
            'step' => 'queued',
            'progress' => 0,
            'message' => '排队中',
            'title' => $title,
            'force' => $force ? 1 : 0,
            'model' => $model,
            'result' => null,
            'error' => null,
            'created_at' => current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ];

        self::update_job($job_id, $job);

        // 编辑器交互：默认同步执行，避免前端先看到 queued/排队。
        if ($sync) {
            // 先写入一个更贴近真实的状态，避免“排队中”在日志/状态里出现。
            $job['status'] = 'running';
            $job['step'] = 'prepare';
            $job['progress'] = 3;
            $job['message'] = '准备生成...';
            $job['updated_at'] = current_time('mysql');
            self::update_job($job_id, $job);

            self::run_job($job_id);
            $final = self::get_job($job_id);
            if (!$final) {
                self::release_lock($post_id);
                return new \WP_REST_Response(['ok' => false, 'error' => 'job_not_found_after_run'], 500);
            }
            return new \WP_REST_Response(['ok' => true, 'data' => ['job' => $final, 'job_id' => $job_id]], 200);
        }

        // async：继续走队列/cron（用于兼容旧行为或特殊场景）。
        $enqueued = false;
        try {
            if (class_exists('HaoZiTeam\\AIPost\\Service\\Features\\PriorityQueue')) {
                $enqueued = PriorityQueue::enqueue_editor_generate($job_id);
            }
        } catch (\Throwable $e) {
            $enqueued = false;
        }

        // 兜底：如果队列不可用或入队失败，仍按旧逻辑直接排一个 job cron。
        if (!$enqueued) {
            if (!wp_next_scheduled(self::CRON_HOOK, [$job_id])) {
                wp_schedule_single_event(time() + 1, self::CRON_HOOK, [$job_id]);
            }

            // 主动触发 WP-Cron，避免必须等待下一次访问才执行，导致界面长时间停在“排队中”。
            // 使用 transient 节流，避免频繁触发。
            try {
                $spawn_guard = 'aipost_editor_spawn_guard_' . md5((string)$job_id);
                if (!function_exists('get_transient') || !get_transient($spawn_guard)) {
                    if (function_exists('set_transient')) {
                        set_transient($spawn_guard, 1, 30);
                    }
                    if (function_exists('spawn_cron')) {
                        @spawn_cron(time() + 1);
                    }
                }
            } catch (\Throwable $e) {
                // ignore
            }
        }

        return new \WP_REST_Response(['ok' => true, 'data' => ['job_id' => $job_id]], 200);
    }

    public static function handle_status(\WP_REST_Request $req)
    {
        $job_id = trim((string)$req->get_param('job_id'));
        if ($job_id === '') {
            return new \WP_REST_Response(['ok' => false, 'error' => 'job_id_required'], 400);
        }

        $job = self::get_job($job_id);
        if (!$job) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'job_not_found'], 404);
        }

        $post_id = intval($job['post_id'] ?? 0);
        if ($post_id > 0 && !current_user_can('edit_post', $post_id)) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'forbidden'], 403);
        }

        return new \WP_REST_Response(['ok' => true, 'data' => $job], 200);
    }

    public static function handle_cancel(\WP_REST_Request $req)
    {
        $job_id = trim((string)$req->get_param('job_id'));
        if ($job_id === '') {
            return new \WP_REST_Response(['ok' => false, 'error' => 'job_id_required'], 400);
        }

        $job = self::get_job($job_id);
        if (!$job) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'job_not_found'], 404);
        }

        $post_id = intval($job['post_id'] ?? 0);
        if ($post_id > 0 && !current_user_can('edit_post', $post_id)) {
            return new \WP_REST_Response(['ok' => false, 'error' => 'forbidden'], 403);
        }

        if (in_array((string)($job['status'] ?? ''), ['success', 'failed', 'canceled'], true)) {
            return new \WP_REST_Response(['ok' => true, 'data' => $job], 200);
        }

        $job['status'] = 'canceled';
        $job['step'] = 'canceled';
        $job['progress'] = 100;
        $job['message'] = '已取消';
        $job['updated_at'] = current_time('mysql');
        self::update_job($job_id, $job);

        self::release_lock($post_id);

        return new \WP_REST_Response(['ok' => true, 'data' => $job], 200);
    }

    public static function run_job(string $job_id): void
    {
        $job = self::get_job($job_id);
        if (!$job) {
            return;
        }

        if (function_exists('ignore_user_abort')) {
            @ignore_user_abort(true);
        }
        if (function_exists('set_time_limit')) {
            @set_time_limit(0);
        }

        $post_id = intval($job['post_id'] ?? 0);
        if ($post_id <= 0) {
            self::fail_job($job_id, 'post_id_invalid');
            return;
        }

        if (($job['status'] ?? '') === 'canceled') {
            self::release_lock($post_id);
            return;
        }

        try {
            self::set_job_state($job_id, 'running', 'prepare', 5, '准备生成...');

            $post = get_post($post_id);
            if (!$post) {
                self::fail_job($job_id, 'post_not_found');
                self::release_lock($post_id);
                return;
            }

            $title = trim((string)($job['title'] ?? ''));
            if ($title === '') {
                $title = trim((string)$post->post_title);
            }
            if ($title === '') {
                self::fail_job($job_id, 'title_required');
                self::release_lock($post_id);
                return;
            }

            $lang = self::detect_language($title);

            $settings = get_option('ai-post', []);
            if (!is_array($settings)) {
                $settings = [];
            }

            $task_config = self::lang_to_task_config($lang);

            $overrideProvider = '';
            $overrideModel = '';
            $requested = trim((string)($job['model'] ?? ''));
            if ($requested !== '') {
                $allow = [];
                try {
                    $uiModels = EditorGenerateUI::build_model_options();
                    $allow = isset($uiModels['allow']) && is_array($uiModels['allow']) ? $uiModels['allow'] : [];
                } catch (\Throwable $e) {
                    $allow = [];
                }

                if (in_array($requested, $allow, true)) {
                    // 支持 multi provider 格式：provider::model
                    if (strpos($requested, '::') !== false) {
                        $parts = explode('::', $requested, 2);
                        $p = trim((string)($parts[0] ?? ''));
                        $m = trim((string)($parts[1] ?? ''));
                        if ($p !== '' && $m !== '') {
                            $overrideProvider = $p;
                            $overrideModel = $m;
                        }
                    } else {
                        // 兼容旧格式：仅 model
                        $overrideModel = $requested;
                    }
                }
            }

            if ($overrideProvider !== '') {
                $task_config['task-api-server-type'] = $overrideProvider;
            }

            $client = ApiClientService::create_api_client($settings, 'editor', $task_config, $overrideModel);
            if (!is_object($client)) {
                $providerTip = $overrideProvider !== '' ? $overrideProvider : (string)($settings['api-server-type'] ?? 'custom');
                throw new \Exception('未能创建 API 客户端（provider=' . $providerTip . '）。请检查该服务是否已配置可用的 API Key/令牌，以及 API 服务器地址是否正确。');
            }

            self::set_job_state($job_id, 'running', 'content', 20, '生成正文...');
            $content = self::generate_content($client, $title, $lang);
            $content = self::ensure_html_article($content);

            self::set_job_state($job_id, 'running', 'keywords', 55, '生成关键词...');
            $keywords = self::generate_keywords($client, $title, $content, $lang);

            self::set_job_state($job_id, 'running', 'description', 75, '生成描述...');
            $description = self::generate_description($client, $title, $content, $lang);

            self::set_job_state($job_id, 'running', 'save', 90, '写入文章...');
            self::save_to_post($post_id, $content, $keywords, $description);

            $job = self::get_job($job_id) ?: [];
            $job['status'] = 'success';
            $job['step'] = 'done';
            $job['progress'] = 100;
            $job['message'] = '完成';
            $job['result'] = [
                'content' => $content,
                'keywords' => $keywords,
                'description' => $description,
                'language' => $lang,
                'provider' => $overrideProvider,
                'model' => $overrideModel,
            ];
            $job['updated_at'] = current_time('mysql');
            self::update_job($job_id, $job);

            self::release_lock($post_id);
        } catch (\Throwable $e) {
            self::fail_job($job_id, $e->getMessage());
            self::release_lock($post_id);
        }
    }

    private static function save_to_post(int $post_id, string $content, array $keywords, string $description): void
    {
        $content = (string)$content;
        $description = (string)$description;

        wp_update_post([
            'ID' => $post_id,
            'post_content' => $content,
            'post_excerpt' => $description,
        ]);

        $keywords = array_values(array_unique(array_filter(array_map('trim', $keywords))));
        if (!empty($keywords)) {
            wp_set_post_terms($post_id, $keywords, 'post_tag', false);
        }

        $keywords_string = !empty($keywords) ? implode(',', $keywords) : '';
        update_post_meta($post_id, '_ai_post_keywords', $keywords_string);
        update_post_meta($post_id, '_ai_post_keywords_array', $keywords);

        update_post_meta($post_id, '_ai_post_meta_description', $description);
        update_post_meta($post_id, '_ai_post_description', $description);

        // Yoast SEO
        update_post_meta($post_id, '_yoast_wpseo_metadesc', $description);

        // RankMath
        update_post_meta($post_id, 'rank_math_description', $description);
    }

    private static function generate_content($client, string $title, string $lang): string
    {
        $prompt = "你是一位经验丰富的SEO内容创作者和资深撰稿人，擅长写出既符合SEO又高度拟人化的文章，具有真实个人视角与情绪表达。\n";
        $prompt .= "请根据标题创作一篇原创且信息准确的文章正文，必须同时满足以下要求：\n";
        $prompt .= "1) 最终输出语言必须与标题语言一致；\n";
        $prompt .= "2) 只输出文章正文（不要输出Meta描述、不要输出关键词列表、不要输出JSON、不要解释写作过程）；\n";
        $prompt .= "3) SEO：从标题中提取1~2个核心关键词，并自然分布在：开头约200字、各级标题、正文关键段落、结尾；避免堆砌，同时自然出现语义相关词和长尾词；\n";
        $prompt .= "4) 结构：直接进入主题，不要用固定模板段落；结尾可以有总结，但不要使用程式化总结句；可附加FAQ（2~5问答，真实且有用）；\n";
        $prompt .= "5) 标题层级：正文中合理使用 <h2>（主要章节）与 <h3>（子章节）；<h2>标题要更像人写的章节名，避免使用“引言/结语/总结/结论/综述/前言/概述”等模板化标题；\n";
        $prompt .= "6) 长度：1500~2500字左右（按主题深度自然浮动，宁可写得有价值也不要为了凑字数灌水）；\n";
        $prompt .= "7) 拟人化：主要用第一人称“我/我们”，适当使用第二人称“你”，像有经验的博主在分享；允许少量自然的题外话但不跑题；可虚构个人经历但要合情合理；\n";
        $prompt .= "8) 句式与风格：长短句混合，适当口语化、反问或感叹；用自然过渡替代套路连接词；严禁大量出现以下词语或其同义模板（包含大小写、变形、近义改写）：\n";
        $prompt .= "   - 结构词：引言、前言、结语、总结、结论、综述、概述、综上所述、总而言之、总的来说、总之\n";
        $prompt .= "   - 序列词：首先、其次、再次、最后、第一/第二/第三（以固定套路列举）\n";
        $prompt .= "   - 英文常见AI词/话术（避免直接使用）：delve, landscape, navigate, embark on, underpin, embody, illuminate, capture, foster, leverage, holistic, nuanced, paradigm, spectrum, trajectory, synergy, confluence, facet, intricacy, resonance\n";
        $prompt .= "     indeed, absolutely, clearly, obviously, importantly, notably, significantly, in fact\n";
        $prompt .= "     first and foremost, additionally, furthermore, moreover, consequently, therefore, as a result\n";
        $prompt .= "     in conclusion, in summary, it is important to note that, it is crucial to, it is worth noting that\n";
        $prompt .= "     for example, for instance, to illustrate\n";
        $prompt .= "     on one hand... on the other hand..., not only... but also..., imagine if, have you ever wondered, what if\n";
        $prompt .= "     a myriad of, a tapestry of, a testament to, at the core of, in the context of\n";
        $prompt .= "     in today's fast-paced world, in today's digital world, the dynamic interplay, evolving over time\n";
        $prompt .= "     a comprehensive overview, plays a pivotal role\n";
        $prompt .= "9) 输出格式：使用HTML输出（必须包含 <h2>、<h3>、<p>，段落清晰），只输出正文HTML本体（不要Markdown、不要代码块）。\n\n";
        $prompt .= "标题：\"{$title}\"";

        $resp = $client->ask($prompt);
        $answer = is_array($resp) ? (string)($resp['answer'] ?? '') : '';
        $answer = trim($answer);
        $answer = self::light_clean_ai_phrases_html($answer);
        return trim($answer);
    }

    private static function light_clean_ai_phrases_html(string $html): string
    {
        $html = (string)$html;
        if ($html === '') {
            return $html;
        }

        $replacements = [
            '/<h2>\s*(引言|前言)\s*<\/h2>/iu' => '<h2>聊聊这件事</h2>',
            '/<h2>\s*(结语|总结|结论)\s*<\/h2>/iu' => '<h2>写在最后</h2>',
            '/<h2>\s*(FAQ|常见问题解答)\s*<\/h2>/iu' => '<h2>常见问题</h2>',

            '/\b综上所述\b/iu' => '说到底',
            '/\b总而言之\b/iu' => '一句话',
            '/\b总的来说\b/iu' => '整体来看',
            '/\b总之\b/iu' => '简单说',

            '/\b首先\b/iu' => '一开始',
            '/\b其次\b/iu' => '再说',
            '/\b再次\b/iu' => '另外',
        ];

        $englishPhraseReplacements = [
            // discourse / transitions
            '/\bin\s+conclusion\b/iu' => 'to wrap up',
            '/\bin\s+summary\b/iu' => 'to sum up',
            '/\bfirst\s+and\s+foremost\b/iu' => 'first',
            '/\bin\s+fact\b/iu' => '',
            '/\badditionally\b/iu' => 'also',
            '/\bfurthermore\b/iu' => 'also',
            '/\bmoreover\b/iu' => 'also',
            '/\bconsequently\b/iu' => 'so',
            '/\btherefore\b/iu' => 'so',
            '/\bas\s+a\s+result\b/iu' => 'so',
            '/\bit\s+is\s+important\s+to\s+note\s+that\b/iu' => '',
            '/\bit\s+is\s+crucial\s+to\b/iu' => '',
            '/\bit\s+is\s+worth\s+noting\s+that\b/iu' => '',
            '/\bfor\s+example\b/iu' => 'like',
            '/\bfor\s+instance\b/iu' => 'like',
            '/\bto\s+illustrate\b/iu' => 'to show',
            '/\bhave\s+you\s+ever\s+wondered\b/iu' => '',
            '/\bimagine\s+if\b/iu' => 'imagine',
            '/\bwhat\s+if\b/iu' => 'if',
            '/\ba\s+myriad\s+of\b/iu' => 'many',
            '/\ba\s+tapestry\s+of\b/iu' => 'a mix of',
            '/\ba\s+testament\s+to\b/iu' => 'proof of',
            '/\bat\s+the\s+core\s+of\b/iu' => 'at the heart of',
            '/\bin\s+the\s+context\s+of\b/iu' => 'in',
            '/\bin\s+today\'s\s+fast-paced\s+world\b/iu' => 'today',
            '/\bin\s+today\'s\s+digital\s+world\b/iu' => 'today',
            '/\bthe\s+dynamic\s+interplay\b/iu' => 'the interaction',
            '/\bevolving\s+over\s+time\b/iu' => 'changing over time',
            '/\ba\s+comprehensive\s+overview\b/iu' => 'an overview',
            '/\bplays\s+a\s+pivotal\s+role\b/iu' => 'matters a lot',
        ];

        $englishWordReplacements = [
            '/\bdelve\b/iu' => 'look into',
            '/\blandscape\b/iu' => 'scene',
            '/\bnavigate\b/iu' => 'handle',
            '/\bembark\b/iu' => 'start',
            '/\bunderpin\b/iu' => 'support',
            '/\bembody\b/iu' => 'show',
            '/\billuminate\b/iu' => 'explain',
            '/\bcapture\b/iu' => 'show',
            '/\bfoster\b/iu' => 'build',
            '/\bleverage\b/iu' => 'use',
            '/\bholistic\b/iu' => 'overall',
            '/\bnuanced\b/iu' => 'subtle',
            '/\bparadigm\b/iu' => 'model',
            '/\bspectrum\b/iu' => 'range',
            '/\btrajectory\b/iu' => 'path',
            '/\bsynergy\b/iu' => 'teamwork',
            '/\bconfluence\b/iu' => 'meeting',
            '/\bfacet\b/iu' => 'part',
            '/\bintricacy\b/iu' => 'complexity',
            '/\bresonance\b/iu' => 'connection',
            '/\bindeed\b/iu' => '',
            '/\babsolutely\b/iu' => '',
            '/\bclearly\b/iu' => '',
            '/\bobviously\b/iu' => '',
            '/\bimportantly\b/iu' => '',
            '/\bnotably\b/iu' => '',
            '/\bsignificantly\b/iu' => '',
        ];

        foreach ($replacements as $pattern => $replacement) {
            $html = preg_replace($pattern, $replacement, $html);
        }

        foreach ($englishPhraseReplacements as $pattern => $replacement) {
            $html = preg_replace($pattern, $replacement, $html);
        }

        foreach ($englishWordReplacements as $pattern => $replacement) {
            $html = preg_replace($pattern, $replacement, $html);
        }

        $html = preg_replace('/\s+/u', ' ', $html);
        $html = preg_replace('/>\s+</u', '><', $html);
        return trim($html);
    }

    private static function generate_keywords($client, string $title, string $content, string $lang): array
    {
        $input = self::build_keywords_input($title, $content);
        $prompt = "请基于以下信息，提取3-5个关键词（适合作为 WordPress 标签）。\n";
        $prompt .= "要求：\n";
        $prompt .= "1) 关键词语言必须与标题语言一致；\n";
        $prompt .= "2) 只输出 JSON 数组，例如 [\"关键词1\",\"关键词2\"]；\n";
        $prompt .= "3) 不要输出多余文字；\n";
        $prompt .= "4) 关键词尽量短（1~4个词或2~8个字）。\n\n";
        $prompt .= $input;

        $resp = $client->ask($prompt);
        $answer = is_array($resp) ? (string)($resp['answer'] ?? '') : '';
        $answer = trim($answer);

        $decoded = json_decode($answer, true);
        if (is_array($decoded)) {
            $out = [];
            foreach ($decoded as $it) {
                if (is_string($it)) {
                    $t = trim($it);
                    if ($t !== '') {
                        $out[] = $t;
                    }
                }
            }
            return $out;
        }

        // 兜底：按逗号/换行分割
        $answer = preg_replace('/[\r\n]+/u', ',', $answer);
        $parts = array_filter(array_map('trim', explode(',', $answer)));
        return array_values($parts);
    }

    private static function generate_description($client, string $title, string $content, string $lang): string
    {
        $prompt = "请为下面这篇文章生成一段 Meta Description（同时可作为摘要）。\n";
        $prompt .= "要求：\n";
        $prompt .= "1) 输出语言必须与标题语言一致；\n";
        $prompt .= "2) 长度尽量控制在约 150~160 个字符（不同语言可自然浮动，但不要太长）；\n";
        $prompt .= "3) 1~2 句，不要分段，不要换行；\n";
        $prompt .= "4) 不要添加任何标签或解释，只输出描述本体。\n\n";
        $prompt .= "标题：\"{$title}\"\n\n";
        $prompt .= "正文：\n" . mb_substr(wp_strip_all_tags($content), 0, 1200, 'UTF-8');

        $resp = $client->ask($prompt);
        $answer = is_array($resp) ? (string)($resp['answer'] ?? '') : '';
        $answer = trim(preg_replace('/[\r\n]+/u', ' ', $answer));
        return $answer;
    }

    private static function fail_job(string $job_id, string $error): void
    {
        $job = self::get_job($job_id) ?: ['job_id' => $job_id];
        $job['status'] = 'failed';
        $job['step'] = 'failed';
        $job['progress'] = 100;
        $job['message'] = '失败';
        $job['error'] = $error;
        $job['updated_at'] = current_time('mysql');
        self::update_job($job_id, $job);
    }

    private static function set_job_state(string $job_id, string $status, string $step, int $progress, string $message): void
    {
        $job = self::get_job($job_id) ?: ['job_id' => $job_id];
        $job['status'] = $status;
        $job['step'] = $step;
        $job['progress'] = $progress;
        $job['message'] = $message;
        $job['updated_at'] = current_time('mysql');
        self::update_job($job_id, $job);
    }

    private static function new_job_id(): string
    {
        return wp_generate_password(20, false, false) . '_' . time();
    }

    private static function get_job(string $job_id): ?array
    {
        $all = get_option(self::OPTION_JOBS, []);
        if (!is_array($all)) {
            return null;
        }
        return isset($all[$job_id]) && is_array($all[$job_id]) ? $all[$job_id] : null;
    }

    private static function update_job(string $job_id, array $job): void
    {
        $all = get_option(self::OPTION_JOBS, []);
        if (!is_array($all)) {
            $all = [];
        }
        $all[$job_id] = $job;
        update_option(self::OPTION_JOBS, $all, false);
    }

    private static function release_lock(int $post_id): void
    {
        if ($post_id > 0) {
            $lock_key = 'aipost_editor_generate_lock_' . $post_id;
            delete_transient($lock_key);
        }
    }
}

EditorGenerateEndpoint::register();
