import abc
import dataclasses
import logging
import typing
import urllib.parse
from types import UnionType
from typing import Any, Optional, Dict, Union

import aiohttp.web
import aiohttp.abc
import init_helpers
from dynamic_types.class_name import prepare_class_name
from dynamic_types.create import create_type, create_dataclass
from init_helpers import custom_dumps, is_instance, raise_if
from init_helpers import dict_to_dataclass, convert_to_type
from yarl import URL

from .charset import Charset
from .http_status_codes import HttpStatusCode
from .mime_types import ContentType

logger = logging.getLogger(__name__)


class Nothing:
    pass


class ErrorCode(int):
    pass


_FIXED_CONTENT_TYPE__NAME = "_fixed_content_type"
_FIXED_STATUS_CODE__NAME = "_fixed_status_code"
_FIXED_PAYLOAD_TYPE__NAME = "_fixed_payload_type"
_FIXED_ERROR_CODE__NAME = "_fixed_error_code_type"


class BaseAnswer(abc.ABC):
    # noinspection PyUnusedLocal
    @abc.abstractmethod
    def __init__(self,
                 payload: Union[str, bytes, None],
                 status: HttpStatusCode = None,
                 content_type: Optional[Union[ContentType, str]] = None,
                 headers: Optional[Dict[str, str]] = None,
                 charset: Charset = None,
                 ):
        raise NotImplementedError

    def __class_getitem__(cls, modifier: Union[HttpStatusCode, ContentType]) -> type:
        field_name_to_value = {}
        if isinstance(modifier, HttpStatusCode):
            if cls.get_class_status_code():
                raise TypeError("Status code already fixed")
            field_name_to_value[_FIXED_STATUS_CODE__NAME] = modifier
        elif isinstance(modifier, ContentType):
            if cls.get_class_content_type():
                raise TypeError("Content type already fixed")
            field_name_to_value[_FIXED_CONTENT_TYPE__NAME] = modifier
        else:
            raise TypeError(f"Unexpected modifier type: {type(modifier)}")

        new_type = create_type(prepare_class_name([cls], modifier), [cls], field_name_to_value)
        return new_type

    @classmethod
    def get_class_content_type(cls) -> Optional[ContentType]:
        if hasattr(cls, _FIXED_CONTENT_TYPE__NAME):
            return getattr(cls, _FIXED_CONTENT_TYPE__NAME)

    @classmethod
    def get_class_status_code(cls) -> Optional[HttpStatusCode]:
        if hasattr(cls, _FIXED_STATUS_CODE__NAME):
            return getattr(cls, _FIXED_STATUS_CODE__NAME)

    @classmethod
    def get_class_payload_type(cls) -> type:
        return bytes


class StreamAnswer(BaseAnswer, aiohttp.web.StreamResponse):
    def __init__(self,
                 payload: str | bytes | None,
                 status: HttpStatusCode = None,
                 content_type: ContentType | str | None = None,
                 headers: dict[str, str] | None = None,
                 charset: Charset = None,
                 ):
        if status is None and hasattr(self, _FIXED_STATUS_CODE__NAME):
            status = getattr(self, _FIXED_STATUS_CODE__NAME)
        headers = headers or {}

        status_code = status or self.get_class_status_code() or 200
        content_type = content_type or self.get_class_content_type()
        content_type = content_type.value if isinstance(content_type, ContentType) else content_type

        if isinstance(payload, str):
            content_type = content_type or "text/plain"
            charset = charset or "utf-8"
            payload = payload.encode(charset)

        raise_if(
            payload is not None and not isinstance(payload, bytes),
            TypeError(f'Payload must be a str | bytes | None, got {type(payload)}'))

        if content_type is not None:
            content_type_header_value = content_type + ('' if charset is None else f'; charset={charset}')
            headers[aiohttp.hdrs.CONTENT_TYPE] = content_type_header_value
        aiohttp.web.StreamResponse.__init__(self, status=status_code, headers=headers)
        self.initial_data: bytes | None = payload

    async def prepare(self, request: aiohttp.web.BaseRequest) -> Optional[aiohttp.abc.AbstractStreamWriter]:
        result = await super().prepare(request)
        await self._write_initial_data()
        return result

    async def _write_initial_data(self) -> None:
        if self.initial_data is not None:
            initial_data, self.initial_data = self.initial_data, None
            await aiohttp.web.StreamResponse.write(self, initial_data)

    async def write(self, data: bytes) -> None:
        await self._write_initial_data()
        await aiohttp.web.StreamResponse.write(self, data)

    def __class_getitem__(cls, modifier: Union[HttpStatusCode, ContentType, type, UnionType]) -> type:
        return super().__class_getitem__(modifier)


class Answer(BaseAnswer, aiohttp.web.Response):
    def __init__(self,
                 payload: Union[str, bytes, None],
                 status: HttpStatusCode = None,
                 content_type: Optional[Union[ContentType, str]] = None,
                 headers: Optional[Dict[str, str]] = None,
                 charset: Charset = None,
                 ):
        if status is None and hasattr(self, _FIXED_STATUS_CODE__NAME):
            status = getattr(self, _FIXED_STATUS_CODE__NAME)

        status_code = status or self.get_class_status_code() or 200
        content_type = content_type or self.get_class_content_type()
        content_type = content_type.value if isinstance(content_type, ContentType) else content_type
        aiohttp.web.Response.__init__(
            self, body=payload, status=status_code, content_type=content_type, headers=headers, charset=charset)

    @property
    def bytes_body(self) -> bytes:
        # noinspection PyProtectedMember
        return self.body._value if isinstance(self.body, aiohttp.Payload) else self.body

    def __class_getitem__(cls, modifier: Union[HttpStatusCode, ContentType, type, UnionType]) -> type:
        return super().__class_getitem__(modifier)


class JsonableAnswer(Answer):
    """
    Class accepts dict, list, set, str, int, float, bool, None, dataclass instances
    """
    def __init__(self,
                 payload: Any,
                 status: HttpStatusCode = None,
                 headers: Optional[Dict[str, str]] = None,
                 charset: Charset = Charset.UTF8
                 ):
        try:
            payload = self.convert_payload(payload)
            payload = custom_dumps(payload)
        except TypeError as er:
            error_msg = f"Failed to serialise data: '{payload}' to json, problem: {er}"
            logger.exception(error_msg)
            payload = custom_dumps(error_msg)
            status = HttpStatusCode.InternalServerError

        super().__init__(payload=payload, status=status, content_type=self.get_class_content_type(),
                         headers=headers, charset=charset)

    def convert_payload(self, payload):
        if payload_type := self.get_class_payload_type():
            payload_type, _ = init_helpers.try_extract_type_notes(payload_type)
            if is_instance(payload, payload_type):
                return payload

            if dataclasses.is_dataclass(payload_type):
                # noinspection PyTypeChecker
                payload = dict_to_dataclass(payload, payload_type)
            else:
                # noinspection PyTypeChecker
                payload = convert_to_type(payload_type, payload)

        return payload

    @classmethod
    def get_class_content_type(cls) -> ContentType:
        return ContentType.Json

    @classmethod
    def get_class_payload_type(cls) -> type:
        if hasattr(cls, _FIXED_PAYLOAD_TYPE__NAME):
            return getattr(cls, _FIXED_PAYLOAD_TYPE__NAME)

    def __class_getitem__(cls, modifier: Union[HttpStatusCode, ContentType, type, UnionType]) -> type:
        if typing.get_origin(modifier) is not None or isinstance(modifier, type | UnionType):
            if cls.get_class_payload_type():
                raise TypeError("Payload type already fixed")
            return create_type(prepare_class_name([cls], modifier), [cls], {_FIXED_PAYLOAD_TYPE__NAME: modifier})
        return super().__class_getitem__(modifier)


class WrappedAnswerBody:
    pass


class WrappedAnswer(JsonableAnswer):
    def __init__(self, payload: Any = Nothing, status: HttpStatusCode = HttpStatusCode.OK,
                 headers: Optional[Dict[str, str]] = None, charset: Charset = Charset.UTF8) -> None:
        payload = {"done": True, "result": payload}
        super().__init__(payload, status, headers, charset)

    @classmethod
    def _wrap_type(cls, type_or_annotated: type | typing.Annotated | None) -> type:
        type_, notes = init_helpers.try_extract_type_notes(type_or_annotated)
        attribute_key_to_type = {"done": bool}
        if type_ not in (None, type[None], type(None)):
            attribute_key_to_type["result"] = type_or_annotated
        new_type_name = prepare_class_name([type_or_annotated],  "Wrapped")
        return create_dataclass(new_type_name, [WrappedAnswerBody], attribute_key_to_type)

    def __class_getitem__(cls, modifier: Union[HttpStatusCode, ContentType, type, UnionType, type[None]]) -> type:
        if modifier is None:
            raise TypeError('None is not valid modifier. Use type[None] or type(None) to specify no result in body')
        if typing.get_origin(modifier) is not None or isinstance(modifier, type | UnionType):
            if cls.get_class_payload_type():
                raise TypeError("Payload type already fixed")
            wrapped_type = cls._wrap_type(modifier)
            return create_type(prepare_class_name([cls], modifier), [cls], {_FIXED_PAYLOAD_TYPE__NAME: wrapped_type})
        return super().__class_getitem__(modifier)

    @staticmethod
    def _wrap_payload(payload: Any) -> Dict[str, Any]:
        return {"done": True, "result": payload}


@dataclasses.dataclass
class ErrorDescription:
    error: str
    error_type: Optional[str] = None
    error_code: Optional[ErrorCode] = None
    done: bool = False


class ErrorAnswer(JsonableAnswer):
    def __init__(self,
                 error: Any = Nothing,
                 status: HttpStatusCode = HttpStatusCode.InternalServerError,
                 error_type: Optional[str] = None,
                 error_code: Optional[ErrorCode] = None,
                 headers: Optional[Dict[str, str]] = None,
                 charset: Charset = Charset.UTF8) -> None:
        error_code = error_code or self.get_class_error_code()
        super().__init__(
            payload=ErrorDescription(error, error_type, error_code),
            status=status, headers=headers, charset=charset)

    def __class_getitem__(cls, modifier: Union[HttpStatusCode, ContentType, ErrorCode, type, None]) -> type:
        if isinstance(modifier, ErrorCode):
            if cls.get_class_error_code():
                raise TypeError("ErrorCode already fixed")
            return create_type(prepare_class_name([cls], modifier), [cls], {_FIXED_ERROR_CODE__NAME: modifier})
        return super().__class_getitem__(modifier)

    @classmethod
    def get_class_payload_type(cls) -> type:
        return ErrorDescription

    @classmethod
    def get_class_error_code(cls) -> Optional[ErrorCode]:
        if hasattr(cls, _FIXED_ERROR_CODE__NAME):
            return getattr(cls, _FIXED_ERROR_CODE__NAME)


class ExceptionAnswer(ErrorAnswer):
    def __init__(self,
                 exception: Exception,
                 status: Optional[HttpStatusCode] = None,
                 error_code: Optional[ErrorCode] = None,
                 headers: Optional[Dict[str, str]] = None,
                 charset: Charset = Charset.UTF8) -> None:
        super().__init__(
            error=str(exception), status=status, error_type=type(exception).__name__, error_code=error_code,
            headers=headers, charset=charset)


class RedirectAnswer(Answer):
    def __init__(self, location: str, headers: Optional[Dict[str, str]] = None):
        headers = {} if headers is None else headers
        headers['Location'] = str(URL(location))
        Answer.__init__(self, None, HttpStatusCode.Found, headers=headers)

    @property
    def location(self):
        return self.headers['Location']

    @classmethod
    def get_class_status_code(cls) -> HttpStatusCode:
        return HttpStatusCode.Found


class FileAnswer(Answer):
    def __init__(self,
                 payload: Any,
                 file_name: str = None,
                 status: HttpStatusCode = HttpStatusCode.OK,
                 headers: dict[str, str] | None = None,
                 content_type: ContentType | str | None = None,
                 charset: str = None
                 ) -> None:
        if headers is None:
            headers = {}

        if file_name:
            file_name = urllib.parse.quote_plus(file_name)
            headers["Content-Disposition"] = f"attachment; filename=\"{file_name}\""

        content_type = content_type or self.get_class_content_type()
        if charset is None and content_type is not None and content_type.startswith("text/"):
            charset = Charset.UTF8

        super().__init__(payload, status, content_type, headers, charset=charset)
