from __future__ import annotations

"""
Builders for the oas3 object types

These are completely internal, so can be refactored if desired without concern
for breaking user experience
"""
from collections import defaultdict
from typing import TYPE_CHECKING, Optional, Sequence, Union

from sanic_ext.extensions.openapi.constants import (
    SecuritySchemeAuthorization,
    SecuritySchemeLocation,
    SecuritySchemeType,
)

from ...utils.route import remove_nulls, remove_nulls_from_kwargs
from .autodoc import YamlStyleParametersParser
from .definitions import (
    Any,
    Components,
    Contact,
    Dict,
    ExternalDocumentation,
    Flows,
    Info,
    License,
    List,
    OpenAPI,
    Operation,
    Parameter,
    PathItem,
    RequestBody,
    Response,
    SecurityRequirement,
    SecurityScheme,
    Server,
    Tag,
)

if TYPE_CHECKING:
    from sanic import Sanic


class OperationBuilder:
    summary: str
    description: str
    operationId: str
    requestBody: RequestBody
    externalDocs: ExternalDocumentation
    tags: List[str]
    security: List[Any]
    parameters: List[Parameter]
    responses: Dict[str, Response]
    callbacks: List[str]  # TODO
    deprecated: bool = False

    def __init__(self):
        self.tags = []
        self.security = []
        self.parameters = []
        self.responses = {}
        self._default = {}
        self._autodoc = None
        self._exclude = False
        self._allow_autodoc = True
        self._app: Optional[Sanic] = None

    def name(self, value: str):
        self.operationId = value

    def describe(self, summary: str = None, description: str = None):
        if summary:
            self.summary = summary

        if description:
            self.description = description

    def document(self, url: str, description: str = None):
        self.externalDocs = ExternalDocumentation.make(url, description)

    def tag(self, *args: str):
        for arg in args:
            if isinstance(arg, Tag):
                arg = arg.fields["name"]
            self.tags.append(arg)

    def deprecate(self):
        self.deprecated = True

    def body(self, content: Any, **kwargs):
        self.requestBody = RequestBody.make(content, **kwargs)

    def parameter(
        self, name: str, schema: Any, location: str = "query", **kwargs
    ):
        self.parameters.append(
            Parameter.make(name, schema, location, **kwargs)
        )

    def response(
        self, status, content: Any = None, description: str = None, **kwargs
    ):
        response = Response.make(content, description, **kwargs)
        if status in self.responses:
            self.responses[status]._fields["content"].update(
                response.fields["content"]
            )
        else:
            self.responses[status] = response

    def secured(self, *args, **kwargs):
        if not kwargs and len(args) == 1 and isinstance(args[0], dict):
            items = args[0]
        else:
            items = {**{v: [] for v in args}, **kwargs}
        gates = {}

        for name, params in items.items():
            gate = name.__name__ if isinstance(name, type) else name
            gates[gate] = params

        self.security.append(gates)

    def disable_autodoc(self):
        self._allow_autodoc = False

    def build(self):
        operation_dict = self._build_merged_dict()
        if "responses" not in operation_dict:
            # todo -- look into more consistent default response format
            operation_dict["responses"] = {"default": {"description": "OK"}}

        return Operation(**operation_dict)

    def _build_merged_dict(self):
        defined_dict = self.__dict__.copy()
        autodoc_dict = self._autodoc or {}
        default_dict = self._default
        merged_dict = {}

        for d in (default_dict, autodoc_dict, defined_dict):
            cleaned = {
                k: v for k, v in d.items() if v and not k.startswith("_")
            }
            merged_dict.update(cleaned)

        return merged_dict

    def autodoc(self, docstring: str):
        y = YamlStyleParametersParser(docstring)
        self._autodoc = y.to_openAPI_3()

    def exclude(self, flag: bool = True):
        self._exclude = flag


class OperationStore(defaultdict):
    _singleton = None

    def __new__(cls) -> Any:
        if not cls._singleton:
            cls._singleton = super().__new__(cls)
        return cls._singleton

    def __init__(self):
        super().__init__(OperationBuilder)

    @classmethod
    def reset(cls):
        cls._singleton = None


class SpecificationBuilder:
    _urls: List[str]
    _title: str
    _version: str
    _description: Optional[str]
    _terms: Optional[str]
    _contact: Contact
    _license: License
    _paths: Dict[str, Dict[str, OperationBuilder]]
    _tags: Dict[str, Tag]
    _security: List[SecurityRequirement]
    _components: Dict[str, Any]
    _servers: List[Server]
    # _components: ComponentsBuilder
    # deliberately not included
    _singleton = None

    def __new__(cls) -> Any:
        if not cls._singleton:
            cls._singleton = super().__new__(cls)
            cls._setup_instance(cls._singleton)
        return cls._singleton

    @classmethod
    def _setup_instance(cls, instance):
        instance._components = defaultdict(dict)
        instance._contact = None
        instance._description = None
        instance._external = None
        instance._license = None
        instance._paths = defaultdict(dict)
        instance._servers = []
        instance._tags = {}
        instance._security = []
        instance._terms = None
        instance._title = None
        instance._urls = []
        instance._version = None

    @classmethod
    def reset(cls):
        cls._singleton = None

    @property
    def tags(self):
        return self._tags

    @property
    def security(self):
        return self._security

    def url(self, value: str):
        self._urls.append(value)

    def describe(
        self,
        title: str,
        version: str,
        description: Optional[str] = None,
        terms: Optional[str] = None,
    ):
        self._title = title
        self._version = version
        self._description = description
        self._terms = terms

    def _do_describe(
        self,
        title: str,
        version: str,
        description: Optional[str] = None,
        terms: Optional[str] = None,
    ):
        if any([self._title, self._version, self._description, self._terms]):
            return
        self.describe(title, version, description, terms)

    def tag(self, name: str, description: Optional[str] = None, **kwargs):
        self._tags[name] = Tag(name, description=description, **kwargs)

    def external(self, url: str, description: Optional[str] = None, **kwargs):
        self._external = ExternalDocumentation(url, description=description)

    def secured(
        self,
        name: str = None,
        value: Optional[Union[str, Sequence[str]]] = None,
    ):
        if value is None:
            value = []
        elif isinstance(value, str):
            value = [value]
        else:
            value = list(value)
        self._security.append(SecurityRequirement(name=name, value=value))

    def contact(self, name: str = None, url: str = None, email: str = None):
        kwargs = remove_nulls_from_kwargs(name=name, url=url, email=email)
        self._contact = Contact(**kwargs)

    def _do_contact(
        self, name: str = None, url: str = None, email: str = None
    ):
        if self._contact:
            return

        self.contact(name, url, email)

    def license(self, name: str = None, url: str = None):
        if name is not None:
            self._license = License(name, url=url)

    def _do_license(self, name: str = None, url: str = None):
        if self._license:
            return

        self.license(name, url)

    def operation(self, path: str, method: str, operation: OperationBuilder):
        for _tag in operation.tags:
            if _tag in self._tags.keys():
                continue

            self._tags[_tag] = Tag(_tag)

        self._paths[path][method.lower()] = operation

    def add_component(self, location: str, name: str, obj: Any):
        self._components[location].update({name: obj})

    def has_component(self, location: str, name: str) -> bool:
        return name in self._components.get(location, {})

    def add_security_scheme(
        self,
        ident: str,
        type: Union[str, SecuritySchemeType],
        *,
        bearer_format: Optional[str] = None,
        description: Optional[str] = None,
        flows: Optional[Union[Flows, Dict[str, Any]]] = None,
        location: Union[
            str, SecuritySchemeLocation
        ] = SecuritySchemeLocation.HEADER,
        name: str = "authorization",
        openid_connect_url: Optional[str] = None,
        scheme: Union[
            str, SecuritySchemeAuthorization
        ] = SecuritySchemeAuthorization.BEARER,
    ):
        if isinstance(type, str):
            type = SecuritySchemeType(type)
        if isinstance(location, str):
            location = SecuritySchemeLocation(location)

        kwargs: Dict[str, Any] = {"type": type, "description": description}

        if type is SecuritySchemeType.API_KEY:
            kwargs["location"] = location
            kwargs["name"] = name
        elif type is SecuritySchemeType.HTTP:
            kwargs["scheme"] = scheme
            kwargs["bearerFormat"] = bearer_format
        elif type is SecuritySchemeType.OAUTH2:
            kwargs["flows"] = flows
        elif type is SecuritySchemeType.OPEN_ID_CONNECT:
            kwargs["openIdConnectUrl"] = openid_connect_url

        self.add_component(
            "securitySchemes",
            ident,
            SecurityScheme(**kwargs),
        )  # type: ignore

    def raw(self, data):
        if "info" in data:
            self.describe(
                data["info"].get("title"),
                data["info"].get("version"),
                data["info"].get("description"),
                data["info"].get("terms"),
            )

        if "servers" in data:
            for server in data["servers"]:
                self._servers.append(Server(**server))

        if "paths" in data:
            self._paths.update(data["paths"])

        if "components" in data:
            for location, component in data["components"].items():
                self._components[location].update(component)

        if "security" in data:
            for security in data["security"]:
                if not security:
                    self.secured()
                else:
                    for key, value in security.items():
                        self.secured(key, value)

        if "tags" in data:
            for tag in data["tags"]:
                self.tag(**tag)

        if "externalDocs" in data:
            self.external(**data["externalDocs"])

    def build(self, app: Sanic) -> OpenAPI:
        info = self._build_info()
        paths = self._build_paths(app)
        tags = self._build_tags()
        security = self._build_security()

        url_servers = getattr(self, "_urls", None)
        servers = self._servers
        existing = [
            server.fields["url"].strip("/") for server in self._servers
        ]
        if url_servers is not None:
            for url_server in url_servers:
                if url_server.strip("/") not in existing:
                    servers.append(Server(url=url_server))

        components = (
            Components(**self._components) if self._components else None
        )

        return OpenAPI(
            info,
            paths,
            tags=tags,
            servers=servers,
            security=security,
            components=components,
            externalDocs=self._external,
        )

    def _build_info(self) -> Info:
        kwargs = remove_nulls(
            {
                "description": self._description,
                "termsOfService": self._terms,
                "license": self._license,
                "contact": self._contact,
            },
            deep=False,
        )

        return Info(self._title, self._version, **kwargs)

    def _build_tags(self):
        return [self._tags[k] for k in self._tags]

    def _build_paths(self, app: Sanic) -> Dict:
        paths = {}

        for path, operations in self._paths.items():
            paths[path] = PathItem(
                **{
                    k: v if isinstance(v, dict) else v.build()
                    for k, v in operations.items()
                    if isinstance(v, dict) or v._app is app
                }
            )

        return paths

    def _build_security(self):
        return [
            {sec.fields["name"]: sec.fields["value"]}
            if sec.fields["name"] is not None
            else {}
            for sec in self.security
        ]
