"""
Caprail HTTP Response classes.

Provides JSON, HTML, redirect, file, and streaming responses
with a Laravel-inspired fluent interface.
"""
from __future__ import annotations

import json
import os
from collections.abc import AsyncGenerator
from http import HTTPStatus


class Response:
    """
    A standard HTTP response.

    Usage::

        return Response("Hello")
        return Response.json({"ok": True})
        return Response.redirect("/login")
    """

    def __init__(
        self,
        content: str | bytes = "",
        status: int = 200,
        headers: dict[str, str] | None = None,
        content_type: str = "text/html; charset=utf-8",
    ) -> None:
        self.content = content.encode() if isinstance(content, str) else content
        self.status = status
        self.content_type = content_type
        self._headers: dict[str, str] = headers or {}
        self._cookies: list[str] = []

    # ------------------------------------------------------------------
    # Factories
    # ------------------------------------------------------------------

    @classmethod
    def make(
        cls,
        content: str | bytes = "",
        status: int = 200,
        headers: dict[str, str] | None = None,
    ) -> "Response":
        return cls(content, status, headers)

    @classmethod
    def json(
        cls,
        data,
        status: int = 200,
        headers: dict[str, str] | None = None,
        indent: int | None = None,
    ) -> "Response":
        body = json.dumps(data, default=str, indent=indent, ensure_ascii=False)
        return cls(body, status, headers, "application/json; charset=utf-8")

    @classmethod
    def redirect(
        cls,
        url: str,
        status: int = 302,
        headers: dict[str, str] | None = None,
    ) -> "Response":
        h = dict(headers or {})
        h["location"] = url
        return cls("", status, h)

    @classmethod
    def back(cls) -> "Response":
        """Placeholder — the application resolves the Referer at runtime."""
        r = cls("", 302)
        r._headers["_back"] = "1"
        return r

    @classmethod
    def no_content(cls, status: int = 204) -> "Response":
        return cls("", status)

    # ------------------------------------------------------------------
    # Fluent setters
    # ------------------------------------------------------------------

    def with_header(self, name: str, value: str) -> "Response":
        self._headers[name.lower()] = value
        return self

    def with_headers(self, headers: dict[str, str]) -> "Response":
        for k, v in headers.items():
            self._headers[k.lower()] = v
        return self

    def with_cookie(
        self,
        name: str,
        value: str,
        max_age: int | None = None,
        path: str = "/",
        domain: str | None = None,
        secure: bool = False,
        http_only: bool = True,
        same_site: str = "Lax",
    ) -> "Response":
        parts = [f"{name}={value}", f"Path={path}"]
        if max_age is not None:
            parts.append(f"Max-Age={max_age}")
        if domain:
            parts.append(f"Domain={domain}")
        if secure:
            parts.append("Secure")
        if http_only:
            parts.append("HttpOnly")
        parts.append(f"SameSite={same_site}")
        self._cookies.append("; ".join(parts))
        return self

    def forget_cookie(self, name: str, path: str = "/") -> "Response":
        return self.with_cookie(name, "", max_age=0, path=path)

    def with_status(self, status: int) -> "Response":
        self.status = status
        return self

    # ------------------------------------------------------------------
    # ASGI send
    # ------------------------------------------------------------------

    async def send(self, send) -> None:
        headers = self._build_headers()
        await send({
            "type": "http.response.start",
            "status": self.status,
            "headers": headers,
        })
        await send({
            "type": "http.response.body",
            "body": self.content,
            "more_body": False,
        })

    def _build_headers(self) -> list[tuple[bytes, bytes]]:
        out: dict[str, str] = {}
        out["content-type"] = self.content_type
        out["content-length"] = str(len(self.content))
        out.update(self._headers)
        result = [(k.encode(), v.encode()) for k, v in out.items()]
        for cookie in self._cookies:
            result.append((b"set-cookie", cookie.encode()))
        return result

    def __repr__(self) -> str:
        return f"<Response {self.status} {self.content_type}>"


class StreamingResponse(Response):
    """
    Streams content from an async generator.

    Usage::

        async def gen():
            for chunk in data:
                yield chunk

        return StreamingResponse(gen(), content_type="text/plain")
    """

    def __init__(
        self,
        generator: AsyncGenerator[bytes, None],
        status: int = 200,
        headers: dict[str, str] | None = None,
        content_type: str = "text/plain; charset=utf-8",
    ) -> None:
        self._generator = generator
        self.status = status
        self.content_type = content_type
        self._headers: dict[str, str] = headers or {}
        self._cookies: list[str] = []

    async def send(self, send) -> None:
        headers = [(b"content-type", self.content_type.encode())]
        for k, v in self._headers.items():
            headers.append((k.encode(), v.encode()))
        await send({
            "type": "http.response.start",
            "status": self.status,
            "headers": headers,
        })
        async for chunk in self._generator:
            await send({"type": "http.response.body", "body": chunk, "more_body": True})
        await send({"type": "http.response.body", "body": b"", "more_body": False})


class FileResponse(Response):
    """Serve a file from disk."""

    def __init__(
        self,
        path: str,
        filename: str | None = None,
        content_type: str = "application/octet-stream",
        as_attachment: bool = True,
    ) -> None:
        self._path = path
        self._filename = filename or os.path.basename(path)
        self.content_type = content_type
        self.as_attachment = as_attachment
        self.status = 200
        self._headers: dict[str, str] = {}
        self._cookies: list[str] = []

    async def send(self, send) -> None:
        import aiofiles
        async with aiofiles.open(self._path, "rb") as f:
            data = await f.read()
        disposition = "attachment" if self.as_attachment else "inline"
        headers = [
            (b"content-type", self.content_type.encode()),
            (b"content-length", str(len(data)).encode()),
            (b"content-disposition", f'{disposition}; filename="{self._filename}"'.encode()),
        ]
        await send({"type": "http.response.start", "status": self.status, "headers": headers})
        await send({"type": "http.response.body", "body": data, "more_body": False})
