import inspect
import string
from inspect import Signature
from typing import Callable, Literal, Annotated, Iterable

from http_tools import Answer, WrappedAnswer, HttpStatusCode
from http_tools.answer import ExceptionAnswer, JsonableAnswer
from init_helpers import raise_if, try_extract_type_notes

from ..spec import Tag
from ..spec import Example, SecurityRequirement, get_securities_arg_names, SecurityScheme
from ..spec import SpecParameter, QueryParameter, PathParameter, JsonBodyParameter, CallParameter, RawBodyParameter
from .endpoint import AnswerType, ResultType, Endpoint


def gen_query_endpoint(
        func: Callable,
        method: str, path: str = '', securities: Iterable[SecurityRequirement | SecurityScheme] = tuple(),
        exception_type_to_answer_type: dict[type[Exception], type[Answer]] | None = None,
        unhandled_exception__answer_type: type[Answer] | None = ExceptionAnswer[HttpStatusCode.InternalServerError],
        arg_name_to_param: dict[str, CallParameter | type[RawBodyParameter] | None] | None = None,
        answer_type: type[AnswerType] = WrappedAnswer[HttpStatusCode.OK],
        answer_factory: Callable[[ResultType], AnswerType] | None = None,
        summary: str | None = None,
        description: str | None = None,
        tags: list[str | Tag] | None = None,
        deprecated: bool = False,
        operation_id: str = '',
        ignore_extra_params: bool = False,
        additional_parameters: tuple[SpecParameter, ...] | None = None,  # add to spec, no other processing
) -> Endpoint:
    """
    Generate Endpoint based on function signature:
    - Return type will be used to generated answer type.
    - All missing arguments will be treated as QueryParameters
    """
    return _gen_endpoint(
        param_type=QueryParameter, func=func, method=method, path=path, securities=securities,
        exception_type_to_answer_type=exception_type_to_answer_type,
        unhandled_exception__answer_type=unhandled_exception__answer_type,
        arg_name_to_param=arg_name_to_param, answer_type=answer_type, answer_factory=answer_factory,
        summary=summary, description=description, tags=tags, deprecated=deprecated, operation_id=operation_id,
        ignore_extra_params=ignore_extra_params, additional_parameters=additional_parameters,
    )


def gen_json_endpoint(
        func: Callable,
        method: str, path: str = '', securities: Iterable[SecurityRequirement | SecurityScheme] = tuple(),
        exception_type_to_answer_type: dict[type[Exception], type[Answer]] | None = None,
        unhandled_exception__answer_type: type[Answer] | None = ExceptionAnswer[HttpStatusCode.InternalServerError],
        arg_name_to_param: dict[str, CallParameter | type[RawBodyParameter] | None] | None = None,
        answer_type: type[AnswerType] = WrappedAnswer[HttpStatusCode.OK],
        answer_factory: Callable[[ResultType], AnswerType] | None = None,
        summary: str | None = None,
        description: str | None = None,
        tags: list[str | Tag] | None = None,
        deprecated: bool = False,
        operation_id: str = '',
        ignore_extra_params: bool = False,
        additional_parameters: tuple[SpecParameter, ...] | None = None,  # add to spec, no other processing
) -> Endpoint:
    """
    Generate RPC endpoint based on function signature:
    - Return type will be used to generated answer type.
    - All missing arguments will be treated as JsonBodyParameter
    """
    return _gen_endpoint(
        param_type=JsonBodyParameter, func=func, method=method, path=path, securities=securities,
        exception_type_to_answer_type=exception_type_to_answer_type,
        unhandled_exception__answer_type=unhandled_exception__answer_type,
        arg_name_to_param=arg_name_to_param, answer_type=answer_type, answer_factory=answer_factory,
        summary=summary, description=description, tags=tags, deprecated=deprecated, operation_id=operation_id,
        ignore_extra_params=ignore_extra_params, additional_parameters=additional_parameters,
    )


def _get_path_param_names(path: str) -> set[str]:
    return {i[1] for i in string.Formatter().parse(path) if i[1] is not None}


def _gen_endpoint(
        param_type: type[SpecParameter], func: Callable[..., ResultType],
        method: str, path: str = '', securities: Iterable[SecurityRequirement | SecurityScheme] = tuple(),
        exception_type_to_answer_type: dict[type[Exception], type[Answer]] | None = None,
        unhandled_exception__answer_type: type[Answer] | None = ExceptionAnswer[HttpStatusCode.InternalServerError],
        arg_name_to_param: dict[str, CallParameter | type[RawBodyParameter]] | None = None,
        answer_type: type[AnswerType] = WrappedAnswer[HttpStatusCode.OK],
        answer_factory: Callable[[ResultType], AnswerType] | None = None,
        summary: str | None = None,
        description: str | None = None,
        tags: list[str | Tag] | None = None,
        deprecated: bool = False,
        operation_id: str = '',
        ignore_extra_params: bool = False,
        additional_parameters: tuple[SpecParameter, ...] | None = None,  # add to spec, no other processing
) -> Endpoint:
    path_param_names = _get_path_param_names(path)
    security_param_names = get_securities_arg_names(securities)
    signature = Signature.from_callable(func)
    arg_name_to_param = arg_name_to_param or {}
    result_arg_name_to_param = {}
    for arg in signature.parameters.values():
        if arg.annotation == Signature.empty:
            raise TypeError(f"undefined type for arg: {arg}")

        # дописать description и примеры?
        if arg.name in arg_name_to_param:
            if arg_name_to_param[arg.name] is not None:
                result_arg_name_to_param[arg.name] = arg_name_to_param[arg.name]
                # if
            continue

        schema = arg.annotation
        schema, notes = try_extract_type_notes(schema)
        if arg.kind == inspect.Parameter.VAR_POSITIONAL:
            schema = list[schema]
        elif arg.kind == inspect.Parameter.VAR_KEYWORD:
            schema = dict[str, schema]

        parameter_kwargs = {"name": arg.name, "schema": schema}
        if examples := tuple(note for note in notes if isinstance(note, Example)):
            parameter_kwargs["examples"] = examples

        if descriptions := [note for note in notes if isinstance(note, str)]:
            parameter_kwargs["description"] = descriptions[0]

        # description: str = ''
        # examples: tuple[BaseExample, ...] = field(default_factory=tuple)
        if arg.default != Signature.empty:
            parameter_kwargs["default"] = arg.default
        if arg.name in path_param_names:
            param = PathParameter(**parameter_kwargs)
        elif arg.name in security_param_names:
            continue
        else:
            # noinspection PyArgumentList
            param = param_type(**parameter_kwargs)
        result_arg_name_to_param[arg.name] = param

    if not ignore_extra_params:
        if extra_params := {k for k, v in arg_name_to_param.items() if v is not None} - result_arg_name_to_param.keys():
            raise KeyError(f"arg_name_to_param has {extra_params=}, use ignore_extra_params=True to skip those")

    if signature.return_annotation == Signature.empty:
        raise TypeError("undefined return type")

    answer_type, notes = try_extract_type_notes(answer_type)
    if issubclass(answer_type, JsonableAnswer) and not isinstance(answer_type.get_class_payload_type(), type):
        return_type = signature.return_annotation
        return_type = type[None] if return_type is None else return_type
        answer_type = answer_type[return_type]
    if notes:
        answer_type = Annotated[answer_type, *notes]

    raise_if(answer_type.get_class_status_code() is None,
             TypeError(f'Answer type MUST have status code, got {answer_type}'))

    # raise_if(answer_type.get_class_content_type() is None,
    #          TypeError(f'Answer type MUST have content type, got {answer_type}'))
    return Endpoint(
        func=func, method=method, path=path, securities=securities, arg_name_to_param=result_arg_name_to_param,
        answer_type=answer_type,
        exception_type_to_answer_type=exception_type_to_answer_type,
        unhandled_exception__answer_type=unhandled_exception__answer_type,
        answer_factory=answer_factory,
        summary=summary, description=description, tags=tags, deprecated=deprecated, operation_id=operation_id,
        additional_parameters=additional_parameters
    )
