<?php

namespace Mainto\RpcServer\RpcServer;

use Doctrine\Common\Annotations\AnnotationReader;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Mainto\RpcServer\Exceptions\BaseServiceException;
use Mainto\RpcServer\Exceptions\RpcRuntimeException;
use Mainto\RpcServer\Protocol\Common\Body;
use Mainto\RpcServer\Protocol\Response\Response;
use Mainto\RpcServer\RpcAnnotations\Alias;
use Mainto\RpcServer\RpcAnnotations\RpcApi;
use Mainto\RpcServer\RpcAnnotations\RpcAuthority;
use Mainto\RpcServer\RpcAnnotations\RpcCron;
use Mainto\RpcServer\RpcAnnotations\RpcDisableTypeValidation;
use Mainto\RpcServer\RpcAnnotations\RpcFormat;
use Mainto\RpcServer\RpcAnnotations\RpcHeader;
use Mainto\RpcServer\RpcAnnotations\RpcMessageHook;
use Mainto\RpcServer\RpcAnnotations\RpcParam;
use Mainto\RpcServer\RpcAnnotations\RpcResponseHeader;
use Mainto\RpcServer\RpcAnnotations\RpcWebsocket;
use Mainto\RpcServer\RpcServer\Definition\Controller;
use Mainto\RpcServer\RpcServer\Definition\Enum;
use Mainto\RpcServer\RpcServer\Definition\Exception;
use Mainto\RpcServer\RpcServer\Definition\ExceptionCode;
use Mainto\RpcServer\RpcServer\Definition\Method;
use Mainto\RpcServer\Service\Struct\Config;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use RuntimeException;

class RpcRouter {

    /**
     * @var RpcRouter
     */
    private static ?RpcRouter $instance = null;
    /**
     * @var string
     */
    private static string $enumsPath = "";
    /**
     * @var string
     */
    private static string $exceptionsPath = "";
    /**
     * 注解解释器
     * @var AnnotationReader
     */
    private AnnotationReader $_annotationReader;
    /**
     * alias Map
     * @var array ['controller' => 'registerName']
     */
    private array $registerMap = [];

    /**
     * RpcRouter constructor.
     */
    private function __construct () {
        $this->_annotationReader = new AnnotationReader();
    }

    public static function getInstance (): RpcRouter {
        if (self::$instance == null) {
            self::$instance = new RpcRouter();
            self::$instance->registerEnums();
            self::$instance->registerExceptions();
        }

        return self::$instance;
    }

    public function registerEnums () {
        RpcDefinition::getInstance()->enums = [];
        if (!self::$enumsPath) {
            self::$enumsPath = app_path('Enums');
        }

        if (!file_exists(self::$enumsPath)) return;

        $enumFiles = get_files(self::$enumsPath);
        foreach ($enumFiles as $_name) {
            if (substr($_name, -8, 8) != 'Enum.php') {
                continue;
            }

            $namespace = must_get_namespace($_name);

            $_nameArr = explode('/', $_name);
            $className = str_replace(['/', '.php'], ['\\', ''], array_pop($_nameArr));

            $enum = $namespace.'\\'.$className;

            $reflection = new ReflectionClass($enum);

            $enum = new Enum();
            $enum->namespace = $reflection->getNamespaceName();
            $enum->name = $reflection->getName();
            $enum->enums = $reflection->getConstants();

            RpcDefinition::getInstance()->enums[] = $enum;
        }
    }

    public function registerExceptions () {
        RpcDefinition::getInstance()->exceptions = [];
        if (!self::$exceptionsPath) {
            self::$exceptionsPath = app_path('Exceptions');
        }

        if (!file_exists(self::$exceptionsPath)) return;

        $exceptionFiles = get_files(self::$exceptionsPath);
        $exceptionCodes = [];
        foreach ($exceptionFiles as $exceptionFile) {
            if (!Str::endsWith($exceptionFile, "Exception.php")) {
                continue;
            }
            $namespace = must_get_namespace($exceptionFile);
            $_nameArr = explode('/', $exceptionFile);
            $className = str_replace(['/', '.php'], ['\\', ''], array_pop($_nameArr));

            $exceptionClass = $namespace.'\\'.$className;
            $reflection = new ReflectionClass($exceptionClass);
            $regex = '/@method[ ]*static[ ]*([A-Z0-9a-z_]?[A-Za-z0-9_]*)[ ]*([A-Z0-9_]*)[ ]*\([ ]*\$(code|codeOrText)[ ]*=[ ]*(0x[0-9A-F]{9})[ ]*,[ ]*\$(text)[ ]*=[ ]*[\'"](.*)[\'"][ ]*\)/m';
            if ($reflection->isSubclassOf(BaseServiceException::class)) {
                $comment = '';
                $exceptionClassName = $alias = $reflection->getName();
                $reader = new AnnotationReader();
                $annotations = $reader->getClassAnnotations($reflection);
                foreach ($annotations as $annotation) {
                    if ($annotation instanceof Alias) {
                        $alias = $annotation->name;
                    }
                }
                $exception = new Exception();
                $exception->alias = $alias;
                $exception->name = $exceptionClassName;

                foreach (explode("\n", $reflection->getDocComment()) as $line) {
                    $line = trim($line, " \t\n\r\0\x0B*");

                    if (strpos($line, '@method') !== false) {
                        if (preg_match_all($regex, $line, $matches, PREG_SET_ORDER, 0)) {
                            [$_, $exceptionName, $exceptionMethodName, $firstParamName, $exceptionCode, $secondParamName, $message] = $matches[0];
                            $code = intval(substr($exceptionCode, 2), 16);
                            if (isset($exceptionCodes[$code])) {
                                throw new RpcRuntimeException("duplicate exception code on {$reflection->getName()}, code : $exceptionCode");
                            }
                            $exceptionCodes[$code] = true;

                            if ($exceptionName != $reflection->getShortName()) {
                                throw new RuntimeException("doc: {$line} return name is not equals class {$reflection->getName()}", 500);
                            }

                            $exceptionCode = new ExceptionCode();
                            $exceptionCode->code = $code;
                            $exceptionCode->comment = $comment ?: $message;
                            $exceptionCode->message = $message;
                            $exceptionCode->exceptionName = $exceptionMethodName;

                            $exception->codes[] = $exceptionCode;
                            $comment = '';
                        } else {
                            throw new RpcRuntimeException("match failed on on {$reflection->getName()}, line content : $line");
                        }
                    }
                    if (strpos($line, '@exception-text') !== false) {
                        $comment = trim(str_replace(['@exception-text', '*'], '', $line));
                    }
                }

                RpcDefinition::getInstance()->exceptions[] = $exception;
            }
        }
    }

    public static function clear () {
        self::$instance = null;
    }

    /**
     * @param string $enumsPath
     */
    public static function setEnumsPath (string $enumsPath): void {
        self::$enumsPath = $enumsPath;
        if (self::$instance) {
            self::$instance->registerEnums();
        }
    }

    /**
     * @param string $exceptionsPath
     */
    public static function setExceptionsPath (string $exceptionsPath): void {
        self::$exceptionsPath = $exceptionsPath;
        if (self::$instance) {
            self::$instance->registerExceptions();
        }
    }

    /**
     * 注册一个RPC路由
     *
     * @param string $class
     * @param string|null $registerName
     */
    public function register (string $class, string $registerName = null) {
        try {
            $refClass = new ReflectionClass($class);
            if (!$registerName) {
                $registerName = $refClass->getName();
                $pos = strrpos($registerName, 'Controllers\\');
                if ($pos !== false) {
                    $registerName = substr($registerName, $pos + 12);
                } else {
                    throw new RuntimeException($class.' not has alias and path error');
                }
            }

            if (!ends_with($registerName, 'Controller')) {
                throw new RuntimeException("class name must be use Controller end");
            }

            $registerName = substr($registerName, 0, -10);

            $this->addController($refClass, $registerName);
        } catch (ReflectionException $e) {
            /** @var RuntimeException $e */
            throw $e;
        }
    }

    /**
     * @param ReflectionClass $refClass
     * @param string $registerName
     * @throws ReflectionException
     */
    public function addController (ReflectionClass $refClass, string $registerName) {
        $definition = RpcDefinition::getInstance();

        if (isset($definition->controllers[$registerName])) {
            if ($refClass->isInstance($definition->controllers[$registerName]->getInstance())) {
                return;
            } else {
                throw new InvalidArgumentException("duplicate class name/alias: {$registerName}");
            }
        }

        $definition->controllers[$registerName] = $controller = new Controller($refClass->newInstance());
        $controller->registerName = $registerName;;
        $controller->name = $refClass->getShortName();
        $controller->namespace = $refClass->getNamespaceName();

        if (!($controller->getInstance() instanceof \Mainto\RpcServer\Base\Controller)) {
            throw new RuntimeException("register router {$refClass->getName()} is not support");
        }

        $methods = $refClass->getMethods(ReflectionMethod::IS_PUBLIC);

        foreach ($methods as $method) {
            $this->addMethod($controller, $method);
        }

        $this->registerMap[$refClass->getName()] = $registerName;
    }

    /**
     * @param Controller $controller
     * @param ReflectionMethod $refMethod
     * @throws ReflectionException
     */
    public function addMethod (Controller $controller, ReflectionMethod $refMethod) {
        $methodName = $refMethod->getShortName();
        if ($methodName[0] == "_") {
            return;
        }

        $controller->methods[$methodName] = $method = new Method($refMethod);
        $method->name = $methodName;

        $defaultResponse = new Response();
        $defaultResponse->useReturnOKType();
        $defaultResponse->setBody(Body::newJsonBody("{}"));

        if ($refMethod->hasReturnType()) {
            $method->responseType = $refMethod->getReturnType()->getName();
        }

        $method->loadDocument($refMethod->getDocComment());

        $annotations = app(AnnotationReader::class)->getMethodAnnotations($refMethod);
        foreach ($annotations as $annotation) {
            if ($annotation instanceof RpcApi) {
                $method->loadFromRpcApi($annotation);
            } elseif ($annotation instanceof RpcHeader) {
                $method->loadFromRpcHeader($annotation);
            } elseif ($annotation instanceof RpcFormat) {
                $method->loadFromRpcFormat($annotation);
            } elseif ($annotation instanceof RpcResponseHeader) {
                $method->loadFromRpcResponseHeader($annotation);
            } elseif ($annotation instanceof RpcAuthority) {
                $method->loadFromRpcAuthority($annotation);
            } elseif ($annotation instanceof RpcMessageHook) {
                $method->loadFromRpcMessageHook($annotation);
            } elseif ($annotation instanceof RpcWebsocket) {
                $method->loadFromRpcWebsocket($annotation);
            } elseif ($annotation instanceof RpcDisableTypeValidation) {
                $method->loadFromRpcDisableTypeValidation($annotation);
            } elseif ($annotation instanceof RpcCron) {
                $method->loadFromRpcCron($annotation, $controller->registerName);
            } elseif ($annotation instanceof RpcParam) {
                $method->loadFromRpcParam($annotation);
            } elseif ($annotation instanceof Alias) {
                $method->loadFromAlias($annotation);
            }
        }

        // validate and cache
        $_ = $method->getParameters();
    }

    public function getController (string $registerClassName): ?Controller {
        return RpcDefinition::getInstance()->controllers[$registerClassName] ?? null;
    }

    public function getControllerMethod (string $registerClassName, $methodName): ?Method {
        return RpcDefinition::getInstance()->controllers[$registerClassName]->methods[$methodName] ?? null;
    }

    public function getConfig (): Config {
        return Config::createFromDefinition(RpcDefinition::getInstance());
    }

    /**
     * 获取控制器注册名
     *
     * @param $class
     * @return string|null
     */
    public function getControllerRegisterName ($class): ?string {
        return $this->registerMap[$class] ?? null;
    }

    /**
     * 获取基础配置信息
     *
     * @return array
     */
    public function getBaseRouterConfig (): array {
        $struct = Config::createFromDefinition(RpcDefinition::getInstance());

        $config = [
            'name'       => config('rpc-server.service_name'),
            'paths'      => $struct->getPaths()->toArray(),
            'crons'      => [],
            'messages'   => [],
            'websockets' => $struct->getWebsockets()->toArray(),
        ];

        if (config('rpc-server.cron.enable')) {
            $config['crons'] = $struct->getCrons()->toArray();
        }

        if (config('rpc-server.sidecar.rabbitmq.enable')) {
            $config['messages'] = $struct->getMessages()->toArray();
        }

        return $config;
    }
}