<?php

namespace App\Services;

use App\Exceptions\SectionTimeoutException;
use App\Exceptions\TaskFailException;
use App\Http\Api\ChannelApi;
use App\Http\Api\MdRender;
use App\Models\AiModel;
use App\Models\PaliSentence;
use App\Models\PaliText;
use App\Models\Sentence;
use App\Models\Task;
use App\Tools\Tools;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class DatabaseException extends \Exception {}

class AiTranslateService
{
    private $queue = 'ai_translate_v2';

    private $modelToken = null;

    private $task = null;

    protected $mq;

    private $apiTimeout = 30;

    private $llmTimeout = 300;

    private $taskTopicId;

    private $stop = false;

    private $maxProcessTime = 15 * 60; // 一个句子的最大处理时间

    private $mqTimeout = 60;

    private $openaiProxy = null;

    public function __construct() {}

    public function setProxy(string $proxy): self
    {
        $this->openaiProxy = $proxy;

        return $this;
    }

    /**
     * @param  array  $translateData
     */
    public function processTranslate(string $messageId, array $messages): bool
    {
        $start = time();

        if (! is_array($messages) || count($messages) === 0) {
            Log::error('message is not array');

            return false;
        }

        $first = $messages[0];
        $this->task = $first->task->info;
        $taskId = $this->task->id;
        Cache::put("/task/{$taskId}/message_id", $messageId);
        $pointerKey = "/task/{$taskId}/pointer";
        $pointer = 0;
        if (Cache::has($pointerKey)) {
            // 回到上次中断的点
            $pointer = Cache::get($pointerKey);
            Log::info("last break point {$pointer}");
        }

        // 获取model token
        $this->modelToken = $first->model->token;

        $this->setTaskStatus($this->task->id, 'running');

        // 设置task discussion topic
        $this->taskTopicId = $this->taskDiscussion(
            $this->task->id,
            'task',
            $this->task->title,
            $this->task->category,
            null
        );

        $time = [$this->maxProcessTime];
        for ($i = $pointer; $i < count($messages); $i++) {
            // 获取当前内存使用量
            Log::debug('memory usage: '.memory_get_usage(true) / 1024 / 1024 .' MB');
            // 获取峰值内存使用量
            Log::debug('memory peak usage: '.memory_get_peak_usage(true) / 1024 / 1024 .' MB');

            if ($this->stop) {
                Log::info("收到退出信号 pointer={$i}");

                return false;
            }
            if (Tools::isStop()) {
                // 检测到停止标记
                return false;
            }

            Cache::put($pointerKey, $i);
            $message = $messages[$i];
            $taskDiscussionContent = [];

            // 推理
            $responseLLM = $this->requestLLM($message);
            $taskDiscussionContent[] = '- LLM request successful';

            if ($this->task->category === 'translate') {
                // 写入句子库
                $message->sentence->content = $responseLLM['content'];
                try {
                    $this->saveSentence($message->sentence);
                } catch (\Exception $e) {
                    Log::error('sentence', ['message' => $e]);

                    continue;
                }
            }
            if ($this->task->category === 'suggest') {
                // 写入pr
                try {
                    $this->savePr($message->sentence, $responseLLM['content']);
                } catch (\Exception $e) {
                    Log::error('sentence', ['message' => $e]);

                    continue;
                }
            }

            // 获取句子id
            $sUid = $this->getSentenceId($message->sentence);

            // 写入句子 discussion
            $topicId = $this->taskDiscussion(
                $sUid,
                'sentence',
                $this->task->title,
                $this->task->category,
                null
            );

            if ($topicId) {
                Log::info($this->queue.' discussion create topic successful');
                $data['parent'] = $topicId;
                unset($data['title']);
                $topicChildren = [];
                // 提示词
                $topicChildren[] = $message->prompt;
                // 任务结果
                $topicChildren[] = $responseLLM['content'];
                // 推理过程写入discussion
                if (
                    isset($responseLLM['reasoningContent']) &&
                    ! empty($responseLLM['reasoningContent'])
                ) {
                    $topicChildren[] = $responseLLM['reasoningContent'];
                }
                foreach ($topicChildren as $content) {
                    Log::debug($this->queue.' discussion child request', ['data' => $data]);

                    $dId = $this->taskDiscussion($sUid, 'sentence', $this->task->title, $content, $topicId);
                    if ($dId) {
                        Log::info($this->queue.' discussion child successful');
                    }
                }
            } else {
                Log::error($this->queue.' discussion create topic response is null');
            }

            // 修改task 完成度
            $progress = $this->setTaskProgress($message->task->progress);
            $taskDiscussionContent[] = '- progress='.$progress;
            // 写入task discussion
            if ($this->taskTopicId) {
                $content = implode("\n", $taskDiscussionContent);
                $dId = $this->taskDiscussion(
                    $this->task->id,
                    'task',
                    $this->task->title,
                    $content,
                    $this->taskTopicId
                );
            } else {
                Log::error('no task discussion root');
            }

            // 计算剩余时间是否足够再做一次
            $time[] = time() - $start;
            rsort($time);
            $remain = $this->mqTimeout - (time() - $start);
            if ($remain < $time[0]) {
                throw new SectionTimeoutException;
            }
        }
        // 任务完成 修改任务状态为 done
        if ($i === count($messages)) {
            $this->setTaskStatus($this->task->id, 'done');
            Cache::forget($pointerKey);
            Log::info('ai translate task complete');
        }

        return true;
    }

    private function setTaskStatus($taskId, $status)
    {
        $url = config('app.url').'/api/v2/task-status/'.$taskId;
        $data = [
            'status' => $status,
        ];
        Log::debug('ai_translate task status request', ['url' => $url, 'data' => $data]);
        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->patch($url, $data);
        // 判断状态码
        if ($response->failed()) {
            Log::error('ai_translate task status error', ['data' => $response->json()]);
        } else {
            Log::info('ai_translate task status done');
        }
    }

    private function saveModelLog($token, $data)
    {
        $url = config('app.url').'/api/v2/model-log';

        $response = Http::timeout($this->apiTimeout)->withToken($token)->post($url, $data);
        if ($response->failed()) {
            Log::error('ai-translate model log create failed', ['data' => $response->json()]);

            return false;
        }

        return true;
    }

    private function taskDiscussion($resId, $resType, $title, $content, $parentId = null)
    {
        $url = config('app.url').'/api/v2/discussion';
        $taskDiscussionData = [
            'res_id' => $resId,
            'res_type' => $resType,
            'content' => $content,
            'content_type' => 'markdown',
            'type' => 'discussion',
            'notification' => false,
        ];
        if ($parentId) {
            $taskDiscussionData['parent'] = $parentId;
        } else {
            $taskDiscussionData['title'] = $title;
        }
        Log::debug($this->queue.' discussion create', ['url' => $url, 'data' => json_encode($taskDiscussionData)]);

        $response = Http::timeout($this->apiTimeout)
            ->withToken($this->modelToken)
            ->post($url, $taskDiscussionData);
        if ($response->failed()) {
            Log::error($this->queue.' discussion create error', ['data' => $response->json()]);

            return false;
        }
        Log::debug($this->queue.' discussion create', ['data' => json_encode($response->json())]);

        if (isset($response->json()['data']['id'])) {
            return $response->json()['data']['id'];
        }

        return false;
    }

    private function requestLLM($message)
    {
        $param = [
            'model' => $message->model->model,
            'messages' => [
                ['role' => 'system', 'content' => $message->model->system_prompt ?? ''],
                ['role' => 'user', 'content' => $message->prompt],
            ],
            'temperature' => 0.3,  // 低随机性，确保准确
            'top_k' => 20,         // 限制候选词范围
            'stream' => false,
        ];
        if ($this->openaiProxy) {
            $requestUrl = $this->openaiProxy;
            $body = [
                'open_ai_url' => $message->model->url,
                'api_key' => $message->model->key,
                'payload' => $param,
            ];
        } else {
            $requestUrl = $message->model->url;
            $body = $param;
        }
        Log::info($this->queue.' LLM request '.$message->model->url.' model:'.$param['model']);
        Log::debug($this->queue.' LLM api request', [
            'url' => $message->model->url,
            'data' => json_encode($param),
        ]);

        // 写入 model log
        $modelLogData = [
            'model_id' => $message->model->uid,
            'request_at' => now(),
            'request_data' => json_encode($param, JSON_UNESCAPED_UNICODE),
        ];
        // 失败重试
        $maxRetries = 3;
        $attempt = 0;
        try {
            while ($attempt < $maxRetries) {
                try {
                    $response = Http::withToken($message->model->key)
                        ->timeout($this->llmTimeout)
                        ->post($requestUrl, $body);

                    // 如果状态码是 4xx 或 5xx，会自动抛出 RequestException
                    $response->throw();

                    Log::info($this->queue.' LLM request successful');

                    $modelLogData['request_headers'] = json_encode($response->handlerStats(), JSON_UNESCAPED_UNICODE);
                    $modelLogData['response_headers'] = json_encode($response->headers(), JSON_UNESCAPED_UNICODE);
                    $modelLogData['status'] = $response->status();
                    $modelLogData['response_data'] = json_encode($response->json(), JSON_UNESCAPED_UNICODE);
                    self::saveModelLog($this->modelToken, $modelLogData);
                    break; // 跳出 while 循环
                } catch (RequestException $e) {
                    Log::error($this->queue.' LLM request exception: '.$e->getMessage());
                    $failResponse = $e->response;
                    $modelLogData['request_headers'] = json_encode($failResponse->handlerStats(), JSON_UNESCAPED_UNICODE);
                    $modelLogData['response_headers'] = json_encode($failResponse->headers(), JSON_UNESCAPED_UNICODE);
                    $modelLogData['status'] = $failResponse->status();
                    $modelLogData['response_data'] = $response->body();
                    $modelLogData['success'] = false;
                    self::saveModelLog($this->modelToken, $modelLogData);

                    $attempt++;
                    $status = $e->response->status();

                    // 某些错误不需要重试
                    if (in_array($status, [400, 401, 403, 404, 422])) {
                        Log::warning("客户端错误，不重试: {$status}\n");
                        throw new TaskFailException; // 重新抛出异常
                    }
                    // 服务器错误或网络错误可以重试
                    if ($attempt < $maxRetries) {
                        $delay = pow(2, $attempt); // 指数退避
                        Log::warning("请求失败（第 {$attempt} 次），{$delay} 秒后重试...\n");
                        sleep($delay);
                    } else {
                        Log::error("达到最大重试次数，请求最终失败\n");
                        throw new TaskFailException;
                    }
                } catch (\Exception $e) {
                    throw $e;
                }
            }
        } catch (\Exception $e) {
            throw $e;
        }

        Log::info($this->queue.' model log saved');

        $aiData = $response->json();
        Log::debug($this->queue.' LLM http response', ['data' => $response->json()]);
        $responseContent = $aiData['choices'][0]['message']['content'];
        if (isset($aiData['choices'][0]['message']['reasoning_content'])) {
            $reasoningContent = $aiData['choices'][0]['message']['reasoning_content'];
        }
        $output = ['content' => $responseContent];
        Log::debug($this->queue.' LLM response content='.$responseContent);
        if (empty($reasoningContent)) {
            Log::debug($this->queue.' no reasoningContent');
        } else {
            Log::debug($this->queue.' reasoning='.$reasoningContent);
            $output['reasoningContent'] = $reasoningContent;
        }

        return $output;
    }

    /**
     * 写入句子库
     */
    private function saveSentence(array $sentence, ?string $token = null)
    {
        $url = config('app.url').'/api/v2/sentence';

        Log::info($this->queue." sentence update {$url}");
        $response = Http::timeout($this->apiTimeout)
            ->withToken($token ?? $this->modelToken)
            ->post($url, [
                'sentences' => [$sentence],
            ]);
        if ($response->failed()) {
            Log::error($this->queue.' sentence update failed', [
                'url' => $url,
                'data' => $response->json(),
            ]);
            throw new DatabaseException('sentence 数据库写入错误');
        }
        $count = $response->json()['data']['count'];
        Log::info("{$this->queue} sentence update {$count} successful");
    }

    private function savePr($sentence, $content)
    {
        $url = config('app.url').'/api/v2/sentpr';
        Log::info($this->queue." sentence update {$url}");
        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->post($url, [
            'book' => $sentence->book_id,
            'para' => $sentence->paragraph,
            'begin' => $sentence->word_start,
            'end' => $sentence->word_end,
            'channel' => $sentence->channel_uid,
            'text' => $content,
            'notification' => false,
            'webhook' => false,
        ]);
        if ($response->failed()) {
            Log::error($this->queue.' sentence update failed', [
                'url' => $url,
                'data' => $response->json(),
            ]);
            throw new DatabaseException('pr 数据库写入错误');
        }
        if ($response->json()['ok']) {
            Log::info("{$this->queue} sentence suggest update successful");
        } else {
            Log::error("{$this->queue} sentence suggest update failed", [
                'url' => $url,
                'data' => $response->json(),
            ]);
        }
    }

    private function getSentenceId($sentence)
    {
        $url = config('app.url').'/api/v2/sentence-info/aa';
        Log::info('ai translate', ['url' => $url]);
        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->get($url, [
            'book' => $sentence->book_id,
            'par' => $sentence->paragraph,
            'start' => $sentence->word_start,
            'end' => $sentence->word_end,
            'channel' => $sentence->channel_uid,
        ]);
        if (! $response->json()['ok']) {
            Log::error($this->queue.' sentence id error', ['data' => $response->json()]);

            return false;
        }
        $sUid = $response->json()['data']['id'];
        Log::debug("sentence id={$sUid}");

        return $sUid;
    }

    private function setTaskProgress($current)
    {
        $taskProgress = $current;
        if ($taskProgress->total > 0) {
            $progress = (int) ($taskProgress->current * 100 / $taskProgress->total);
        } else {
            $progress = 100;
            Log::error($this->queue.' progress total is zero', ['task_id' => $this->task->id]);
        }

        $url = config('app.url').'/api/v2/task/'.$this->task->id;
        $data = [
            'progress' => $progress,
        ];
        Log::debug($this->queue.' task progress request', ['url' => $url, 'data' => $data]);
        $response = Http::timeout($this->apiTimeout)->withToken($this->modelToken)->patch($url, $data);
        if ($response->failed()) {
            Log::error($this->queue.' task progress error', ['data' => $response->json()]);
        } else {

            Log::info($this->queue.' task progress successful progress='.$response->json()['data']['progress']);
        }

        return $progress;
    }

    public function handleFailedTranslate(string $messageId, array $translateData, \Exception $exception): void
    {
        try {
            // 彻底失败时的业务逻辑
            // 设置task为失败状态
            $this->setTaskStatus($this->task->id, 'stop');
            // 将故障信息写入task discussion
            if ($this->taskTopicId) {
                $dId = $this->taskDiscussion(
                    $this->task->id,
                    'task',
                    $this->task->title,
                    "**处理失败ai任务时出错** 请重启任务 message id={$messageId} 错误信息：".$exception->getMessage(),
                    $this->taskTopicId
                );
            }
        } catch (\Exception $e) {
            Log::error('处理失败ai任务时出错', ['error' => $e->getMessage()]);
        }
    }

    /**
     * 读取task信息，将任务拆解为单句小任务
     *
     * @param  string  $taskId  任务uuid
     * @return array 拆解后的提示词数组
     */
    public static function makeByTask(string $taskId, $aiAssistantId)
    {
        $task = Task::findOrFail($taskId);
        $description = $task->description;
        $rows = explode("\n", $description);
        $params = [];
        foreach ($rows as $key => $row) {
            if (strpos($row, '=') !== false) {
                $param = explode('=', trim($row, '|'));
                $params[$param[0]] = $param[1];
            }
        }
        if (! isset($params['type'])) {
            Log::error('no $params.type');

            return false;
        }

        // get sentences in article
        $sentences = [];
        $totalLen = 0;
        switch ($params['type']) {
            case 'sentence':
                if (! isset($params['id'])) {
                    Log::error('no $params.id');

                    return false;
                }
                $sentences[] = explode('-', $params['id']);
                break;
            case 'para':
                if (! isset($params['book']) || ! isset($params['paragraphs'])) {
                    Log::error('no $params.book or paragraphs');

                    return false;
                }
                $sent = PaliSentence::where('book', $params['book'])
                    ->where('paragraph', $params['paragraphs'])->orderBy('word_begin')->get();
                foreach ($sent as $key => $value) {
                    $sentences[] = [
                        'id' => [
                            $value->book,
                            $value->paragraph,
                            $value->word_begin,
                            $value->word_end,
                        ],
                        'strlen' => $value->length,
                    ];
                    $totalLen += $value->length;
                }
                break;
            case 'chapter':
                if (! isset($params['book']) || ! isset($params['paragraphs'])) {
                    Log::error('no $params.book or paragraphs');

                    return false;
                }
                $chapterLen = PaliText::where('book', $params['book'])
                    ->where('paragraph', $params['paragraphs'])->value('chapter_len');
                $sent = PaliSentence::where('book', $params['book'])
                    ->whereBetween('paragraph', [$params['paragraphs'], $params['paragraphs'] + $chapterLen - 1])
                    ->orderBy('paragraph')
                    ->orderBy('word_begin')->get();
                foreach ($sent as $key => $value) {
                    $sentences[] = [
                        'id' => [
                            $value->book,
                            $value->paragraph,
                            $value->word_begin,
                            $value->word_end,
                        ],
                        'strlen' => $value->length,
                    ];
                    $totalLen += $value->length;
                }
                break;
            default:
                return false;
                break;
        }

        // render prompt
        $mdRender = new MdRender([
            'format' => 'prompt',
            'footnote' => false,
            'paragraph' => false,
        ]);
        $m = new \Mustache_Engine([
            'entity_flags' => ENT_QUOTES,
            'escape' => function ($value) {
                return $value;
            },
        ]);

        // ai model
        $aiModel = AiModel::findOrFail($aiAssistantId);
        $modelToken = AuthService::getUserToken($aiModel->uid);
        $aiModel['token'] = $modelToken;

        /**
         * 返回值会被 ProcessAITranslateJob::publish() 写入缓存。Laravel 13 默认
         * 拒绝从缓存反序列化对象（cache.serializable_classes = false），所以这里
         * 先把模型摊平成数组。JSON 输出与直接编码模型完全一致。
         */
        $aiModelData = $aiModel->toArray();
        $taskData = $task->toArray();
        $sumLen = 0;
        $mqData = [];
        foreach ($sentences as $key => $sentence) {
            $sumLen += $sentence['strlen'];
            $sid = implode('-', $sentence['id']);
            Log::debug($sid);
            $sentChannelInfo = explode('@', $params['channel']);
            $channelId = $sentChannelInfo[0];
            $data = [];
            $data['origin'] = '{{'.$sid.'}}';
            $data['translation'] = '{{sent|id='.$sid;
            $data['translation'] .= '|channel='.$channelId;
            $data['translation'] .= '|text=translation}}';
            if (isset($params['nissaya']) && ! empty($params['nissaya'])) {
                $nissayaChannel = explode('@', $params['nissaya']);
                $channelInfo = ChannelApi::getById($nissayaChannel[0]);
                if ($channelInfo) {
                    // 查看句子是否存在
                    $nissayaSent = Sentence::where('book_id', $sentence['id'][0])
                        ->where('paragraph', $sentence['id'][1])
                        ->where('word_start', $sentence['id'][2])
                        ->where('word_end', $sentence['id'][3])
                        ->where('channel_uid', $nissayaChannel[0])->first();
                    if ($nissayaSent && ! empty($nissayaSent->content)) {
                        $nissayaData = [];
                        $nissayaData['channel'] = $channelInfo;
                        $nissayaData['data'] = '{{sent|id='.$sid;
                        $nissayaData['data'] .= '|channel='.$nissayaChannel[0];
                        $nissayaData['data'] .= '|text=translation}}';
                        $data['nissaya'] = $nissayaData;
                    }
                }
            }

            $content = $m->render($description, $data);
            $prompt = $mdRender->convert($content, []);
            // gen mq
            $aiMqData = [
                'model' => $aiModelData,
                'task' => [
                    'info' => $taskData,
                    'progress' => [
                        'current' => $sumLen,
                        'total' => $totalLen,
                    ],
                ],
                'prompt' => $prompt,
                'sentence' => [
                    'book_id' => $sentence['id'][0],
                    'paragraph' => $sentence['id'][1],
                    'word_start' => $sentence['id'][2],
                    'word_end' => $sentence['id'][3],
                    'channel_uid' => $channelId,
                    'content' => $prompt,
                    'content_type' => 'markdown',
                    'access_token' => $sentChannelInfo[1] ?? $params['token'],
                ],
            ];
            array_push($mqData, $aiMqData);
        }

        $output = [
            'model' => $aiModelData,
            'task' => $taskData,
        ];
        $us = ['openai.com', 'googleapis.com', 'x.ai', 'anthropic.com'];
        $found = array_filter($us, function ($value) use ($output) {
            return str_contains($output['model']['url'], $value);
        });
        if ($found) {
            $output['area'] = 'us';
        } else {
            $output['area'] = 'cn';
        }
        $output['payload'] = $mqData;

        return $output;
    }

    public function stop()
    {
        $this->stop = true;
    }
}
