# coding:utf-8

import asyncio
import inspect
import re
import typing as t
from functools import cached_property

import pydantic

from makit.lib import inspect
from makit.web import status
from makit.web.context import Context
from makit.web.convert import CONVERTERS
from makit.web.exceptions import InvalidMiddlewareError

__all__ = [
    'Route',
    'Router'
]

from makit.web.middleware.base import HandlerMiddleware, Middleware, MiddlewareHandler, MiddlewareClass

PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-zA-Z_][a-zA-Z0-9_]*))?}")


class Router:

    def __init__(self, prefix='', parent=None):
        self.prefix = prefix or ''
        self._routes: t.List['Route'] = []
        self.parent: t.Optional['Router'] = parent
        self.children: t.List['Router'] = []
        self.middlewares = []

    @cached_property
    def path(self):
        result = self.prefix
        r = self
        while r.parent:
            result = r.parent.prefix + result
            r = r.parent
        return result

    @property
    def routes(self):
        return list(self.__iter__())

    def matches(self, c: Context) -> t.Optional['Route']:
        path = c.scope.get('path')
        match = re.compile('^' + self.path).match(path)
        if not match:
            return None
        # c.use_middleware(*self.middlewares)
        for route in self._routes:
            match = route.matches(c)
            if match:
                return c.route

        for child in self.children:
            route = child.matches(c)
            if route:
                return route
        return None

    def sub_router(self, prefix=''):
        router = Router(prefix=prefix, parent=self)
        self.children.append(router)
        return router

    def include(self, router):
        router.parent = self
        self.children.append(router)

    def use(self, middleware: MiddlewareHandler | MiddlewareClass, *args, **kwargs):
        if inspect.isroutine(middleware):
            self.middlewares.append((HandlerMiddleware, [middleware], dict()))
        elif inspect.isclass(middleware) and issubclass(middleware, Middleware):
            self.middlewares.append((middleware, args, kwargs))
        else:
            raise InvalidMiddlewareError('Middleware must be MiddlewareHandler or Middleware class')

    def route(self, path: str, methods: t.List[str], **kwargs):

        def deco(f):
            self.add_route(path, f, methods=methods, **kwargs)

        return deco

    def get(self, path: str, **kwargs):
        return self.route(path, ['GET'], **kwargs)

    def post(self, path: str, **kwargs):
        return self.route(path, ['POST'], **kwargs)

    def put(self, path: str, **kwargs):
        return self.route(path, ['PUT'], **kwargs)

    def patch(self, path: str, **kwargs):
        return self.route(path, ['PATCH'], **kwargs)

    def delete(self, path: str, **kwargs):
        return self.route(path, ['DELETE'], **kwargs)

    def add_route(self, path: str, endpoint, methods: t.List[str], **kwargs):
        route = Route(path, endpoint, methods, router=self, **kwargs)
        self._routes.append(route)
        return route

    def add_get(self, path: str, endpoint, **kwargs):
        return self.add_route(path, endpoint, methods=['GET'], **kwargs)

    def add_post(self, path: str, endpoint, **kwargs):
        return self.add_route(path, endpoint, methods=['POST'], **kwargs)

    def add_put(self, path: str, endpoint, **kwargs):
        return self.add_route(path, endpoint, methods=['PUT'], **kwargs)

    def add_patch(self, path: str, endpoint, **kwargs):
        return self.add_route(path, endpoint, methods=['PATCH'], **kwargs)

    def add_delete(self, path: str, endpoint, **kwargs):
        return self.add_route(path, endpoint, methods=['DELETE'], **kwargs)

    def add_head(self, path: str, endpoint, **kwargs):
        return self.add_route(path, endpoint, methods=['HEAD'], **kwargs)

    def __repr__(self):
        return f'<Router {self.path}>'

    def __iter__(self):
        for route in self._routes:
            yield route
        for router in self.children:
            for route in router:
                yield route


class Route:
    def __init__(self, path: str, handler, methods, router=None, **kwargs):
        self.path = path
        self.handler = handler
        self.methods = methods
        if 'GET' in methods:
            self.methods.append('HEAD')
        self._data = kwargs
        self.param_converters = {}
        self.path_params = dict()
        self.router = router
        self.__handler_func = inspect.Method(handler)
        # self.openapi_params = []
        # for arg in f.args:
        #     if issubclass(arg.type, pydantic.BaseModel):
        #         self.openapi_params.append(arg)

    @cached_property
    def name(self):
        name = self._data.get('name')
        if name:
            return name
        return inspect.obj_name(self.handler)

    @cached_property
    def path_regex(self):
        match = PARAM_REGEX.search(self.path)
        i = 0
        regex = ''
        if not match:
            return re.compile(self.router.path + self.path + '$')
        for match in PARAM_REGEX.finditer(self.path):
            p, t = match.groups("str")  # TODO
            if t not in CONVERTERS:
                raise RouteError(f'Unsupported path converter type: {t}')
            if p in self.param_converters:
                raise RouteError(f'Duplicate path param: {p}')
            self.param_converters[p] = cvt = CONVERTERS[t]
            regex += self.path[i: match.start()]
            regex += f"(?P<{p}>{cvt.regex})"
            i = match.end()
        regex += self.path[i:] + '$'
        regex = (self.router.path + regex).replace('//', '/')
        return re.compile(regex)

    @property
    def data(self):
        """
        Data bound to route
        :return:
        """
        return self._data

    @cached_property
    def middlewares(self):
        router = self.router
        stack = list(router.middlewares)
        while router.parent:
            for middleware in reversed(router.parent.middlewares):
                stack.insert(0, middleware)
            router = router.parent
        return stack

    def matches(self, c: Context):
        path = c.scope['path']
        match = self.path_regex.fullmatch(path)
        if match and (c.method in self.methods or c.method == 'OPTIONS'):
            path_params = match.groupdict()
            for key, value in path_params.items():
                path_params[key] = self.param_converters[key]().convert(value)
            self.path_params = path_params
            c.scope['route'] = self
            return True
        return False

    async def __call__(self, c: Context):
        scope = c.scope
        method = scope['method']
        if method != "OPTIONS" and scope["method"] not in self.methods:
            headers = {"Allow": ", ".join(self.methods)}
            await c.respond_text(
                "Method Not Allowed",
                status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
                headers=headers
            )
        else:
            if not self.__handler_func:
                self.__handler_func = inspect.Method(self.handler)
            f = self.__handler_func
            args, kwargs = [], {}
            json_data = await c.json()
            kwargs.update(json_data)
            kwargs.update(c.query)
            kwargs.update(c.route.path_params)
            for p in f.args:
                if p.name == 'self':
                    args.append(object.__new__(f.cls))
                if not inspect.isclass(p.type):
                    continue
                if issubclass(p.type, pydantic.BaseModel):
                    kwargs[p.name] = p.type(**kwargs)
                elif issubclass(p.type, Context):
                    kwargs[p.name] = c
            if asyncio.iscoroutinefunction(self.handler):
                output = await inspect.async_run(self.handler, *args, **kwargs)
            else:
                output = inspect.run(self.handler, *args, **kwargs)
            output = c.handle_api_output(output)
            if not c.response_completed:
                if output is not None:
                    await c.respond_json(output)
                else:
                    await c.respond_json(dict(message='OK'))

    def __getattr__(self, item):
        value = self._data.get(item, None)
        return value

    def __getitem__(self, item):
        return self._data.get(item, None)

    def __repr__(self):
        return f'<Route {self.path_regex}>'


class RouteError(Exception):
    """"""


class RouteDataError(Exception):
    """"""
