# coding:utf-8

import hashlib
# import json
import os
import sys
import traceback
import typing as t
from email.utils import formatdate
from functools import partial
from mimetypes import guess_type as mimetypes_guess_type
from urllib.parse import quote

import anyio

from makit.lib import json
from makit.web.headers import Headers
from makit.web.types import Scope, Receive, Send
from makit.web.utils.concurrency import iterate_in_threadpool


class Response:
    media_type = None
    charset = 'utf-8'

    def __init__(self, content=None, status_code=200, headers=None, media_type=None):
        self.status_code = status_code
        self.media_type = media_type or self.__class__.media_type
        self.body = self.render(content)
        self.headers = headers or Headers(charset=self.charset)

    def render(self, content: t.Any) -> bytes:
        if content is None:
            return b""
        if isinstance(content, bytes):
            return content
        if hasattr(content, 'encode'):
            return content.encode(self.charset)
        return content.encode(self.charset)

    def build_headers(self, scope=None):
        headers = Headers(scope=scope)
        headers.charset = self.charset
        headers.update(**self.headers)
        if not (self.status_code < 200 or self.status_code in (204, 304)):
            content_length = str(len(self.body))
            headers['content-length'] = content_length
        content_type = self.media_type
        if content_type is not None:
            if content_type.startswith("text/"):
                content_type += "; charset=" + self.charset
            headers['content-type'] = content_type
        origin = headers.get('Origin')
        headers['Access-Control-Allow-Origin'] = origin
        return headers

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        try:
            headers = self.build_headers(scope)
            await send(
                {
                    "type": "http.response.start",
                    "status": self.status_code,
                    "headers": headers.raw_data,
                }
            )
            await send({"type": "http.response.body", "body": self.body})
        except:
            print(traceback.format_exc())


class PlainTextResponse(Response):
    media_type = "text/plain"


class HtmlResponse(Response):
    media_type = "text/html"


class JsonResponse(Response):
    media_type = "application/json"

    def __init__(
            self,
            content: t.Any,
            status_code: int = 200,
            headers: t.Optional[t.Dict[str, str]] = None
    ):
        super().__init__(content, status_code, headers, None)

    def render(self, content: t.Any) -> bytes:
        serialized = json.dumps(content).encode('utf-8')
        return serialized


class FileResponse(Response):
    chunk_size = 64 * 1024

    def __init__(
            self,
            path: t.Union[str, "os.PathLike[str]"],
            status_code: int = 200,
            headers: t.Optional[t.Mapping[str, str]] = None,
            media_type: t.Optional[str] = None,
            filename: t.Optional[str] = None,
            stat_result: t.Optional[os.stat_result] = None,
            method: t.Optional[str] = None,
            content_disposition_type: str = "attachment"
    ):
        super().__init__(None, status_code, headers, media_type)
        self.path = path
        self.status_code = status_code
        self.filename = filename
        self.send_header_only = method is not None and method.upper() == "HEAD"
        if media_type is None:
            media_type = guess_type(filename or path)[0]
            media_type = media_type or "application/octet-stream"
        self.media_type = media_type
        if self.filename is not None:
            content_disposition_filename = quote(self.filename)
            if content_disposition_filename != self.filename:
                content_disposition = "{}; filename*=utf-8''{}".format(
                    content_disposition_type, content_disposition_filename
                )
            else:
                content_disposition = '{};filename="{}"'.format(
                    content_disposition_type, self.filename
                )
            self.headers["content-disposition"] = content_disposition
        self.stat_result = stat_result
        if stat_result is not None:
            self.set_stat_headers(stat_result)

    def set_stat_headers(self, stat_result: os.stat_result) -> None:
        content_length = str(stat_result.st_size)
        last_modified = formatdate(stat_result.st_mtime, usegmt=True)
        etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size)
        etag = md5_hexdigest(etag_base.encode(), usedforsecurity=False)

        self.headers["content-length"] = content_length
        self.headers["last-modified"] = last_modified
        self.headers["etag"] = etag

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        origin = Headers(scope=scope)['origin']
        headers = self.build_headers()
        headers['Access-Control-Allow-Origin'] = origin
        headers.remove('token')
        if self.stat_result is None:
            try:
                stat_result = await anyio.to_thread.run_sync(os.stat, self.path)
                content_length = str(stat_result.st_size)
                last_modified = formatdate(stat_result.st_mtime, usegmt=True)
                etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size)
                etag = md5_hexdigest(etag_base.encode(), usedforsecurity=False)

                headers["content-length"] = content_length
                headers["last-modified"] = last_modified
                headers["etag"] = etag
            except FileNotFoundError:
                raise RuntimeError(f"File at path {self.path} does not exist.")
            else:
                mode = stat_result.st_mode
                if not os.stat.S_ISREG(mode):
                    raise RuntimeError(f"File at path {self.path} is not a file.")
        await send(
            {
                "type": "http.response.start",
                "status": self.status_code,
                "headers": headers.raw_data,
            }
        )
        if self.send_header_only:
            await send({"type": "http.response.body", "body": b"", "more_body": False})
        else:
            async with await anyio.open_file(self.path, mode="rb") as file:
                more_body = True
                size = 0
                while more_body:
                    chunk = await file.read(self.chunk_size)
                    size += len(chunk)
                    more_body = len(chunk) == self.chunk_size
                    await send(
                        {
                            "type": "http.response.body",
                            "body": chunk,
                            "more_body": more_body,
                        }
                    )


class RedirectResponse(Response):
    def __init__(
            self,
            url: str,
            status_code: int = 307,
            headers: t.Optional[t.Mapping[str, str]] = None
    ):
        super().__init__(
            content=b"", status_code=status_code, headers=headers
        )
        self.headers["location"] = quote(str(url), safe=":/%#?=@[]!$&'()*+,;")


Content = t.Union[str, bytes]
SyncContentStream = t.Iterator[Content]
AsyncContentStream = t.AsyncIterable[Content]
ContentStream = t.Union[AsyncContentStream, SyncContentStream]


class StreamingResponse(Response):
    body_iterator: AsyncContentStream

    def __init__(
            self, content: ContentStream,
            status_code: int = 200,
            headers: t.Optional[t.Mapping[str, str]] = None,
            media_type: t.Optional[str] = None
    ):
        super().__init__(None, status_code, headers, media_type)
        if isinstance(content, t.AsyncIterable):
            self.body_iterator = content
        else:
            self.body_iterator = iterate_in_threadpool(content)
        self.status_code = status_code
        self.media_type = self.media_type if media_type is None else media_type

    async def listen_for_disconnect(self, receive: Receive) -> None:
        while True:
            message = await receive()
            if message["type"] == "http.disconnect":
                break

    async def stream_response(self, scope: Scope, send: Send) -> None:
        headers = self.build_headers(scope)
        await send(
            {
                "type": "http.response.start",
                "status": self.status_code,
                "headers": headers.raw_data,
            }
        )
        async for chunk in self.body_iterator:
            if not isinstance(chunk, bytes):
                chunk = chunk.encode(self.charset)
            await send({"type": "http.response.body", "body": chunk, "more_body": True})

        await send({"type": "http.response.body", "body": b"", "more_body": False})

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        async with anyio.create_task_group() as task_group:
            async def wrap(func: "t.Callable[[], t.Awaitable[None]]") -> None:
                await func()
                await task_group.cancel_scope.cancel()

            task_group.start_soon(wrap, partial(self.stream_response, scope, send))
            await wrap(partial(self.listen_for_disconnect, receive))


def guess_type(
        url: t.Union[str, "os.PathLike[str]"], strict: bool = True
) -> t.Tuple[t.Optional[str], t.Optional[str]]:
    if sys.version_info < (3, 8):  # pragma: no cover
        url = os.fspath(url)
    return mimetypes_guess_type(url, strict)


def md5_hexdigest(data: bytes, *, usedforsecurity: bool = True) -> str:
    return hashlib.md5(data).hexdigest()
