from __future__ import annotations

from pathlib import Path
from typing import Any

import httpx

from simplicity_cli.config import RuntimeConfig
from simplicity_cli.errors import CliError, ExitCode
from simplicity_cli.models import JSONDict


class SimplicityApiClient:
    def __init__(self, config: RuntimeConfig) -> None:
        self._timeout = config.request_timeout_seconds
        self._client = httpx.Client(
            base_url=config.base_url,
            timeout=self._timeout,
            headers={"X-API-Key": config.api_key},
            follow_redirects=True,
        )

    def __enter__(self) -> "SimplicityApiClient":
        return self

    def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
        self.close()

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

    def upload_document_file(self, file_path: Path, document_type: str) -> JSONDict:
        with file_path.open("rb") as handle:
            files = {"file": (file_path.name, handle, "application/octet-stream")}
            return self._request(
                "POST",
                "/v1/documents/upload-document",
                params={"document_type": document_type},
                files=files,
            )

    def upload_document_url(self, url: str, document_type: str) -> JSONDict:
        return self._request(
            "POST",
            "/v1/documents/upload-document-url",
            params={"document_type": document_type},
            json={"url": url},
        )

    def create_autofill_form(
        self,
        form_file_document_id: str,
        source_document_ids: list[str] | None = None,
        context: str | None = None,
        instructions: str | None = None,
    ) -> JSONDict:
        body: JSONDict = {"form_file_document_id": form_file_document_id}
        if source_document_ids:
            body["source_document_ids"] = source_document_ids
        if context is not None:
            body["context"] = context
        if instructions is not None:
            body["instructions"] = instructions
        return self._request("POST", "/v1/forms/autofill", json=body)

    def autofill_existing_form(
        self,
        form_id: str,
        context: str | None = None,
        instructions: str | None = None,
    ) -> JSONDict:
        body: JSONDict = {}
        if context is not None:
            body["context"] = context
        if instructions is not None:
            body["instructions"] = instructions
        return self._request("POST", f"/v1/forms/{form_id}/autofill", json=body)

    def get_task(self, task_id: str) -> JSONDict:
        return self._request("GET", f"/v1/tasks/{task_id}")

    def get_form(self, form_id: str) -> JSONDict:
        return self._request("GET", f"/v1/forms/{form_id}")

    def get_document(self, document_id: str) -> JSONDict:
        return self._request("GET", f"/v1/documents/{document_id}")

    def download_file(self, download_url: str, destination: Path) -> None:
        destination.parent.mkdir(parents=True, exist_ok=True)
        try:
            with httpx.Client(timeout=self._timeout, follow_redirects=True) as download_client:
                with download_client.stream("GET", download_url) as response:
                    if response.status_code >= 400:
                        raise CliError(
                            f"file download failed with status {response.status_code}.",
                            exit_code=ExitCode.DOWNLOAD_ERROR,
                            code="download_http_error",
                            details={"status_code": response.status_code, "url": download_url},
                        )
                    with destination.open("wb") as handle:
                        for chunk in response.iter_bytes():
                            handle.write(chunk)
        except httpx.RequestError as exc:
            raise CliError(
                "file download failed due to network error.",
                exit_code=ExitCode.DOWNLOAD_ERROR,
                code="download_network_error",
                details={"url": download_url, "error": str(exc)},
            ) from exc

    def _request(self, method: str, path: str, **kwargs: Any) -> JSONDict:
        try:
            response = self._client.request(method, path, **kwargs)
        except httpx.RequestError as exc:
            raise CliError(
                "request to API failed.",
                exit_code=ExitCode.API_ERROR,
                code="request_error",
                details={"error": str(exc), "path": path, "method": method},
            ) from exc

        if response.status_code >= 400:
            self._raise_api_error(response)

        payload = self._safe_json(response)
        if isinstance(payload, dict):
            return payload
        return {"data": payload}

    def _raise_api_error(self, response: httpx.Response) -> None:
        payload = self._safe_json(response)
        if response.status_code in (401, 403):
            raise CliError(
                "authentication failed. Check your API key.",
                exit_code=ExitCode.AUTH_ERROR,
                code="auth_failed",
                details={"status_code": response.status_code, "response": payload},
            )

        error_code = "validation_error" if response.status_code == 422 else "api_error"
        message = self._extract_error_message(payload) or f"API request failed ({response.status_code})."
        raise CliError(
            message,
            exit_code=ExitCode.API_ERROR,
            code=error_code,
            details={"status_code": response.status_code, "response": payload},
        )

    @staticmethod
    def _safe_json(response: httpx.Response) -> Any:
        try:
            return response.json()
        except ValueError:
            return {"text": response.text}

    @staticmethod
    def _extract_error_message(payload: Any) -> str | None:
        if isinstance(payload, dict):
            detail = payload.get("detail")
            if isinstance(detail, str) and detail.strip():
                return detail.strip()
            if isinstance(detail, list) and detail:
                first = detail[0]
                if isinstance(first, dict):
                    msg = first.get("msg")
                    if isinstance(msg, str) and msg.strip():
                        return msg.strip()
        return None
