import mimetypes
from http import HTTPStatus
from os import path
from typing import Any, AsyncGenerator, Optional, Union, TYPE_CHECKING
import traceback
import sys

from functools import singledispatch, update_wrapper
from orjson import dumps, loads

from .constants import MESSAGE_NOT_FOUND, STATUS_NOT_FOUND
from .context import Context
from .tutorials import get_guardian_angel_html, ASYNC_RENDER, NO_TEMPLATING, SYNC_RENDER
from .request import Request
if TYPE_CHECKING:
    from router import App, Router  # pragma: no cover


# For compatibility with older versions of python3 using this
# otherwise would use singledispatchmethod available from ^3.8
def MethodDispatch(method):
    decorated = singledispatch(method)
    def decorator(*args, **kwargs):
        return decorated.dispatch(args[1].__class__)(*args, **kwargs)
    decorator.register = decorated.register # type: ignore
    update_wrapper(decorator, method)
    return decorator


@singledispatch
def _body(payload):
    return payload

@_body.register(str)
def _(payload: str):
    return payload.encode()

@_body.register(int)
@_body.register(float)
def _(payload):
    return f'{payload}'.encode()

def is_async_gen(obj):
    return hasattr(obj, '__aiter__') or hasattr(obj, '__anext__')


@_body.register(object)
def _(payload):
    if is_async_gen(payload):
        return payload
    return payload

def _get_guardian_angel(res: 'Response', exc: Exception):
    res.headers = 'Content-Type', 'text/html'
    res.status = HTTPStatus.INTERNAL_SERVER_ERROR
    tb = traceback.format_exc()
    res.body = get_guardian_angel_html(res._req, exc, tb)


class Response():
    def __init__(self, app: 'App', context: 'Context', request: Request):
        self._app = app
        self._ctx = context
        self._req = request
        self._abort = False
        self._body = MESSAGE_NOT_FOUND.encode()
        self._deferred = []
        self._metadata = {}
        self._headers = []
        self._status = STATUS_NOT_FOUND
        self._template = None
        self._mounted_from_application : Union['Router', None] = None

    @MethodDispatch
    def abort(self, payload):
        self._abort = True
        self._body = payload

    @abort.register(str) # type: ignore
    def _(self, payload: str):
        self._abort = True
        self._body = payload.encode()

    @abort.register(int) # type: ignore
    @abort.register(float) # type: ignore
    def _(self, payload):
        self._abort = True
        self._body = f'{payload}'.encode()

    @property
    def body(self):
        return self._body

    @body.setter
    def body(self, value):
        self._body = _body(value)

    def defer(self, func) -> 'Response':
        self._deferred.append(func)
        return self

    @property
    def deferred(self):
        return len(self._deferred) > 0

    @property
    def json(self) -> Any:
        if isinstance(self.body, (dict, list)):
            return self.body
        return loads(self.body)

    @property
    def text(self) -> str:
        return self.body.decode()

    @property
    def http(self):
        return HTTPStatus

    def header(self, key, val) -> 'Response':
        if isinstance(val, (list, tuple, set)): val = ', '.join(map(str, val))
        else: val = str(val)

        _encode = lambda k: k.encode('utf-8') if isinstance(k, str) else k
        value = _encode(key), _encode(val)
        self._headers.append(value)
        return self

    @property
    def headers(self):
        return self._headers

    @headers.setter
    def headers(self, value) -> 'Response':
        key, val = value
        return self.header(key, val)

    @property
    def metadata(self):
        return self._metadata

    @metadata.setter
    def metadata(self, value):
        if not isinstance(value, dict): raise ValueError
        self._metadata = value

    def cookie(self, name: str, value: str, **kwargs):
        cookie_string = f'{name}={value}'
        for key, val in kwargs.items():
            _key = {
                'expires': 'Expires',
                'secure': 'Secure',
                'httponly': 'HttpOnly',
                'http_only': 'HttpOnly',
                'samesite': 'SameSite',
                'same_site': 'SameSite',
                'domain': 'Domain',
                'path': 'Path',
                'partitioned': 'Partitioned',
                'max_age': 'Max-Age',
                'maxage': 'Max-Age',
            }.get(key.lower(), key)
            if _key == 'Expires':
                try: val = val.strftime('%a, %d %b %Y %H:%M:%S GMT')
                except: raise ValueError(f'Expires must be a datetime object, got {val}')
            if _key in ['Secure', 'HttpOnly', 'Partitioned']:
                if val: cookie_string += f'; {_key}'
                continue
            if _key == 'Max-Age':
                try: val = int(val)
                except: raise ValueError(f'Max-Age must be an integer, got {val}')
            if _key == 'SameSite':
                _val = str(val).capitalize()
                if _val not in ['Strict', 'Lax', 'None']:
                    raise ValueError(f'SameSite must be one of Strict, Lax, None, got {val}')
                val = _val
            cookie_string += f'; {_key}={val}'
        self.headers = 'Set-Cookie', cookie_string

    async def render(self, name: str, **contexts) -> 'Response':
        """Serve html file walking up parent router/app tree until base parent if necessary"""
        templater = self._app._templater
        self.headers = 'content-type', 'text/html; charset=utf-8'
        # if self._mounted_from_application: templater = self._mounted_from_application._templater or templater
        if not templater:
            _get_guardian_angel(self, ValueError('Templating not enabled. Call app.TEMPLATES() first.'))
            return self

        if not templater.is_async:
            _get_guardian_angel(self, RuntimeError('Trying to use Sync HTML Renderer to render HTML Async'))
            return self

        template = templater.get_template(name)
        html = await template.render_async({'ctx': self._ctx, 'res': self, 'req': self._req, **contexts})
        self.body = html
        return self

    def renders(self, name: str, **contexts) -> 'Response':
        """Synchronous version of render method above"""
        templater = self._app._templater
        self.headers = 'content-type', 'text/html; charset=utf-8'
        if not templater:
            _get_guardian_angel(self, ValueError('Templating not enabled. Call app.TEMPLATES() first.'))
            return self

        if templater.is_async:
            _get_guardian_angel(self, RuntimeError('Trying to use Async HTML Renderer to render Sync HTML'))
            return self
        template = templater.get_template(name)
        html = template.render({'ctx': self._ctx, 'res': self, 'req': self._req, **contexts})
        self.body = html
        return self

    def redirect(self, location, permanent=False) -> 'Response':
        if permanent: self.status = HTTPStatus.PERMANENT_REDIRECT
        else: self.status = HTTPStatus.TEMPORARY_REDIRECT
        self.headers = 'Location', location
        return self

    @property
    def status(self): # pragma: no cover
        return self._status

    @status.setter
    def status(self, value: int) -> 'Response':
        self._status = value
        return self

    def stream(self, generator, content_type='text/plain', status=200, sse=False) -> 'Response':
        """Stream data back to the client using an async generator"""
        self.status = status
        
        if sse:
            self.headers = 'Content-Type', 'text/event-stream'
            self.headers = 'Cache-Control', 'no-cache'
            self.headers = 'Connection', 'keep-alive'
            self.headers = 'X-Accel-Buffering', 'no'  # Disable Nginx buffering
            
            async def sse_wrapper():
                async for item in generator:
                    # If item is a dict or list, encode as JSON
                    if isinstance(item, (dict, list)):
                        item = dumps(item)
                    yield f"data: {item}\n\n".encode()
            self.body = sse_wrapper()
        else:
            self.headers = 'Content-Type', content_type
            self.headers = 'Cache-Control', 'no-cache'
            self.headers = 'Transfer-Encoding', 'chunked'
            self.body = generator
            
        return self

    @property
    def template(self):  # pragma: no cover
        return self._template

    @template.setter
    def template(self, path):
        self._template

    def out(self, status: int, body, headers=None) -> 'Response':
        self.status = status
        self.body = body
        if headers: self.headers = headers
        return self

    async def interpolate(self, name: str, **contexts):
        """Serve html file walking up parent router/app tree until base parent if necessary"""
        templater = self._app._templater
        if not templater: raise AttributeError('Can not interpolate without enabling templating on the heaven application')
        if not templater.is_async: raise AttributeError('Async rendering not supported by sync renderer')
        template = templater.get_template(name)
        return await template.render_async({'ctx': self._ctx, **contexts})

    def file(self, filepath: str, filename: Optional[str] = None, chunk_size: int = 4096) -> 'Response':
        """Serve a file from the filesystem with streaming support"""
        if not path.isfile(filepath):
            self.status = HTTPStatus.NOT_FOUND
            self.body = b'File not found'
            return self

        mime_type, _ = mimetypes.guess_type(filepath)
        self.headers = 'Content-Type', mime_type or 'application/octet-stream'
        
        if filename:
            self.headers = 'Content-Disposition', f'attachment; filename="{filename}"'
        else:
            self.headers = 'Content-Disposition', f'inline; filename="{path.basename(filepath)}"'

        async def file_sender():
            from aiofiles import open as async_open
            async with async_open(filepath, mode='rb') as f:
                while True:
                    chunk = await f.read(chunk_size)
                    if not chunk:
                        break
                    yield chunk

        self.body = file_sender()
        self.status = HTTPStatus.OK
        return self
