from __future__ import annotations

from dataclasses import dataclass
from uuid import uuid4

import httpx

from .config import DEFAULT_USER_AGENT, get_default_qf_version, get_timeout_seconds, normalize_base_url
from .errors import QingflowApiError
from .json_types import JSONObject, JSONScalar, JSONValue


@dataclass(slots=True)
class BackendRequestContext:
    base_url: str
    token: str
    ws_id: int | None
    qf_request_id: str | None = None
    qf_version: str | None = None
    qf_version_source: str | None = None


@dataclass(slots=True)
class BackendResponse:
    data: JSONValue
    headers: dict[str, str]
    request_id: str
    http_status: int
    qf_response_version: str | None = None


class BackendClient:
    def __init__(self, timeout: float | None = None, client: httpx.Client | None = None) -> None:
        self._owns_client = client is None
        self._default_qf_version = get_default_qf_version()
        self._client = client or httpx.Client(
            timeout=timeout or get_timeout_seconds(),
            follow_redirects=True,
            trust_env=False,
        )

    def close(self) -> None:
        if self._owns_client:
            self._client.close()

    def public_request(
        self,
        method: str,
        base_url: str,
        path: str,
        *,
        params: JSONObject | None = None,
        json_body: JSONValue = None,
        unwrap: bool = True,
        qf_version: str | None = None,
        ) -> JSONValue:
        return self.public_request_with_meta(
            method,
            base_url,
            path,
            params=params,
            json_body=json_body,
            unwrap=unwrap,
            qf_version=qf_version,
        ).data

    def public_request_with_meta(
        self,
        method: str,
        base_url: str,
        path: str,
        *,
        params: JSONObject | None = None,
        json_body: JSONValue = None,
        unwrap: bool = True,
        qf_version: str | None = None,
    ) -> BackendResponse:
        return self._request_with_meta(
            method,
            self._build_url(base_url, path),
            params=params,
            json_body=json_body,
            headers=self._base_headers(None, None, qf_version=qf_version),
            unwrap=unwrap,
        )

    def request(
        self,
        method: str,
        context: BackendRequestContext,
        path: str,
        *,
        params: JSONObject | None = None,
        json_body: JSONValue = None,
        unwrap: bool = True,
    ) -> JSONValue:
        return self.request_with_meta(
            method,
            context,
            path,
            params=params,
            json_body=json_body,
            unwrap=unwrap,
        ).data

    def request_with_meta(
        self,
        method: str,
        context: BackendRequestContext,
        path: str,
        *,
        params: JSONObject | None = None,
        json_body: JSONValue = None,
        unwrap: bool = True,
    ) -> BackendResponse:
        return self._request_with_meta(
            method,
            self._build_url(context.base_url, path),
            params=params,
            json_body=json_body,
            headers=self._base_headers(
                context.token,
                context.ws_id,
                context.qf_request_id,
                qf_version=context.qf_version,
            ),
            unwrap=unwrap,
        )

    def describe_route(self, context: BackendRequestContext) -> JSONObject:
        qf_version, source = self._resolve_qf_version(context.qf_version)
        if context.qf_version is not None and context.qf_version_source:
            source = context.qf_version_source
        return {
            "base_url": normalize_base_url(context.base_url) or context.base_url,
            "qf_version": qf_version,
            "qf_version_source": source,
        }

    def upload_binary(
        self,
        url: str,
        content: bytes,
        *,
        content_type: str | None = None,
        headers: dict[str, str] | None = None,
    ) -> JSONObject:
        request_headers = dict(headers or {})
        if content_type:
            request_headers.setdefault("Content-Type", content_type)
        try:
            response = self._client.put(url, content=content, headers=request_headers or None)
        except httpx.RequestError as exc:
            raise QingflowApiError(category="network", message=str(exc))
        if response.status_code >= 400:
            raise QingflowApiError(
                category="http",
                message=self._extract_message(response.text) or f"HTTP {response.status_code}",
                http_status=response.status_code,
            )
        return {
            "status_code": response.status_code,
            "headers": dict(response.headers),
        }

    def upload_form_file(
        self,
        url: str,
        *,
        form_fields: dict[str, str],
        file_field: str,
        file_name: str,
        content: bytes,
        content_type: str | None = None,
        headers: dict[str, str] | None = None,
    ) -> JSONObject:
        try:
            response = self._client.post(
                url,
                data=form_fields,
                files={file_field: (file_name, content, content_type or "application/octet-stream")},
                headers=headers or None,
            )
        except httpx.RequestError as exc:
            raise QingflowApiError(category="network", message=str(exc))
        if response.status_code >= 400:
            raise QingflowApiError(
                category="http",
                message=self._extract_message(response.text) or f"HTTP {response.status_code}",
                http_status=response.status_code,
            )
        body: JSONValue = None
        if response.content:
            try:
                body = response.json()
            except ValueError:
                body = response.text
        return {
            "status_code": response.status_code,
            "headers": dict(response.headers),
            "body": body,
        }

    def _request_with_meta(
        self,
        method: str,
        url: str,
        *,
        params: JSONObject | None,
        json_body: JSONValue,
        headers: dict[str, str],
        unwrap: bool,
    ) -> BackendResponse:
        attempts = 2 if method.upper() == "GET" else 1
        last_error: QingflowApiError | None = None
        for _ in range(attempts):
            try:
                response = self._client.request(method.upper(), url, params=params, json=json_body, headers=headers)
                parsed = self._parse_response(response, headers["Qf-Request-Id"], unwrap=unwrap)
                return BackendResponse(
                    data=parsed,
                    headers=dict(response.headers),
                    request_id=headers["Qf-Request-Id"],
                    http_status=response.status_code,
                    qf_response_version=self._extract_response_qf_version(response.headers),
                )
            except httpx.RequestError as exc:
                last_error = QingflowApiError(category="network", message=str(exc), request_id=headers["Qf-Request-Id"])
        assert last_error is not None
        raise last_error

    def _parse_response(self, response: httpx.Response, request_id: str, *, unwrap: bool) -> JSONValue:
        payload: JSONValue
        try:
            payload = response.json()
        except ValueError:
            payload = response.text
        if response.status_code >= 400:
            raise QingflowApiError(
                category="http",
                message=self._extract_message(payload) or f"HTTP {response.status_code}",
                backend_code=self._extract_code(payload),
                request_id=request_id,
                http_status=response.status_code,
            )
        if not unwrap:
            return payload
        return self._unwrap_payload(payload, request_id, response.status_code)

    def _unwrap_payload(self, payload: JSONValue, request_id: str, http_status: int) -> JSONValue:
        if not isinstance(payload, dict):
            return payload
        if "success" in payload:
            if not bool(payload.get("success")):
                raise QingflowApiError(
                    category="backend",
                    message=self._extract_message(payload) or "Qingflow request failed",
                    backend_code=self._extract_code(payload),
                    request_id=request_id,
                    http_status=http_status,
                )
            return self._extract_success_data(payload)
        code = self._extract_code(payload)
        if code not in (None, 0, "0"):
            raise QingflowApiError(
                category="backend",
                message=self._extract_message(payload) or "Qingflow request failed",
                backend_code=code,
                request_id=request_id,
                http_status=http_status,
            )
        if code in (0, "0"):
            return self._extract_success_data(payload)
        return payload

    def _extract_success_data(self, payload: JSONObject) -> JSONValue:
        for key in ("result", "data", "page", "obj"):
            if key in payload:
                return payload[key]
        return payload

    def _extract_message(self, payload: JSONValue) -> str | None:
        if not isinstance(payload, dict):
            return str(payload) if payload else None
        for key in ("message", "msg", "errMsg", "error", "detail"):
            value = payload.get(key)
            if value:
                return str(value)
        return None

    def _extract_code(self, payload: JSONValue) -> JSONScalar:
        if isinstance(payload, dict):
            return payload.get("code", payload.get("errCode"))
        return None

    def _base_headers(
        self,
        token: str | None,
        ws_id: int | None,
        request_id: str | None = None,
        *,
        qf_version: str | None = None,
    ) -> dict[str, str]:
        headers = {
            "User-Agent": DEFAULT_USER_AGENT,
            "Qf-Request-Id": request_id or str(uuid4()),
        }
        resolved_qf_version, _ = self._resolve_qf_version(qf_version)
        if resolved_qf_version:
            headers["Cookie"] = f"qfVersion={resolved_qf_version}"
        if token:
            headers["token"] = token
        if ws_id is not None:
            headers["wsId"] = str(ws_id)
        return headers

    def _resolve_qf_version(self, explicit_qf_version: str | None) -> tuple[str | None, str]:
        if explicit_qf_version is not None:
            normalized = str(explicit_qf_version).strip() or None
            return normalized, "context"
        if self._default_qf_version:
            return self._default_qf_version, "default_config"
        return None, "unset"

    def _extract_response_qf_version(self, headers: httpx.Headers) -> str | None:
        value = headers.get("x-q-response-version")
        if value is None:
            return None
        normalized = str(value).strip()
        return normalized or None

    def _build_url(self, base_url: str, path: str) -> str:
        normalized = normalize_base_url(base_url)
        if not normalized:
            raise QingflowApiError.config_error("base_url is required")
        return f"{normalized}/{path.lstrip('/')}"
