#  Copyright (C) 2025
#  ABM, Moscow
#
#  UNPUBLISHED PROPRIETARY MATERIAL.
#  ALL RIGHTS RESERVED.
#
#  Authors: Mike Orlov <m.orlov@abm-jsc.ru>
from dataclasses import dataclass, field
from typing import Literal, Iterable, Mapping, ClassVar, Container

from frozendict import frozendict
from init_helpers import Jsonable

from .response import Response
from .security import SecurityRequirement
from .parameter import SpecParameter
from .request_body import RequestBody, SpecRef, SpecResource
from .tag import Tag
from .referencable_resource import ReferencableResource


@dataclass(frozen=True, kw_only=True, slots=True)
class Operation(ReferencableResource):
    """Operation https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#operation-object"""
    supported_methods: ClassVar[Container[str]] = ('get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace')

    path: str
    method: Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE',
                    'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']   # HTTP method
    operation_id: str  # MUST be unique among all operations in spec
    securities: Iterable[SecurityRequirement] = tuple()  # empty iterable means "public"(no limitations)
    parameters: Iterable[SpecParameter] = tuple()
    request_body: RequestBody | None = None
    # code_to_answer: Mapping[int | Literal['default'], type[Answer]] = field(default_factory=frozendict)
    code_to_response: Mapping[int | Literal['default'], Response] = field(default_factory=frozendict)
    summary: str = ''
    description: str = ''
    tags: Iterable[str | Tag] = tuple()
    deprecated: bool = False

    def __post_init__(self) -> None:
        if self.method.lower() not in self.supported_methods:
            raise ValueError(f'Unsupported method {self.method.lower()}, supported methods: {self.supported_methods}')
        object.__setattr__(self, 'securities', tuple(self.securities))
        param_full_name_to_param = {}
        for p in self.parameters:
            key = f'{p.location}.{p.name}'
            if present_p := param_full_name_to_param.get(key):
                raise KeyError(f'Parameter "{p}" has same {p.location.value=} and {p.name=} as "{present_p}')
            param_full_name_to_param[key] = p

        object.__setattr__(self, 'parameters', tuple(self.parameters))
        object.__setattr__(self, 'code_to_response', frozendict(self.code_to_response))
        object.__setattr__(self, 'tags', tuple(self.tags))

    def get_key(self) -> str:
        return self.method.lower()

    @property
    def spec_path(self) -> tuple[str, ...]:
        return 'paths', self.path

    def get_spec_dependencies(self) -> frozenset['SpecResource']:
        result: set[SpecResource] = set(self.securities) | set(self.parameters)
        result |= {t for t in self.tags if isinstance(t, Tag)}
        result.update(self.code_to_response.values())
        if self.request_body:
            result.add(self.request_body)
        return frozenset(result)

    def get_spec_dict(self, dependency_to_ref: Mapping['SpecResource', SpecRef]) -> frozendict[str, Jsonable]:
        result: dict[str, Jsonable] = {'operationId': self.operation_id}
        result |= {
            'tags': tuple(dependency_to_ref[t] if isinstance(t, Tag) else t for t in self.tags)} if self.tags else {}
        result |= {'summary': self.summary} if self.summary else {}
        result |= {'description': self.description} if self.description else {}
        result |= {'deprecated': self.deprecated} if self.deprecated else {}
        # result |= {'externalDocs': ...}  # TODO: implement external docs
        result |= {'security': tuple(dependency_to_ref[s] for s in self.securities)} if self.securities else {}
        result |= {'parameters': tuple(dependency_to_ref[p] for p in self.parameters)} if self.parameters else {}
        result |= {'requestBody': dependency_to_ref[self.request_body]} if self.request_body else {}
        if code_to_response := self.code_to_response:
            result['responses'] = {code: dependency_to_ref[response] for code, response in code_to_response.items()}
        # result |= {'callbacks': ...}  # TODO: implement callbacks
        # result |= {'servers': ...}  # TODO: implement servers
        return frozendict(result)

    def __repr__(self):
        parts = [f'path={self.path!r}', f'method={self.method!r}', f'operation_id={self.operation_id!r}']
        parts += [f'securities={self.securities!r}'] if self.securities else []
        parts += [f'parameters={list(self.parameters)!r}'] if self.parameters else []
        parts += [f'request_body={self.request_body!r}'] if self.request_body is not None else []
        if self.code_to_response:
            parts += [f'code_to_response={ {str(code): resp for code, resp in self.code_to_response.items() } }']
        parts += [f'summary={self.summary!r}'] if self.summary else []
        parts += [f'description={self.description!r}'] if self.description else []
        parts += [f'tags={self.tags!r}'] if self.tags else []
        parts += [f'deprecated={self.deprecated!r}'] if self.deprecated else []
        return f'{self.__class__.__name__}({", ".join(parts)})'

    __str__ = __repr__
