#  Copyright (C) 2025
#  ABM, Moscow
#
#  UNPUBLISHED PROPRIETARY MATERIAL.
#  ALL RIGHTS RESERVED.
#
#  Authors: Mike Orlov <m.orlov@abm-jsc.ru>
import abc
import functools
from dataclasses import dataclass
from types import UnionType
from typing import Any, Annotated, Mapping, get_args, ClassVar, Iterable, TypeVar

from frozendict import frozendict
from http_tools import IncomingRequest, ContentType
from init_helpers import append_if, Jsonable
from init_helpers import NoValue, convert_to_type

from .call_parameter import CallParameter
from .parameter_location import ParameterLocation
from .parameter_style import ParameterStyle
from ..example import BaseExample, get_examples_as_dict
from ..schema.base_schema import BaseSchema
from ..spec_ref import SpecRef
from ..spec_resource import SpecResource
from ...freeze import freeze


T = TypeVar('T')


@dataclass(frozen=True, slots=True, kw_only=True, repr=False)
class SpecParameter(CallParameter, SpecResource, abc.ABC):
    """Параметр, входящий в спецификацию, location - расположение(хедер/query/path/т.п), name - имя в расположении"""
    # schema_builder: ClassVar[Callable[[AnyType], BaseSchema]] = SchemaBuilder.build_schema
    location: ClassVar[ParameterLocation]
    name: str
    is_optional: bool = False
    default: Any = NoValue
    description: str = ''

    examples: Iterable[BaseExample] = tuple()

    explode: bool | None = None
    style: ParameterStyle | None = None

    content_type: ContentType | None = None

    def __post_init__(self):
        if self.default is not NoValue:
            object.__setattr__(self, 'default', freeze(self.default))
        object.__setattr__(self, 'examples', tuple(self.examples))
        if self.content_type is not None and (self.explode is not None or self.style is not None):
            raise ValueError(f"{self} HAS {self.content_type=}, so it MUST have style=None an explode=None")

    @property
    def _schema(self) -> BaseSchema:
        return self.schema
        # return self.schema if isinstance(self.schema, BaseSchema) else self.schema_builder(self.schema)

    def get_spec_dependencies(self) -> frozenset[SpecResource]:
        result: set[SpecResource] = {self._schema}
        result = result.union(self.examples)
        return frozenset(result)

    def get_spec_dict(self, dependency_to_ref: Mapping['SpecResource', SpecRef]) -> frozendict[str, Jsonable]:
        result = {'name': self.name, 'in': self.location}
        result |= {'description': self.description} if self.description else {}

        rendered_schema = dependency_to_ref[self._schema]
        if self.content_type:
            if self.explode is not None or self.style is not None:
                raise ValueError(f"{self} HAS {self.content_type=}, so it MUST have style=None an explode=None")
            result['content'] = {self.content_type: rendered_schema}
        else:
            result['schema'] = rendered_schema

        result |= {'style': self.style} if self.style else {}
        result |= {'required': self.is_required} if self.is_required else {}
        result |= {'default': self.default} if self.default is not NoValue else {}
        result |= get_examples_as_dict(self.examples, dependency_to_ref)
        return frozendict(result)

    @property
    def annotated_schema(self) -> type | UnionType | Annotated:
        notes = []
        append_if(self.description is not NoValue, notes, self.description)
        append_if(self.examples is not NoValue, notes, *self.examples if self.examples is not NoValue else [])
        return Annotated[self.schema, *notes] if notes else self.schema

    @property
    def is_required(self) -> bool:
        return self.default is NoValue and not self.is_optional and self.location != ParameterLocation.path

    def get_spec_parameters(self) -> frozendict['SpecParameter', None]:
        return frozendict({self: None})

    def _cast_value_to_schema(self, value: Any):
        if self.explode:
            type_args = get_args(self.schema)
            if len(type_args) != 1:
                raise TypeError(f'Only generic containers with single argument (list, set, simple tuple) are supported')
            type_arg = type_args[0]
            return [self._cast_single_value(value=val, schema=type_arg) for val in value]
        return self._cast_single_value(value=value, schema=self.schema)

    def _cast_single_value(self, value: Any, schema: type[T]) -> T:
        if self.style is not None and isinstance(value, str):
            value = self._apply_style(value)
        return convert_to_type(value=value, type_=schema)

    async def get(self, incoming_request: IncomingRequest, security_kwargs: dict[str, Any]) -> Any | NoValue:
        try:
            result = self._get(incoming_request)
        except KeyError:
            if self.default is NoValue:
                if self.is_optional:
                    return NoValue
                raise
            result = self.default
        return self._cast_value_to_schema(result)

    @abc.abstractmethod
    def _get(self, incoming_request: IncomingRequest) -> Any:
        pass

    def _apply_style(self, value: str) -> Any:
        if self.style == ParameterStyle.simple:
            assert isinstance(value, str), "Expected string value, got:"
            return value.split(",")
        return value

    @functools.cache
    def _get_repr_parts(self) -> tuple[str, ...]:
        parts = list(CallParameter._get_repr_parts(self))
        parts.append(f'name={self.name!r}')
        parts += [f'is_optional={self.is_optional!r}'] if self.is_optional else []
        parts += [f'default={self.default!r}'] if self.default is not NoValue else []
        parts += [f'description={self.description!r}'] if self.description else []
        parts += [f'examples={self.examples!r}'] if self.examples else []
        parts += [f'explode={self.explode!r}'] if self.explode is not None else []
        parts += [f'style={self.style!r}'] if self.style is not None else []
        parts += [f'content_type={self.content_type!r}'] if self.content_type is not None else []
        return tuple(parts)

    def __repr__(self):
        return f'{self.__class__.__name__}({", ".join(self._get_repr_parts())})'

    __str__ = __repr__
