<?php


namespace Mainto\RpcServer\RpcServer\Definition;


use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Mainto\RpcServer\Exceptions\RpcRuntimeException;
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\RpcContext;
use Mainto\RpcServer\RpcServer\RpcDefinition;
use Mainto\RpcServer\RpcServer\RpcObject;
use Mainto\RpcServer\Util\ArrayHelper;
use Mainto\RpcServer\Util\Language;
use Mainto\RpcServer\Util\ObjectMapper\ClassRef;
use Mainto\RpcServer\Util\Types\Map;
use ReflectionException;
use ReflectionMethod;
use RuntimeException;

class Method implements Arrayable {
    use ArrayHelper;

    /**
     * @var string
     */
    public string $name;

    /**
     * @var string
     */
    public string $alias = "";

    /**
     * @var Deprecated|null
     */
    public ?Deprecated $deprecated = null;

    /**
     * @var string
     */
    public string $description = "";

    /**
     * @var HttpApi|null
     */
    public ?HttpApi $httpApi = null;

    /**
     * @var ReqHeader[]
     */
    public array $reqHeaders = [];

    /**
     * @var string
     */
    public string $requestType = RpcDefinition::EmptyType;

    /**
     * @var string
     */
    public string $responseType = "mixed";

    /**
     * @var string
     */
    public string $resSerialize = "Json";

    /**
     * @var ResHeader[]
     */
    public array $resHeaders = [];

    /**
     * @var bool
     */
    public bool $checkValidation = true;

    /**
     * @var bool
     */
    public bool $checkType = true;

    /**
     * @var array|null
     */
    public array $needAuthorities = [];

    /**
     * @var string|null
     */
    public ?string $wsRoom = null;

    /**
     * @var bool|null
     */
    public bool $wsNeedConnectMessage = false;

    /**
     * @var bool|null
     */
    public bool $wsNeedDisconnectMessage = false;

    /**
     * @var string|null
     */
    public ?string $messageHookName = null;

    /**
     * @var string|null
     */
    public ?string $cronSpec = null;

    /**
     * @var string|null
     */
    public ?string $cronHookName = null;

    /**
     * @var Map<RequestExample>|RequestExample[]
     */
    public Map $requestExamples;

    /**
     * @var string
     */
    public string $since = "1.0.0";

    private ?ReflectionMethod $refInstance;

    /**
     * @var array|null [name]
     */
    private ?array $requestDeclareMap = null;

    /**
     * @var array|null [name]
     */
    private ?array $requestDeclareNames = null;

    /**
     * @var array|null [name => Property]
     */
    private ?array $parameters = null;

    public function __construct (ReflectionMethod $refInstance = null) {
        $this->refInstance = $refInstance;
        $this->requestExamples = new Map();
        $this->alias = $refInstance ? $refInstance->getShortName() : "";
    }

    /**
     * @param array $params
     * @throws ValidationException
     */
    public function validate (array &$params) {
        $requestType = RpcDefinition::getInstance()->getStruct($this->requestType);

        $this->handlerDefaultAndType($requestType, $params);

        $requestType->validate($params);
    }

    private function handlerDefaultAndType (Struct $requestType, array &$params) {
        foreach ($requestType->properties as $property) {
            [$exists, $value] = $this->keyExists($params, $property->name);

            if ($property->defaultValueAvailable && !$exists) {
                data_set($params, $property->name, $property->default);
                $value = $property->default;
                $exists = true;
            }

            if ($exists && RpcDefinition::getInstance()->inStructCache($property->type)) {
                $this->handlerDefaultAndType(RpcDefinition::getInstance()->getStruct($property->type), $value);

                data_set($params, $property->name, $value);
                continue;
            }

            if ($this->checkType && $exists) {
                // 检查可空的类型
                if ($value === null && $property->nullable) {
                    continue;
                }

                if (!Language::tryAutoFixType($value, $property->type)) {
                    $prefix = ($requestType instanceof NotDefinitionStruct) ? $requestType->originName : "";
                    $key = ltrim($prefix ? "$prefix.$property->name" : $property->name, '.');
                    throw new RpcRuntimeException("key `{$key}` is not a {$property->type} instance", 422);
                }

                data_set($params, $property->name, $value);
            }
        }
    }

    private function keyExists ($target, $key) {
        $value = data_get($target, $key, Language::$placeholder);

        return [$value !== Language::$placeholder, $value];
    }

    public function buildMethodParams (RpcContext $context, array $params) {
        $methodParams = [];

        foreach ($this->getParameters() as $name => $parameter) {
            if ($parameter->type === RpcContext::class) {
                $methodParams[] = $context;
            } else {
                if (is_subclass_of($parameter->type, RpcObject::class)) {
                    /** @var RpcObject $object */
                    $object = new $parameter->type;

                    RpcDefinition::getInstance()->getMapper()->map($params, $object);
                    $methodParams[] = $object;
                } else {
                    $methodParams[] = $params[$name] ?? null;
                }
            }
        }

        return $methodParams;
    }

    /**
     * @return Parameter[]
     * @throws ReflectionException
     */
    public function getParameters (): array {
        if ($this->parameters === null) {
            $this->parameters = [];

            foreach ($this->refInstance->getParameters() as $parameterRef) {
                $parameter = new Parameter();
                $parameter->name = $parameterRef->name;
                $parameter->nullable = $parameterRef->allowsNull();

                if (isset($this->getRequestDeclareMap()[$parameterRef->name])) {
                    /** @var Property $property */
                    $property = $this->getRequestDeclareMap()[$parameterRef->name];
                    $property->nullable = $parameterRef->allowsNull();
                    if ($parameterRef->isDefaultValueAvailable()) {
                        $property->default = $parameterRef->getDefaultValue();
                        $property->defaultValueAvailable = true;
                    } else {
                        $property->require = true;
                    }

                    $parameter->type = $property->type;
                    $parameter->default = $property->default;
                    $parameter->defaultValueAvailable = $property->defaultValueAvailable;
                    $parameter->require = $property->require;

                    $this->parameters[$parameterRef->name] = $parameter;
                    continue;
                }
                if ($parameterRef->getType()) {
                    $parameter->type = $parameterRef->getType()->getName();
                    if ($parameterRef->isDefaultValueAvailable()) {
                        $parameter->default = $parameterRef->getDefaultValue();
                        $parameter->defaultValueAvailable = true;
                    } else {
                        $parameter->require = true;
                    }

                    $this->parameters[$parameterRef->name] = $parameter;

                    if (is_subclass_of($parameterRef->getType()->getName(), RpcObject::class)) {
                        if ($this->requestType != RpcDefinition::EmptyType) {
                            if (!is_subclass_of($this->requestType, RpcObject::class)) {
                                throw new RuntimeException("There can only be one in RpcObject and RpcParam");
                            }

                            throw new RuntimeException("RpcObject only support one");
                        }

                        $_ = RpcDefinition::getInstance()->objectStruct($parameterRef->getType()->getName());
                        $this->requestType = $parameterRef->getType()->getName();
                        continue;
                    }

                    if ($parameterRef->getType()->getName() == RpcContext::class) {
                        continue;
                    }
                }

                throw new RuntimeException("the rpc param [{$parameterRef->name}] not defined in [{$this->refInstance->class}::{$this->name}]");
            }
        }

        return $this->parameters;
    }

    /**
     * @return array
     */
    public function getRequestDeclareMap (): array {
        if ($this->requestDeclareMap === null) {
            $this->requestDeclareMap = [];
            foreach (RpcDefinition::getInstance()->getStruct($this->requestType)->properties as $property) {
                $this->requestDeclareMap[$property->name] = $property;
            }
        }

        return $this->requestDeclareMap;
    }

    public function loadFromRpcApi (RpcApi $rpcApi) {
        $url = $rpcApi->url;
        if ($rpcApi->addPrefix && config('rpc-server.url.enable_prefix')) {
            $url = path_join(config('rpc-server.url.prefix'), $url);
        }

        $this->httpApi = new HttpApi();
        $this->httpApi->method = $rpcApi->method;
        $this->httpApi->url = $url;
    }

    public function loadFromRpcHeader (RpcHeader $rpcHeader) {
        $header = new ReqHeader();
        $header->name = $rpcHeader->name;
        $this->reqHeaders[] = $header;
    }

    public function loadFromRpcFormat (RpcFormat $rpcFormat) {
        $this->resSerialize = $rpcFormat->format;
    }

    public function loadFromRpcResponseHeader (RpcResponseHeader $rpcResponseHeader) {
        $header = new ResHeader();
        $header->name = $rpcResponseHeader->name;
        $header->value = $rpcResponseHeader->value;
        $this->resHeaders[] = $header;
    }

    public function loadFromRpcAuthority (RpcAuthority $rpcAuthority) {
        $this->needAuthorities[] = $rpcAuthority->need;
    }

    public function loadFromRpcMessageHook (RpcMessageHook $rpcMessageHook) {
        // 兼容旧websocket申明
        if (strpos($rpcMessageHook->name, "ws.") === 0) {
            print "\n###!!!!! RpcMessageHook ws.* for websocket is DEPRECATED and will be removed in a future version. Use RpcWebsocket instead.\n";
            $this->wsNeedConnectMessage = true;
            $this->wsNeedDisconnectMessage = true;
            $this->wsRoom = substr($rpcMessageHook->name, 3);
        } else {
            $this->messageHookName = $rpcMessageHook->name;
        }
    }

    public function loadFromRpcWebsocket (RpcWebsocket $annotation) {
        $this->wsNeedConnectMessage = $annotation->needConnectMessage;
        $this->wsNeedDisconnectMessage = $annotation->needDisconnectMessage;
        $this->wsRoom = $annotation->roomName;
    }

    public function loadFromRpcDisableTypeValidation (RpcDisableTypeValidation $annotation) {
        $this->checkType = $annotation->check_type;
        $this->checkValidation = $annotation->check_validation;
    }

    public function loadFromRpcCron (RpcCron $annotation, $classAliasName) {
        $annotation->every and $this->cronSpec = '@every '.$annotation->every;
        $annotation->entry and $this->cronSpec = '@'.$annotation->entry;
        $annotation->spec and $this->cronSpec = str_replace('\\', '/', $annotation->spec);

        $this->cronHookName = "{$classAliasName}::{$this->name}";
    }

    public function loadFromRpcParam (RpcParam $rpcParam) {
        if ($this->requestType == RpcDefinition::EmptyType) {
            $this->requestType = sprintf("%s\\%sReq", $this->refInstance->getDeclaringClass()->getName(), Str::studly($this->name));
        }

        $struct = RpcDefinition::getInstance()->notDefinitionStruct($this->requestType);
        $property = new Property();

        $names = explode('.', $rpcParam->name);
        $prefixType = $this->requestType;
        foreach ($names as $index => $child) {
            if ($index == count($names) - 1) {
                $property->require = $rpcParam->require;
                $property->validation = $rpcParam->validation;
                if (is_subclass_of($rpcParam->type, RpcObject::class)) {
                    $_ = RpcDefinition::getInstance()->objectStruct($rpcParam->type);
                }

                $property->type = $rpcParam->type;
                $property->nullable = $rpcParam->nullable;
                $property->name = $child;

                if ($rpcParam->default !== null) {
                    $property->default = $rpcParam->default;
                    $property->defaultValueAvailable = true;
                }
                $property->comment = $rpcParam->comment;
                $struct->properties[] = $property;
            } else {
                $childProperty = $this->findProperty($struct->properties, $child);
                if (!$childProperty) {
                    $childStruct = RpcDefinition::getInstance()->notDefinitionStruct($prefixType.Str::studly($child));
                    $childStruct->originName = implode('.', array_slice($names, 0, $index + 1));
                    $childProperty = new Property();
                    $childProperty->name = $child;
                    $childProperty->type = $prefixType.Str::studly($child);
                    $struct->properties[] = $childProperty;
                    $struct = $childStruct;
                } else {
                    $struct = RpcDefinition::getInstance()->notDefinitionStruct($childProperty->type);
                }

                $prefixType = $prefixType.Str::studly($child);
            }
        }
    }

    private function findProperty ($properties, $name): ?Property {
        /** @var Property[] $properties */
        foreach ($properties as $property) {
            if ($property->name == $name) {
                return $property;
            }
        }

        return null;
    }

    /**
     * @return array
     */
    public function getRequestDeclareNames (): array {
        if ($this->requestDeclareNames === null) {
            $this->requestDeclareNames = [];
            foreach ($this->getRequestDeclareMap() as $name => $property) {
                $this->getPropertyNames($this->requestDeclareNames, "", $property);
            }
        }

        return $this->requestDeclareNames;
    }

    private function getPropertyNames (&$names, $prefix, Property $property) {
        $name = $prefix ? $prefix.'.'.$property->name : $property->name;

        if (RpcDefinition::getInstance()->inStructCache($property->type)) {
            foreach (RpcDefinition::getInstance()->getStruct($property->type)->properties as $childProperty) {
                $this->getPropertyNames($names, $name, $childProperty);
            }
        } else {
            $names[] = $name;
        }
    }

    /**
     * @return ReflectionMethod
     */
    public function getRefInstance (): ?ReflectionMethod {
        return $this->refInstance;
    }

    public function loadFromAlias (Alias $annotation) {
        $this->alias = $annotation->name;
    }

    /**
     * handler doc, deprecated, return type
     *
     * @param string $docComment
     */
    public function loadDocument (string $docComment) {
        $this->description = "";

        $useClasses = ClassRef::parseUseClasses($this->refInstance->getFileName());
        $returnType = "";
        foreach (explode("\n", $docComment) as $line) {
            if (strpos($line, '@') === false && strpos($line, '/') === false) {
                $_doc = trim(str_replace('*', '', $line));
                if (!$_doc) continue;

                $this->description .= $_doc."\n";
                continue;
            }
            if (strpos($line, '@deprecated') !== false) {
                $deprecated = new Deprecated();
                $deprecated->reason = trim(substr($line, strpos($line, '@deprecated') + 11));
                $this->deprecated = $deprecated;
            }

            if (strpos($line, '@since') !== false) {
                $this->since = trim(substr($line, strpos($line, '@since') + 6));
            }

            if (!$returnType && strpos($line, '@return') !== false) {
                $returnType = trim(str_replace(['@return', '*'], '', $line));
                if ($this->responseType !== "mixed") {
                    if ($this->responseType === 'array') {
                        $arrayType = null;
                        // handle array type
                        if (preg_match('/(.*)\[]/', $returnType, $match) && count($match) > 1) {
                            $arrayType = $match[1];
                        }
                        if (preg_match('/array<(.*)>/', $returnType, $match) && count($match) > 1) {
                            $types = explode(',', $match[1]);
                            $arrayType = array_pop($types);
                        }

                        if ($arrayType) {
                            if (in_array($arrayType, Language::$simpleType)) {
                                $this->responseType = $arrayType.'[]';
                            } elseif (isset($useClasses[$arrayType])) {
                                $this->responseType = $useClasses[$arrayType].'[]';
                            } else {
                                $this->responseType = $this->refInstance->getNamespaceName().'\\'.$arrayType.'[]';
                            }
                        }
                    }
                } else {
                    if (in_array($returnType, ['int', 'array', 'string', 'float', 'bool', 'integer', 'boolean'])) {
                        $this->responseType = Language::$simpleTypeMap[$returnType];
                    }
                }
            }
        }

        $this->description = rtrim($this->description, "\n");
    }
}