import os
from typing import Any, Dict, Optional, Union


class BaseResponse:
    """
    Base class for all Ignyx responses.
    """

    def __init__(
        self, content: Any, status_code: int = 200, headers: Optional[Dict[str, str]] = None
    ) -> None:
        "Initialize the response."
        self.content: Any = content
        self.status_code: int = status_code
        self.headers: Dict[str, str] = headers or {}
        self.content_type: str = "text/plain"

    def render(self) -> Any:
        "Render the response content."
        return self.content

    def set_cookie(
        self,
        key: str,
        value: str,
        max_age: Optional[int] = None,
        httponly: bool = False,
        secure: bool = False,
        samesite: str = "lax",
        path: str = "/",
    ) -> None:
        """Set a cookie on the response."""
        cookie = f"{key}={value}; Path={path}; SameSite={samesite}"
        if max_age is not None:
            cookie += f"; Max-Age={max_age}"
        if httponly:
            cookie += "; HttpOnly"
        if secure:
            cookie += "; Secure"
        self.headers["set-cookie"] = cookie

    def delete_cookie(self, key: str, path: str = "/") -> None:
        """Delete a cookie from the response."""
        self.headers["set-cookie"] = f"{key}=; Path={path}; Max-Age=0"


class JSONResponse(BaseResponse):
    """
    Returns a JSON encoded response.
    """

    def __init__(
        self, content: Any, status_code: int = 200, headers: Optional[Dict[str, str]] = None
    ) -> None:
        "Initialize the JSON response."
        super().__init__(content, status_code, headers)
        self.content_type = "application/json"

    def render(self) -> str:
        "Serialize content to a JSON string."
        import json

        return json.dumps(self.content)


class HTMLResponse(BaseResponse):
    """
    Returns an HTML response.
    """

    def __init__(
        self, content: str, status_code: int = 200, headers: Optional[Dict[str, str]] = None
    ) -> None:
        "Initialize the HTML response."
        super().__init__(content, status_code, headers)
        self.content_type = "text/html; charset=utf-8"


class PlainTextResponse(BaseResponse):
    """
    Returns a plain text response.
    """

    def __init__(
        self, content: str, status_code: int = 200, headers: Optional[Dict[str, str]] = None
    ) -> None:
        "Initialize the plain text response."
        super().__init__(content, status_code, headers)
        self.content_type = "text/plain; charset=utf-8"


class RedirectResponse(BaseResponse):
    """
    Returns an HTTP redirect.
    """

    def __init__(
        self, url: str, status_code: int = 302, headers: Optional[Dict[str, str]] = None
    ) -> None:
        "Initialize the redirect response."
        super().__init__("", status_code, headers)
        self.content_type = "text/plain"
        self.headers["location"] = url


class FileResponse(BaseResponse):
    """
    Returns a file attachment response.
    """

    def __init__(
        self,
        path: Union[str, "os.PathLike[str]"],
        filename: Optional[str] = None,
        status_code: int = 200,
        headers: Optional[Dict[str, str]] = None,
    ) -> None:
        "Initialize the file response."
        super().__init__("", status_code, headers)
        self.path: str = str(path)
        self.filename: str = filename or str(path).split("/")[-1]
        self.content_type: str = "application/octet-stream"
        self.headers["content-disposition"] = f'attachment; filename="{self.filename}"'

    def render(self) -> bytes:
        "Read file content up to 10MB limit."
        # Safety rail: Prevent OOM crashes by capping in-memory file serving to 10MB
        max_size = 10 * 1024 * 1024
        if os.path.exists(self.path) and os.path.getsize(self.path) > max_size:
            raise RuntimeError(
                f"File '{self.filename}' exceeds the 10MB limit for FileResponse. "
                "Large file streaming support is planned for Ignyx v0.3.0."
            )

        with open(self.path, "rb") as f:
            return f.read()


class StreamingResponse(BaseResponse):
    """
    Returns a streaming response with chunked transfer encoding.
    Accepts a sync or async iterator/generator that yields string chunks.

    Crucial for AI/LLM token streaming and large data exports.

    Usage:
        @app.get("/stream")
        async def stream():
            async def generate():
                for i in range(10):
                    yield f"chunk {i}\\n"
                    await asyncio.sleep(0.1)
            return StreamingResponse(generate(), media_type="text/plain")
    """

    __ignyx_streaming__ = True

    def __init__(
        self,
        content: Any,
        status_code: int = 200,
        headers: Optional[Dict[str, str]] = None,
        media_type: str = "application/octet-stream",
    ) -> None:
        "Initialize the streaming response."
        super().__init__("", status_code, headers)
        self.content_type = media_type
        self.body_iterator = content


class EventSourceResponse(BaseResponse):
    """
    Returns a Server-Sent Events (SSE) streaming response.
    Accepts a sync or async iterator/generator that yields SSE event strings.

    Each yielded chunk should follow the SSE protocol format:
        "data: <payload>\\n\\n"

    Usage:
        @app.get("/events")
        async def events():
            async def generate():
                for i in range(10):
                    yield f"data: token {i}\\n\\n"
                    await asyncio.sleep(0.5)
            return EventSourceResponse(generate())
    """

    __ignyx_streaming__ = True

    def __init__(
        self,
        content: Any,
        status_code: int = 200,
        headers: Optional[Dict[str, str]] = None,
    ) -> None:
        "Initialize the SSE response."
        super().__init__("", status_code, headers)
        self.content_type = "text/event-stream"
        self.body_iterator = content
        self.headers["cache-control"] = "no-cache"
        self.headers["connection"] = "keep-alive"
