# coding:utf-8

import asyncio
import os
import traceback
from datetime import datetime, UTC, timedelta

import jwt
import uvicorn
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from logzero import setup_logger

from makit.lib import Config
from makit.lib.command import AppCommand
from makit.web import default_config, status
from makit.web.context import Context
from makit.web.convert import CONVERTERS, PathConverter
from makit.web.exceptions import HttpException
from makit.web.middleware.error import HandleServerError
from makit.web.routing import Router
from makit.web.static import index, static_file
from makit.web.types import Scope, Receive, Send


class Application(Router):
    __context__ = Context
    __scheduler_class__ = AsyncIOScheduler

    def __init__(self, prefix='', error_handler=None, config=None, logger=None, prog=None):
        super().__init__(prefix)
        self._base_dir = os.getcwd()
        self.config = Config(default_config).use(config)
        self.logger = logger or setup_logger('makit-web')
        self.lifespan = DefaultLifespan(self)
        self._path_converters = CONVERTERS
        self.scheduler = self.__scheduler_class__() if self.__scheduler_class__ else None
        self.use(HandleServerError, error_handler=error_handler)
        self._command = AppCommand(prog)

    @property
    def base_dir(self):
        return self._base_dir

    @property
    def static_dirs(self):
        dirs = self.config.APP.static_dirs
        if not dirs:
            dirs = [os.path.join(self.base_dir, 'static')]
        return dirs

    def get_config(self, key):
        return self.config.get(key)

    def jwt_encode(self, payload: dict) -> str:
        secret_key = self.config.JWT.get('secret_key')
        if not secret_key:
            raise HttpException(detail='missing jwt config: secret_key')
        algorithm = self.config.JWT.get('algorithm', 'HS256')
        payload['exp'] = datetime.now(UTC) + self.config.JWT.get('expiration', timedelta(days=7))
        audience = self.config.JWT.get('audience', None)
        if audience:
            payload['audience'] = audience
        issuer = self.config.JWT.get('issuer', None)
        if issuer:
            payload['issuer'] = issuer
        return jwt.encode(payload, secret_key, algorithm=algorithm)

    def jwt_decode(self, token: str) -> dict:
        secret_key = self.config.JWT.get('secret_key')
        algorithm = self.config.JWT.get('algorithm', 'HS256')
        payload = jwt.decode(token, secret_key, algorithms=algorithm, verify=True)
        return payload

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        scope_type = scope.get('type')
        if scope_type == 'lifespan':
            await self._lifespan(scope, receive, send)
        elif scope_type == 'http':
            c = self.__context__(scope, receive, send)
            c.app = self
            route = self.matches(c)
            if not route:
                path = scope.get('path')
                await c.respond_json(f'route not found: {path}', status_code=status.HTTP_404_NOT_FOUND)
                return
            await c.next()

    def run(self, **kwargs):
        sys_config = self.config.APP
        host = kwargs.pop('host', sys_config.get('host', '127.0.0.1'))
        port = kwargs.pop('port', sys_config.get('port', 8000))

        self.add_get('/', index)
        # self.router.add_get('/media/{path:path}', media_resource, methods=['GET'])
        # self.router.add_get('/static/{path:path}', static_file)
        self.add_get('/assets/{path:path}', static_file)
        if self.config.APP.debug:
            for route in self.routes:
                self.logger.info(f'route: {route.path_regex.pattern}, {route.methods}, {route.name}')

        uvicorn.run(self, ws_max_size=20 * 1024 * 1024, host=host, port=port, **kwargs)

    def startup(self):
        return self.lifespan.startup()

    def shutdown(self):
        return self.lifespan.shutdown()

    def add_path_converter(self, token: str, cvt: PathConverter):
        self._path_converters[token] = cvt

    def enable_open_api(self):
        """ 启用 OpenAPI """

    def command(self, default=False):
        """
        命令装饰器

        示例：

        app = Application()

        @pp.command(default=True)
        def serve():
            app.run()

        app.run_from_command()

        命令行：python main.py serve  或  python main.py

        :param default: 是否默认命令
        :return:
        """
        return self._command.command(default)

    def run_from_command(self):
        """
        从命令行启动
        :return:
        """
        self._command.run()

    async def _lifespan(self, scope: Scope, receive: Receive, send: Send) -> None:
        started = False
        await receive()
        try:
            async with self.lifespan():
                await send({"type": "lifespan.startup.complete"})
                started = True
                await receive()
        except BaseException:
            exc_text = traceback.format_exc()
            if started:
                await send({"type": "lifespan.shutdown.failed", "message": exc_text})
            else:
                await send({"type": "lifespan.startup.failed", "message": exc_text})
            raise
        else:
            await send({"type": "lifespan.shutdown.complete"})


class DefaultLifespan:
    def __init__(self, app):
        self.app = app
        self._startup_handlers = []
        self._shutdown_handlers = []

    async def __aenter__(self) -> None:
        if self.app.scheduler and not self.app.scheduler.running:
            self.app.scheduler.start()
        for handler in self._startup_handlers:
            if asyncio.iscoroutinefunction(handler):
                await handler()
            else:
                handler()

    async def __aexit__(self, *exc_info: object) -> None:
        for handler in self._shutdown_handlers:
            if asyncio.iscoroutinefunction(handler):
                await handler()
            else:
                handler()

    def __call__(self):
        return self

    def startup(self):
        def deco(func):
            self._startup_handlers.append(func)
            return func

        return deco

    def shutdown(self):
        def deco(func):
            self._shutdown_handlers.append(func)
            return func

        return deco
