"""
Caprail HTTP Request.

Wraps an ASGI scope/receive pair into a developer-friendly object
with Laravel-inspired accessor methods.
"""
from __future__ import annotations

import json
from http.cookies import SimpleCookie
from urllib.parse import parse_qs, unquote, urlparse

from caprail.http.uploaded_file import UploadedFile


class Request:
    """
    Represents an incoming HTTP request.

    Instantiated by the application's request factory from the raw
    ASGI scope, receive, and send callables.
    """

    def __init__(self, scope: dict, receive, send) -> None:
        self._scope = scope
        self._receive = receive
        self._send = send
        self._body: bytes | None = None
        self._json: dict | None = None
        self._form: dict | None = None
        self._files: dict[str, UploadedFile] | None = None
        self._query_params: dict[str, list[str]] | None = None
        self._headers_cache: dict[str, str] | None = None
        self._cookies_cache: dict[str, str] | None = None
        # Session/auth are injected by middleware
        self.session: dict = {}
        self.user: object | None = None
        # Route parameters injected by the router
        self.route_params: dict[str, str] = {}
        self.route_name: str | None = None

    # ------------------------------------------------------------------
    # Basic properties
    # ------------------------------------------------------------------

    @property
    def method(self) -> str:
        return self._scope.get("method", "GET").upper()

    @property
    def path(self) -> str:
        return self._scope.get("path", "/")

    @property
    def full_url(self) -> str:
        scheme = self._scope.get("scheme", "http")
        server = self._scope.get("server")
        host = self.header("host", "localhost")
        if server:
            host = f"{server[0]}:{server[1]}"
        query = self._scope.get("query_string", b"").decode()
        qs = f"?{query}" if query else ""
        return f"{scheme}://{host}{self.path}{qs}"

    @property
    def is_secure(self) -> bool:
        return self._scope.get("scheme", "http") == "https"

    @property
    def ip(self) -> str | None:
        client = self._scope.get("client")
        return client[0] if client else None

    # ------------------------------------------------------------------
    # Headers
    # ------------------------------------------------------------------

    @property
    def headers(self) -> dict[str, str]:
        if self._headers_cache is None:
            self._headers_cache = {
                k.decode().lower(): v.decode()
                for k, v in self._scope.get("headers", [])
            }
        return self._headers_cache

    def header(self, name: str, default: str | None = None) -> str | None:
        return self.headers.get(name.lower(), default)

    def has_header(self, name: str) -> bool:
        return name.lower() in self.headers

    def bearer_token(self) -> str | None:
        auth = self.header("authorization", "")
        if auth and auth.lower().startswith("bearer "):
            return auth[7:]
        return None

    # ------------------------------------------------------------------
    # Cookies
    # ------------------------------------------------------------------

    @property
    def cookies(self) -> dict[str, str]:
        if self._cookies_cache is None:
            raw = self.header("cookie", "")
            jar: SimpleCookie = SimpleCookie()
            jar.load(raw or "")
            self._cookies_cache = {k: v.value for k, v in jar.items()}
        return self._cookies_cache

    def cookie(self, name: str, default: str | None = None) -> str | None:
        return self.cookies.get(name, default)

    # ------------------------------------------------------------------
    # Query string
    # ------------------------------------------------------------------

    @property
    def query_params(self) -> dict[str, list[str]]:
        if self._query_params is None:
            raw = self._scope.get("query_string", b"").decode()
            self._query_params = parse_qs(raw, keep_blank_values=True)
        return self._query_params

    def query(self, key: str, default: str | None = None) -> str | list[str] | None:
        values = self.query_params.get(key)
        if values is None:
            return default
        return values[0] if len(values) == 1 else values

    def has_query(self, key: str) -> bool:
        return key in self.query_params

    # ------------------------------------------------------------------
    # Body / input
    # ------------------------------------------------------------------

    async def body(self) -> bytes:
        if self._body is None:
            chunks: list[bytes] = []
            while True:
                message = await self._receive()
                chunks.append(message.get("body", b""))
                if not message.get("more_body", False):
                    break
            self._body = b"".join(chunks)
        return self._body

    async def json(self, default: dict | None = None) -> dict | None:
        if self._json is None:
            raw = await self.body()
            try:
                self._json = json.loads(raw)
            except (ValueError, TypeError):
                return default
        return self._json

    async def form(self) -> dict[str, str | list[str]]:
        if self._form is None:
            raw = await self.body()
            content_type = self.header("content-type", "")
            if "multipart/form-data" in content_type:
                self._form, self._files = self._parse_multipart(raw, content_type)
            else:
                parsed = parse_qs(raw.decode(), keep_blank_values=True)
                self._form = {k: v[0] if len(v) == 1 else v for k, v in parsed.items()}
                self._files = {}
        return self._form  # type: ignore[return-value]

    async def input(self, key: str, default=None):
        """Get a value from JSON body, form data, or query string."""
        data = await self.json(default={}) or {}
        if key in data:
            return data[key]
        form = await self.form()
        if key in form:
            return form[key]
        return self.query(key, default)

    async def all(self) -> dict:
        """Return merged query + form/json inputs."""
        data = dict(self.query_params)
        body = await self.json(default={}) or {}
        if body:
            data.update(body)
        else:
            form = await self.form()
            data.update(form)
        return data

    async def only(self, *keys: str) -> dict:
        all_input = await self.all()
        return {k: all_input[k] for k in keys if k in all_input}

    async def except_keys(self, *keys: str) -> dict:
        all_input = await self.all()
        return {k: v for k, v in all_input.items() if k not in keys}

    async def has(self, key: str) -> bool:
        all_input = await self.all()
        return key in all_input

    # ------------------------------------------------------------------
    # Route helpers
    # ------------------------------------------------------------------

    def route(self, key: str, default=None):
        return self.route_params.get(key, default)

    # ------------------------------------------------------------------
    # Content type helpers
    # ------------------------------------------------------------------

    def wants_json(self) -> bool:
        accept = self.header("accept", "")
        return "application/json" in accept or "text/json" in accept

    def is_json(self) -> bool:
        ct = self.header("content-type", "")
        return "application/json" in ct

    def is_method(self, method: str) -> bool:
        return self.method == method.upper()

    def is_ajax(self) -> bool:
        return self.header("x-requested-with", "").lower() == "xmlhttprequest"

    # ------------------------------------------------------------------
    # Multipart (basic)
    # ------------------------------------------------------------------

    def _parse_multipart(
        self, body: bytes, content_type: str
    ) -> tuple[dict, dict[str, UploadedFile]]:
        boundary = ""
        for part in content_type.split(";"):
            part = part.strip()
            if part.startswith("boundary="):
                boundary = part[9:].strip('"')
        form: dict = {}
        files: dict[str, UploadedFile] = {}
        if not boundary:
            return form, files
        delimiter = f"--{boundary}".encode()
        parts = body.split(delimiter)
        for raw_part in parts[1:]:
            if raw_part.strip() in (b"", b"--", b"--\r\n"):
                continue
            if b"\r\n\r\n" not in raw_part:
                continue
            header_section, _, value = raw_part.partition(b"\r\n\r\n")
            value = value.rstrip(b"\r\n")
            headers_raw = header_section.decode(errors="replace")
            disposition = ""
            content_type_part = ""
            for line in headers_raw.splitlines():
                if line.lower().startswith("content-disposition"):
                    disposition = line
                elif line.lower().startswith("content-type"):
                    content_type_part = line.split(":", 1)[1].strip() if ":" in line else ""
            name = ""
            filename = ""
            for segment in disposition.split(";"):
                segment = segment.strip()
                if segment.startswith("name="):
                    name = unquote(segment[5:].strip('"'))
                elif segment.startswith("filename="):
                    filename = unquote(segment[9:].strip('"'))
            if filename:
                files[name] = UploadedFile(filename, content_type_part, value)
            elif name:
                form[name] = value.decode(errors="replace")
        return form, files

    def __repr__(self) -> str:
        return f"<Request {self.method} {self.path}>"
