from __future__ import annotations

import json
from collections.abc import Iterator, Mapping
from http.cookies import SimpleCookie
from urllib.parse import parse_qsl

from .exceptions import BadRequest
from .typing import Receive, Scope


class MultiDict(Mapping[str, str]):
    def __init__(self, data: dict[str, list[str]] | None = None) -> None:
        self._data = data or {}

    @classmethod
    def from_query_string(cls, query_string: bytes) -> "MultiDict":
        text = query_string.decode("latin-1")
        parsed: dict[str, list[str]] = {}
        for key, value in parse_qsl(text, keep_blank_values=True):
            parsed.setdefault(key, []).append(value)
        return cls(parsed)

    def __getitem__(self, key: str) -> str:
        values = self._data[key]
        return values[0]

    def __iter__(self) -> Iterator[str]:
        return iter(self._data)

    def __len__(self) -> int:
        return len(self._data)

    def getlist(self, key: str) -> list[str]:
        return list(self._data.get(key, []))

    def to_dict(self, flat: bool = True) -> dict[str, str] | dict[str, list[str]]:
        if flat:
            return {k: values[0] for k, values in self._data.items() if values}
        return {k: list(values) for k, values in self._data.items()}


class Request:
    def __init__(self, scope: Scope, receive: Receive) -> None:
        self.scope = scope
        self._receive = receive
        self._body_cache: bytes | None = None
        self._args_cache: MultiDict | None = None
        self._headers_cache: dict[str, str] | None = None

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

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

    @property
    def query_string(self) -> bytes:
        return bytes(self.scope.get("query_string", b""))

    @property
    def headers(self) -> Mapping[str, str]:
        if self._headers_cache is None:
            raw_headers = self.scope.get("headers", [])
            headers: dict[str, str] = {}
            for name, value in raw_headers:
                headers[name.decode("latin-1").lower()] = value.decode("latin-1")
            self._headers_cache = headers
        return self._headers_cache

    @property
    def cookies(self) -> Mapping[str, str]:
        cookie_header = self.headers.get("cookie", "")
        parsed = SimpleCookie()
        parsed.load(cookie_header)
        return {k: morsel.value for k, morsel in parsed.items()}

    @property
    def client(self) -> tuple[str, int] | None:
        client = self.scope.get("client")
        if not isinstance(client, (tuple, list)) or len(client) != 2:
            return None
        host, port = client
        return str(host), int(port)

    @property
    def args(self) -> MultiDict:
        if self._args_cache is None:
            self._args_cache = MultiDict.from_query_string(self.query_string)
        return self._args_cache

    async def body(self) -> bytes:
        if self._body_cache is not None:
            return self._body_cache

        chunks: list[bytes] = []
        more_body = True
        while more_body:
            message = await self._receive()
            message_type = message.get("type")
            if message_type == "http.disconnect":
                break
            if message_type != "http.request":
                continue
            chunks.append(message.get("body", b""))
            more_body = bool(message.get("more_body", False))

        self._body_cache = b"".join(chunks)
        return self._body_cache

    async def text(self, encoding: str = "utf-8") -> str:
        return (await self.body()).decode(encoding)

    async def json(self) -> object:
        raw = await self.body()
        try:
            return json.loads(raw.decode("utf-8"))
        except (UnicodeDecodeError, json.JSONDecodeError) as exc:
            raise BadRequest(description="Invalid JSON body") from exc

    async def form(self) -> dict[str, str | list[str]]:
        raw = await self.body()
        content_type = self.headers.get("content-type", "")
        if "application/x-www-form-urlencoded" not in content_type:
            return {}
        parsed = MultiDict.from_query_string(raw)
        return parsed.to_dict(flat=False)

