<?php

declare(strict_types=1);

namespace ZM\Plugin;

use Choir\Http\HttpFactory;
use OneBot\Driver\Event\StopException;
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
use OneBot\Driver\Event\WebSocket\WebSocketOpenEvent;
use OneBot\V12\Exception\OneBotException;
use OneBot\V12\Object\ActionResponse;
use OneBot\V12\Object\MessageSegment;
use OneBot\V12\Object\OneBotEvent;
use OneBot\V12\Validator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use ZM\Annotation\AnnotationHandler;
use ZM\Annotation\AnnotationMap;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\OneBot\BotActionResponse;
use ZM\Annotation\OneBot\BotCommand;
use ZM\Annotation\OneBot\BotEvent;
use ZM\Annotation\OneBot\CommandArgument;
use ZM\Container\ContainerRegistrant;
use ZM\Context\BotContext;
use ZM\Exception\InterruptException;
use ZM\Exception\OneBot12Exception;
use ZM\Exception\WaitTimeoutException;
use ZM\Utils\ConnectionUtil;
use ZM\Utils\MessageUtil;

class OneBot12Adapter extends ZMPlugin
{
    /**
     * 缓存待询问参数的队列
     *
     * 0: 代表 OneBotEvent 对象，即用于判断是否为同一会话环境
     * 1: \Generator 生成器，协程，不多讲
     * 2: BotCommand 注解对象
     * 3: match_result（array）匹配到一半的结果
     */
    private static array $prompt_queue = [];

    public function __construct(string $submodule = '', ?AnnotationParser $parser = null)
    {
        parent::__construct(__DIR__);
        switch ($submodule) {
            case '':
            case 'onebot12':
                // 处理所有 OneBot 12 的反向 WS 握手事件
                $this->addEvent(WebSocketOpenEvent::class, [$this, 'handleWSReverseOpen']);
                $this->addEvent(WebSocketMessageEvent::class, [$this, 'handleWSReverseMessage']);
                // 在 BotEvent 内处理 BotCommand
                $this->addBotEvent(BotEvent::make(type: 'message', level: 15)->on([$this, 'handleBotCommand']));
                // 在 BotEvent 内处理需要等待回复的 CommandArgument
                $this->addBotEvent(BotEvent::make(type: 'message', level: 20)->on([$this, 'handlePrompt']));
                // 处理和声明所有 BotCommand 下的 CommandArgument
                $parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']);
                // 不需要给列表写入 CommandArgument
                $parser->addSpecialParser(CommandArgument::class, [$this, 'parseCommandArgument']);
                break;
            case 'onebot12-ban-other-ws':
                // 禁止其他类型的 WebSocket 客户端接入
                $this->addEvent(WebSocketOpenEvent::class, [$this, 'handleUnknownWSReverseInput'], 1);
                break;
        }
    }

    /**
     * 将 BotCommand 假设含有 CommandArgument 的话，就注册到参数列表中
     *
     * @param BotCommand $command                 命令对象
     * @param null|array $same_method_annotations 同一个方法的所有注解
     */
    public function parseBotCommand(BotCommand $command, ?array $same_method_annotations = null): ?bool
    {
        if ($same_method_annotations === null) {
            return null;
        }
        foreach ($same_method_annotations as $v) {
            if ($v instanceof CommandArgument) {
                $command->withArgumentObject($v);
            }
        }
        return null;
    }

    /**
     * 忽略解析记录 CommandArgument 注解
     */
    public function parseCommandArgument(): ?bool
    {
        return true;
    }

    /**
     * [CALLBACK] 调用 BotCommand 注解的方法
     *
     * @param  BotContext                  $ctx 机器人环境上下文
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     * @throws \Throwable
     * @throws OneBot12Exception
     */
    public function handleBotCommand(BotContext $ctx)
    {
        if ($ctx->hasReplied()) {
            return;
        }
        // 匹配消息
        $match_result = $this->matchBotCommand($ctx->getEvent());
        if ($match_result === null) {
            return;
        }
        // 匹配成功，检查 CommandArguments
        /** @var BotCommand $command */
        $command = $match_result[0];
        $arguments = $this->matchCommandArguments($match_result[1], $command);
        // 返回的是生成器，说明有需要询问的参数
        if ($arguments instanceof \Generator) {
            /** @var null|CommandArgument $argument */
            $argument = $arguments->current();
            if ($argument === null) {
                // 是 null 表明返回了空生成器，说明参数都已经匹配完毕
                $ctx->setParams($arguments->getReturn());
                $this->callBotCommand($ctx, $command);
                return;
            }
            $message = MessageSegment::text($argument->prompt === '' ? ('请输入' . $argument->name) : $argument->prompt);
            $ctx->reply([$message]);
            // 然后将此事件放入等待队列
            self::$prompt_queue[] = [$ctx->getEvent(), $arguments, $command, $match_result];
            return;
        }
        $ctx->setParams($arguments);
        // 调用方法
        $this->callBotCommand($ctx, $command);
    }

    /**
     * [CALLBACK] 处理需要等待回复的 CommandArgument
     *
     * @param  BotContext                  $ctx 机器人上下文
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     * @throws OneBot12Exception
     * @throws \Throwable
     */
    public function handlePrompt(BotContext $ctx)
    {
        // 需要先从队列里找到定义当前会话的 prompt
        // 定义一个会话的标准是：事件的 detail_type，user_id，[group_id]，[guild_id，channel_id] 全部相同
        $new_event = $ctx->getEvent();
        foreach (self::$prompt_queue as $k => $v) {
            /** @var OneBotEvent $old_event */
            $old_event = $v[0];
            if ($old_event->detail_type !== $new_event->detail_type) {
                continue;
            }
            $matched = match ($old_event->detail_type) {
                'private' => $new_event->getUserId() === $old_event->getUserId(),
                'group' => $new_event->getGroupId() === $old_event->getGroupId(),
                'guild' => $new_event->getGuildId() === $old_event->getGuildId() && $new_event->getChannelId() === $old_event->getChannelId(),
                default => false,
            };
            if (!$matched) {
                continue;
            }
            array_splice(self::$prompt_queue, $k, 1);
            // 找到了，开始处理
            /** @var \Generator $arguments */
            $arguments = $v[1];
            $new_arguments = $arguments->send($new_event->getMessage());
            if ($new_arguments !== null || $arguments->valid()) {
                // 还有需要询问的参数
                /** @var CommandArgument $argument */
                $argument = $arguments->current();
                $message = MessageSegment::text($argument->prompt === '' ? ('请输入' . $argument->name) : $argument->prompt);
                $ctx->reply([$message]);
                // 然后将此事件放入等待队列
                self::$prompt_queue[] = [$ctx->getEvent(), $arguments, $v[2], $v[3]];
            } else {
                // 所有参数都已经获取到了，调用方法
                $ctx->setParams($arguments->getReturn());
                $this->callBotCommand($ctx, $v[2]);
            }
            return;
        }
    }

    /**
     * @throws StopException
     */
    public function handleUnknownWSReverseInput(WebSocketOpenEvent $event)
    {
        // 判断是不是 OneBot 12 反向 WS 连进来的，通过 Sec-WebSocket-Protocol 头
        $line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2);
        if ($line[0] !== '12') {
            logger()->warning('不允许接入除 OneBot 12 以外的 WebSocket Client');
            $event->withResponse(HttpFactory::createResponse(403, 'Forbidden'));
            $event->stopPropagation();
        }
    }

    /**
     * [CALLBACK] 接入和认证反向 WS 的连接
     *
     * @throws StopException
     * @throws \JsonException
     */
    public function handleWSReverseOpen(WebSocketOpenEvent $event): void
    {
        // 判断是不是 OneBot 12 反向 WS 连进来的，通过 Sec-WebSocket-Protocol 头
        $line = explode('.', $event->getRequest()->getHeaderLine('Sec-WebSocket-Protocol'), 2);
        if ($line[0] === '12') {
            logger()->info('检测到 OneBot 12 反向 WS 连接，正在进行认证...');
            // 是 OneBot 12 标准的，准许接入，进行鉴权
            $request = $event->getRequest();
            $info = ['impl' => $line[1] ?? 'unknown'];
            if (($stored_token = $event->getSocketConfig()['access_token'] ?? '') !== '') {
                // 测试 Header
                $token = $request->getHeaderLine('Authorization');
                if ($token === '') {
                    // 测试 Query
                    $token = $request->getQueryParams()['access_token'] ?? '';
                }
                $token = explode('Bearer ', $token);
                if (!isset($token[1]) || $token[1] !== $stored_token) { // 没有 token，鉴权失败
                    logger()->warning('OneBot 12 反向 WS 连接鉴权失败，拒绝接入');
                    $event->withResponse(HttpFactory::createResponse(401, 'Unauthorized'));
                    $event->stopPropagation();
                }
            }
            logger()->info('OneBot 12 反向 WS 连接鉴权成功，接入成功[' . $event->getFd() . ']');
        }
        // 设置 OneBot 相关的东西
        ConnectionUtil::setConnection($event->getFd(), $info ?? []);
    }

    /**
     * [CALLBACK] 处理 WebSocket 消息
     *
     * @param  WebSocketMessageEvent $event 事件对象
     * @throws OneBotException
     * @throws \Throwable
     */
    public function handleWSReverseMessage(WebSocketMessageEvent $event): void
    {
        // 忽略非 OneBot 12 的消息
        $impl = ConnectionUtil::getConnection($event->getFd())['impl'] ?? null;
        if ($impl === null) {
            return;
        }

        // 解析 Frame 到 UTF-8 JSON
        $body = $event->getFrame()->getData();
        $body = json_decode($body, true);
        if ($body === null) {
            logger()->warning('收到非 JSON 格式的消息，已忽略');
            return;
        }

        if (isset($body['type'], $body['detail_type'])) {
            // 如果含有 type，detail_type 字段，表明是 event
            try {
                $obj = new OneBotEvent($body);
            } catch (OneBotException) {
                logger()->debug('收到非 OneBot 12 标准的消息，已忽略');
                return;
            }

            // 绑定容器
            ContainerRegistrant::registerOBEventServices($obj);

            // 调用 BotEvent 事件
            $handler = new AnnotationHandler(BotEvent::class);
            $handler->setRuleCallback(function (BotEvent $event) use ($obj) {
                return ($event->type === null || $event->type === $obj->type)
                    && ($event->sub_type === null || $event->sub_type === $obj->sub_type)
                    && ($event->detail_type === null || $event->detail_type === $obj->detail_type);
            });
            try {
                $handler->handleAll($obj);
            } catch (WaitTimeoutException $e) {
                bot()->reply($e->getMessage());
            }
        } elseif (isset($body['status'], $body['retcode'])) {
            // 如果含有 status，retcode 字段，表明是 action 的 response
            $resp = new ActionResponse();
            $resp->retcode = $body['retcode'];
            $resp->status = $body['status'];
            $resp->message = $body['message'] ?? '';
            $resp->data = $body['data'] ?? null;

            ContainerRegistrant::registerOBActionResponseServices($resp);

            // 调用 BotActionResponse 事件
            $handler = new AnnotationHandler(BotActionResponse::class);
            $handler->setRuleCallback(function (BotActionResponse $event) use ($resp) {
                return $event->retcode === null || $event->retcode === $resp->retcode;
            });
            $handler->handleAll($resp);
        }
    }

    /**
     * 根据事件匹配规则
     *
     * @param OneBotEvent $event 事件对象
     */
    public function matchBotCommand(OneBotEvent $event): ?array
    {
        /** @var BotCommand[] $ls */
        $ls = AnnotationMap::$_list[BotCommand::class] ?? [];
        $msgs = $event->getMessage();
        $head = '';
        $cmd_explode = [];
        $full_str = '';
        foreach ($msgs as $segment) {
            /** @param \MessageSegment $segment */
            if ($segment->type !== 'text') {
                if ($head === '') {
                    continue;
                }
                $cmd_explode[] = $segment;
                continue;
            }
            // 当没识别到命令头的时候，就当作命令头识别
            if ($head === '') {
                $text = $segment->data['text'];
                $full_str .= $text;
                // 切分字符串
                $nlp = MessageUtil::splitMessage(str_replace("\r", '', $text));
                // 啥也没有，分个锤子
                if (empty($nlp)) {
                    continue;
                }
                // 先预留一个给分组而配置的空间
                $cmd_explode = $nlp;
                $head = $nlp[0];
            } else {
                $full_str .= $segment->data['text'];
                // 如果已经识别到了命令头，就当作命令体识别
                $nlp = MessageUtil::splitMessage(str_replace("\r", '', $segment->data['text']));
                if (empty($nlp)) {
                    continue;
                }
                $cmd_explode = array_merge($cmd_explode, $nlp);
            }
            // 先匹配
        }
        if ($head === '') {
            return null;
        }
        // 遍历所有 BotCommand 注解
        foreach ($ls as $v) {
            /** @var BotCommand $v */
            // 测试 deatil_type
            if ($v->detail_type !== '' && $v->detail_type !== $event->detail_type) {
                continue;
            }
            // 测试 prefix
            if ($v->prefix !== '' && mb_strpos($full_str, $v->prefix) !== 0) {
                continue;
            }
            // 测试 match
            if ($v->match !== '' && $v->match === $head) {
                array_shift($cmd_explode);
                return [$v, $cmd_explode, $full_str];
            }
            // 测试 alias
            if ($v->match !== '' && $v->alias !== [] && in_array($head, $v->alias, true)) {
                array_shift($cmd_explode);
                return [$v, $cmd_explode, $full_str];
            }
            // 测试 pattern
            if ($v->pattern !== '' && ($args = match_args($v->pattern, $full_str)) !== false) {
                return [$v, $args, $full_str];
            }
            // 测试 regex
            if ($v->regex !== '' && preg_match('/' . $v->regex . '/u', $full_str, $match) !== 0) {
                return [$v, $match, $full_str];
            }
            // 测试 start_with
            if ($v->start_with !== '' && mb_strpos($full_str, $v->start_with) === 0) {
                return [$v, [mb_substr($full_str, mb_strlen($v->start_with))], $full_str];
            }
            // 测试 end_with
            if ($v->end_with !== '' && mb_substr($full_str, 0 - mb_strlen($v->end_with)) === $v->end_with) {
                return [$v, [mb_substr($full_str, 0, 0 - mb_strlen($v->end_with))], $full_str];
            }
            // 测试 keyword
            if ($v->keyword !== '' && mb_strpos($full_str, $v->keyword) !== false) {
                return [$v, explode($v->keyword, $full_str), $full_str];
            }
        }

        return null;
    }

    /**
     * 根据匹配结果和 CommandArgument 进行匹配
     *
     * @param  array                $match_result 匹配结果的引用
     * @param  BotCommand           $cmd          注解对象
     * @return array|\Generator     返回 array 时为匹配结果，返回 Generator 时为等待结果
     * @throws WaitTimeoutException
     */
    private function matchCommandArguments(array $match_result, BotCommand $cmd): array|\Generator
    {
        $arguments = [];
        /** @var CommandArgument $argument */
        foreach ($cmd->getArguments() as $argument) {
            switch ($argument->type) {
                case 'string':
                case 'any':
                case 'str':
                    $cnt = count($match_result);
                    for ($k = 0; $k < $cnt; ++$k) {
                        $v = $match_result[$k];
                        if (is_string($v)) {
                            array_splice($match_result, $k, 1);
                            $arguments[$argument->name] = $v;
                            break 2;
                        }
                    }
                    if ($argument->required) {
                        // 不够用，且是必需的，就询问用户（这里可能还需要考虑没有协程环境怎么处理）
                        $g = yield $argument;
                        foreach ($g as $v) {
                            if ($v instanceof MessageSegment && $v->type === 'text') {
                                $arguments[$argument->name] = $v->data['text'];
                                break 2;
                            }
                            if (is_string($v)) {
                                $arguments[$argument->name] = $v;
                                break 2;
                            }
                        }
                        if ($argument->error_prompt_policy === 1) {
                            $prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt;
                            $clone_argument = clone $argument;
                            $clone_argument->prompt = $prompt;
                            $g = yield $clone_argument;
                            foreach ($g as $v) {
                                if ($v instanceof MessageSegment && $v->type === 'text') {
                                    $arguments[$argument->name] = $v->data['text'];
                                    break 2;
                                }
                                if (is_string($v)) {
                                    $arguments[$argument->name] = $v;
                                    break 2;
                                }
                            }
                        }
                        throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
                    } else {
                        // 非必需，就使用缺省值
                        $arguments[$argument->name] = $argument->default;
                    }
                    break;
                case 'number':
                    $cnt = count($match_result);
                    // 遍历现存的参数列表，找到第一个数字
                    for ($k = 0; $k < $cnt; ++$k) {
                        $v = $match_result[$k];
                        if (is_numeric($v)) {
                            array_splice($match_result, $k, 1);
                            $arguments[$argument->name] = $v / 1;
                            break 2;
                        }
                    }
                    // 找不到就看看是不是必需的，如果不是必需的，且缺省值是数字，那么就顶上
                    if (!$argument->required && is_numeric($argument->default)) {
                        $arguments[$argument->name] = $argument->default / 1;
                        break;
                    }
                    // 到这里还没找到，就说明需要询问用户了
                    $g = yield $argument;
                    foreach ($g as $v) {
                        if (is_numeric($v)) {
                            $arguments[$argument->name] = $v / 1;
                            break 2;
                        }
                        if ($v instanceof MessageSegment && $v->type === 'text') {
                            if (is_numeric($v->data['text'])) {
                                $arguments[$argument->name] = $v->data['text'] / 1;
                                break 2;
                            }
                        }
                    }
                    if ($argument->error_prompt_policy === 1) {
                        $prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt;
                        $clone_argument = clone $argument;
                        $clone_argument->prompt = $prompt;
                        $g = yield $clone_argument;
                        foreach ($g as $v) {
                            if (is_numeric($v)) {
                                $arguments[$argument->name] = $v / 1;
                                break 2;
                            }
                            if ($v instanceof MessageSegment && $v->type === 'text') {
                                if (is_numeric($v->data['text'])) {
                                    $arguments[$argument->name] = $v->data['text'] / 1;
                                    break 2;
                                }
                            }
                        }
                    }
                    throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
                case 'bool':
                    // 先遍历参数，找到具有布尔值参数的语言
                    $cnt = count($match_result);
                    for ($k = 0; $k < $cnt; ++$k) {
                        $v = $match_result[$k];
                        // 看看有没有true值
                        if (in_array(strtolower($v), TRUE_LIST, true)) {
                            array_splice($match_result, $k, 1);
                            $arguments[$argument->name] = true;
                            break 2;
                        }
                        // 看看有没有false值
                        if (in_array(strtolower($v), FALSE_LIST, true)) {
                            array_splice($match_result, $k, 1);
                            $arguments[$argument->name] = false;
                            break 2;
                        }
                    }
                    // 如果不是必需的，那就使用缺省值
                    if (!$argument->required) {
                        $arguments[$argument->name] = in_array($argument->default === '' ? true : 'true', TRUE_LIST);
                        break;
                    }
                    // 到这里还没找到，就说明需要询问用户了
                    $g = yield $argument;
                    if (in_array(strtolower($g), TRUE_LIST, true)) {
                        $arguments[$argument->name] = true;
                    } elseif (in_array(strtolower($g), FALSE_LIST, true)) {
                        $arguments[$argument->name] = false;
                    } else {
                        if ($argument->error_prompt_policy === 1) {
                            $prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt;
                            $clone_argument = clone $argument;
                            $clone_argument->prompt = $prompt;
                            $g = yield $clone_argument;
                            if (in_array(strtolower($g), TRUE_LIST, true)) {
                                $arguments[$argument->name] = true;
                            } elseif (in_array(strtolower($g), FALSE_LIST, true)) {
                                $arguments[$argument->name] = false;
                            } else {
                                throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
                            }
                        } else {
                            throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
                        }
                    }
                    break;
                default:
                    // 其他类型，处理富文本
                    $msg_type = mb_substr($argument->type, 1);
                    $cnt = count($match_result);
                    for ($k = 0; $k < $cnt; ++$k) {
                        $v = $match_result[$k];
                        if ($v instanceof MessageSegment && $v->type === $msg_type) {
                            array_splice($match_result, $k, 1);
                            $arguments[$argument->name] = $v;
                            break 2;
                        }
                    }
                    // 如果不是必需的，那就使用缺省值
                    if (!$argument->required && is_array($argument->default)) {
                        // 生成一个消息段的段
                        $a = [new MessageSegment($msg_type, $argument->default)];
                        $arguments[$argument->name] = $a;
                        break;
                    }
                    // 到这里还没找到，就说明需要询问用户了
                    $g = yield $argument;
                    foreach ($g as $v) {
                        if ($v instanceof MessageSegment && $v->type === $msg_type) {
                            $arguments[$argument->name] = $v;
                            break 2;
                        }
                    }
                    if ($argument->error_prompt_policy === 1) {
                        $prompt = $argument->getTypeErrorPrompt() . "\n" . $argument->prompt;
                        $clone_argument = clone $argument;
                        $clone_argument->prompt = $prompt;
                        $g = yield $clone_argument;
                        foreach ($g as $v) {
                            if ($v instanceof MessageSegment && $v->type === $msg_type) {
                                $arguments[$argument->name] = $v;
                                break 2;
                            }
                        }
                    }
                    throw new WaitTimeoutException($cmd->name, $argument->getErrorQuitPrompt());
            }
        }
        $arguments['.unnamed'] = $match_result;
        return $arguments;
    }

    /**
     * @throws InterruptException
     * @throws \Throwable
     */
    private function callBotCommand(BotContext $ctx, BotCommand $cmd)
    {
        $handler = new AnnotationHandler(BotCommand::class);
        $handler->setReturnCallback(function ($result) use ($ctx) {
            if (is_string($result)) {
                $ctx->reply($result);
                return;
            }
            try {
                Validator::validateMessageSegment($result);
                $ctx->reply($result);
            } catch (\Throwable) {
            }
            if ($ctx->hasReplied()) {
                AnnotationHandler::interrupt();
            }
        });
        $handler->handle($cmd);
    }
}
