#  Copyright (C) 2021
#  ABM, Moscow
#
#  UNPUBLISHED PROPRIETARY MATERIAL.
#  ALL RIGHTS RESERVED.
#
#  Authors:
#  Mike Orlov <m.orlov@techokert.ru>
import json
import logging
import pathlib
from dataclasses import dataclass
from typing import Callable, Any

import yarl
from http_tools import HttpServer
from http_tools.answer import FileAnswer, RedirectAnswer
from http_tools.mime_types import ContentType
from http_tools.request import IncomingRequest
from init_helpers import custom_dumps, raise_if

from .endpoint import Endpoint
from openapi_tools.spec.spec_generator import SpecGenerator


logger = logging.getLogger(__name__)


def _return_empty_dict(*_: Any, **__: Any):
    return {}


class OpenApiServer:
    @dataclass
    class Context(SpecGenerator.Context):
        http_server: HttpServer

    @dataclass
    class Config(SpecGenerator.Config):
        write_spec_to_file: str = ""  # empty means skip
        http_prefix: str = '/.docs/'
        spec_http_path: str = 'openapi'  # empty means disable
        ui_http_path: str = 'ui'  # empty means disable

        def __post_init__(self):
            if prefix := self.http_prefix:
                raise_if(not prefix[0] == '/', ValueError(f"Http prefix MUST start with '/', got: {prefix}"))
                raise_if(not prefix[-1] == '/', ValueError(f"Http prefix MUST end with '/', got: {prefix}"))
            if spec_path := self.spec_http_path:
                raise_if('/' in spec_path, NotImplementedError(f"spec_http_path with '/' unsupported: {spec_path}"))
            if ui_path := self.ui_http_path:
                raise_if('/' in ui_path, NotImplementedError(f"ui_http_path with '/' unsupported: {ui_path}"))

    @property
    def _spec_path(self) -> str | None:
        if self.config.spec_http_path:
            return yarl.URL(self.config.http_prefix).join(yarl.URL(self.config.spec_http_path)).path

    @property
    def json_spec_path(self) -> str | None:
        if self._spec_path:
            return f'{self._spec_path}.json'

    @property
    def yaml_spec_paths(self) -> tuple[str, ...] | None:
        if self._spec_path:
            return f'{self._spec_path}.yml', f'{self._spec_path}.yaml'

    @property
    def ui_path(self) -> str | None:
        if self.config.ui_http_path:
            return yarl.URL(self.config.http_prefix).join(yarl.URL(self.config.ui_http_path)).path

    def __init__(
            self, config: Config, context: Context,
            spec_generator_factory: Callable[[Config, Context], SpecGenerator] = SpecGenerator
    ):
        self.context = context
        self.config = config
        self.spec_generator = spec_generator_factory(config, context)
        if self._spec_path:
            http_server = self.context.http_server
            http_server.register_handler(self._spec_path, self.get_spec_json)  # compat
            http_server.register_handler(self.json_spec_path, self.get_spec_json)
            for path in self.yaml_spec_paths:
                http_server.register_handler(path, self.get_spec_yaml)
            if self.ui_path:
                full_ui_path = self.ui_path + '/'
                http_server.register_handler(full_ui_path, self.get_ui)
                http_server.register_handler(self.ui_path, lambda _: RedirectAnswer(full_ui_path))
                ui_sub_path = yarl.URL(full_ui_path).join(yarl.URL('{file_name}')).path
                http_server.register_handler(ui_sub_path, self.get_ui)
                swagger_url = f'http://{http_server.config.interface}:{http_server.config.port}{full_ui_path}'
                logger.info(f'Swagger UI available at {swagger_url}')
        elif self.ui_path:
            logger.warning('Swagger UI disabled due to disabled spec endpoint')

    async def get_ui(self, request: IncomingRequest) -> FileAnswer:
        file_name = request.key_value_params.get('file_name', 'index.html')
        target = pathlib.Path(__file__).parent.parent / 'swagger-ui' / file_name
        if file_name.endswith('.html'):
            content = target.read_text()
            content_type = ContentType.HTML
        elif file_name.endswith('.css'):
            content = target.read_text()
            content_type = ContentType.Css
        elif file_name.endswith('.js'):
            content = target.read_text()
            content_type = ContentType.JavaScript
        elif file_name.endswith('.png'):
            content = target.read_bytes()
            content_type = ContentType.PNG
        else:
            raise TypeError(f"Unsupported file type: {file_name}")
        if file_name == 'swagger-initializer.js':
            content = content.replace(
                'https://petstore.swagger.io/v2/swagger.json', f'../{self.config.spec_http_path}.yaml')
        return FileAnswer(content, content_type=content_type)

    async def get_spec_json(self, _: IncomingRequest) -> FileAnswer:
        return FileAnswer(self.spec_generator.get_spec_json(), file_name='openapi.json', content_type=ContentType.Json)

    async def get_spec_yaml(self, _: IncomingRequest) -> FileAnswer:
        return FileAnswer(self.spec_generator.get_spec_yaml(), file_name='openapi.yaml', content_type=ContentType.Yaml)

    def _write_spec_to_file(self):
        spec = self.spec_generator.get_spec_dict()
        if self.config.write_spec_to_file:
            json_res = custom_dumps(spec, indent=2)
            with open(self.config.write_spec_to_file, "w") as f:
                f.write(json_res)

    def register_endpoint(self, rpc_endpoint: Endpoint) -> None:
        self.register(rpc_endpoint)

    def register(self, *rpc_endpoints: Endpoint) -> None:
        for endpoint in rpc_endpoints:
            self.context.http_server.register_handler(endpoint.path, endpoint.gen_handler(), [endpoint.method])
            self.spec_generator.register(endpoint.as_operation())
            self._write_spec_to_file()
