from __future__ import annotations

import os
from typing import Any, Mapping, Sequence

import httpx

from .errors import (
    APIConnectionError,
    APIError,
    ConfigurationError,
    build_api_error,
)
from .types import (
    IngestResponse,
    ModelInfo,
    SessionDeleteResult,
    SessionInfo,
    TSEMessageResponse,
    ingest_from_payload,
    message_from_payload,
    model_from_payload,
    session_delete_from_payload,
    session_from_payload,
)

DEFAULT_MODEL = "artasupport-v1"
API_KEY_ENV = "ARTASUPPORT_API_KEY"
BASE_URL_ENV = "ARTASUPPORT_BASE_URL"


MessageInput = Mapping[str, Any]


def _resolve_api_key(api_key: str | None) -> str:
    candidate = (api_key or os.getenv(API_KEY_ENV) or "").strip()
    if not candidate:
        raise ConfigurationError(
            f"Missing API key. Pass api_key=... or set {API_KEY_ENV}."
        )
    return candidate


def _resolve_base_url(base_url: str | None) -> str:
    candidate = (base_url or os.getenv(BASE_URL_ENV) or "").strip()
    if not candidate:
        raise ConfigurationError(
            f"Missing base URL. Pass base_url=... or set {BASE_URL_ENV}."
        )
    return candidate.rstrip("/")


def _build_auth_headers(api_key: str, api_version: str | None = None) -> dict[str, str]:
    headers = {
        "x-api-key": api_key,
        "Authorization": f"Bearer {api_key}",
    }
    if api_version:
        headers["artasupport-version"] = api_version
    return headers


def _normalize_messages(message: str | Sequence[MessageInput]) -> list[dict[str, Any]]:
    if isinstance(message, str):
        text = message.strip()
        if not text:
            raise ValueError("Message text cannot be empty.")
        return [{"role": "user", "content": text}]

    if not message:
        raise ValueError("messages must include at least one item.")

    normalized: list[dict[str, Any]] = []
    allowed_roles = {"user", "assistant", "system"}
    for item in message:
        if not isinstance(item, Mapping):
            raise TypeError("Each message must be a mapping with role/content.")

        role = item.get("role")
        content = item.get("content")
        if not isinstance(role, str) or role not in allowed_roles:
            raise ValueError("Each message role must be one of: user, assistant, system.")
        if content is None:
            raise ValueError("Each message must include content.")

        normalized.append({"role": role, "content": content})

    return normalized


def _resolve_user_input_or_messages(
    *,
    user_input: str | None = None,
    messages: Sequence[MessageInput] | None = None,
) -> str | Sequence[MessageInput]:
    if user_input is None and messages is None:
        raise ValueError("Provide one of: user_input or messages.")

    if user_input is not None and messages is not None:
        raise ValueError("Provide exactly one input argument among 'user_input' or 'messages'.")

    if user_input is not None:
        cleaned = user_input.strip()
        if not cleaned:
            raise ValueError("user_input cannot be empty.")
        return cleaned

    # messages path (non-None here due to prior checks)
    assert messages is not None
    return messages


def _parse_error_from_response(response: httpx.Response) -> tuple[str, str | None, str | None, Any, Any]:
    default_message = response.text.strip() or "Request failed."

    try:
        payload = response.json()
    except ValueError:
        return default_message, None, None, None, default_message

    if not isinstance(payload, Mapping):
        return default_message, None, None, None, payload

    detail = payload.get("detail")
    if isinstance(detail, Mapping):
        message = str(detail.get("message") or "Request failed.")
        error_code = str(detail.get("error")) if detail.get("error") is not None else None
        request_id = (
            str(detail.get("request_id")) if detail.get("request_id") is not None else None
        )
        details = detail.get("details")
        return message, error_code, request_id, details, payload

    if isinstance(detail, str):
        return detail, None, None, None, payload

    return default_message, None, None, None, payload


class ArtaSupportMessage:
    def __init__(
        self,
        *,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float = 120.0,
        api_version: str | None = None,
        default_model: str = DEFAULT_MODEL,
        client: httpx.Client | None = None,
    ):
        self.api_key = _resolve_api_key(api_key)
        self.base_url = _resolve_base_url(base_url)
        self.api_version = api_version
        self.default_model = default_model
        self._owns_client = client is None
        self._client = client or httpx.Client(base_url=self.base_url, timeout=timeout)

    def __enter__(self) -> ArtaSupportMessage:
        return self

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

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

    def _request(
        self,
        method: str,
        path: str,
        *,
        json_body: Mapping[str, Any] | None = None,
    ) -> Mapping[str, Any]:
        headers = _build_auth_headers(self.api_key, self.api_version)
        try:
            response = self._client.request(
                method=method,
                url=path,
                headers=headers,
                json=json_body,
            )
        except httpx.HTTPError as exc:
            raise APIConnectionError(f"Failed to connect to ArtaSupport API: {exc}") from exc

        if response.status_code >= 400:
            message, error_code, request_id, details, raw = _parse_error_from_response(response)
            raise build_api_error(
                status_code=response.status_code,
                message=message,
                error_code=error_code,
                request_id=request_id,
                details=details,
                raw=raw,
            )

        if response.status_code == 204 or not response.content:
            return {}

        try:
            payload = response.json()
        except ValueError as exc:
            raise APIError(
                "API returned a non-JSON success response.",
                status_code=response.status_code,
                raw=response.text,
            ) from exc

        if not isinstance(payload, Mapping):
            raise APIError(
                "API returned an unexpected response shape.",
                status_code=response.status_code,
                raw=payload,
            )

        return payload

    def _create_message(
        self,
        *,
        message: str | Sequence[MessageInput],
        mode: str,
        model: str | None = None,
        session_id: str | None = None,
        max_tokens: int = 1024,
        temperature: float | None = 0.3,
    ) -> TSEMessageResponse:
        payload: dict[str, Any] = {
            "model": model or self.default_model,
            "messages": _normalize_messages(message),
            "mode": mode,
            "max_tokens": int(max_tokens),
        }
        if session_id:
            payload["session_id"] = session_id
        if temperature is not None:
            payload["temperature"] = float(temperature)

        response_payload = self._request("POST", "/api/tse", json_body=payload)
        return message_from_payload(response_payload)

    def tse_copilot(
        self,
        *,
        user_input: str | None = None,
        messages: Sequence[MessageInput] | None = None,
        model: str | None = None,
        session_id: str | None = None,
        max_tokens: int = 1024,
        temperature: float | None = 0.3,
    ) -> TSEMessageResponse:
        resolved_message = _resolve_user_input_or_messages(
            user_input=user_input,
            messages=messages,
        )
        return self._create_message(
            message=resolved_message,
            mode="copilot",
            model=model,
            session_id=session_id,
            max_tokens=max_tokens,
            temperature=temperature,
        )

    def tse_batch(
        self,
        *,
        user_input: str | None = None,
        messages: Sequence[MessageInput] | None = None,
        model: str | None = None,
        session_id: str | None = None,
        max_tokens: int = 1024,
        temperature: float | None = 0.3,
    ) -> TSEMessageResponse:
        resolved_message = _resolve_user_input_or_messages(
            user_input=user_input,
            messages=messages,
        )
        return self._create_message(
            message=resolved_message,
            mode="batch",
            model=model,
            session_id=session_id,
            max_tokens=max_tokens,
            temperature=temperature,
        )

    def list_models(self) -> list[ModelInfo]:
        payload = self._request("GET", "/v1/models")
        data = payload.get("data")
        if not isinstance(data, list):
            raise APIError("Models response missing 'data' list.", raw=payload)
        return [model_from_payload(item) for item in data if isinstance(item, Mapping)]

    def create_session(self) -> SessionInfo:
        payload = self._request("POST", "/v1/sessions")
        return session_from_payload(payload)

    def delete_session(self, session_id: str) -> SessionDeleteResult:
        normalized = (session_id or "").strip()
        if not normalized:
            raise ValueError("session_id is required.")
        payload = self._request("DELETE", f"/v1/sessions/{normalized}")
        return session_delete_from_payload(payload)

    def tse_ingest(self, file_path: str) -> IngestResponse:
        """
        Upload a PDF or Excel file for ingestion.

        Args:
            file_path: Path to the file to upload (.pdf, .xlsx, or .xls)

        Returns:
            IngestResponse with the ingestion status

        Raises:
            ValueError: If file_path is empty or file doesn't exist
            APIError: If the API returns an error
        """
        from pathlib import Path

        normalized = (file_path or "").strip()
        if not normalized:
            raise ValueError("file_path is required.")

        path = Path(normalized)
        if not path.exists():
            raise ValueError(f"File not found: {normalized}")

        if not path.is_file():
            raise ValueError(f"Path is not a file: {normalized}")

        filename = path.name
        headers = _build_auth_headers(self.api_key, self.api_version)

        try:
            with open(path, "rb") as f:
                files = {"file": (filename, f, "application/octet-stream")}
                response = self._client.request(
                    method="POST",
                    url="/api/ingest",
                    headers=headers,
                    files=files,
                )
        except httpx.HTTPError as exc:
            raise APIConnectionError(f"Failed to connect to ArtaSupport API: {exc}") from exc

        if response.status_code >= 400:
            message, error_code, request_id, details, raw = _parse_error_from_response(response)
            raise build_api_error(
                status_code=response.status_code,
                message=message,
                error_code=error_code,
                request_id=request_id,
                details=details,
                raw=raw,
            )

        try:
            payload = response.json()
        except ValueError as exc:
            raise APIError(
                "API returned a non-JSON success response.",
                status_code=response.status_code,
                raw=response.text,
            ) from exc

        if not isinstance(payload, Mapping):
            raise APIError(
                "API returned an unexpected response shape.",
                status_code=response.status_code,
                raw=payload,
            )

        return ingest_from_payload(payload)


class AsyncArtaSupport:
    def __init__(
        self,
        *,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float = 120.0,
        api_version: str | None = None,
        default_model: str = DEFAULT_MODEL,
        client: httpx.AsyncClient | None = None,
    ):
        self.api_key = _resolve_api_key(api_key)
        self.base_url = _resolve_base_url(base_url)
        self.api_version = api_version
        self.default_model = default_model
        self._owns_client = client is None
        self._client = client or httpx.AsyncClient(base_url=self.base_url, timeout=timeout)

    async def __aenter__(self) -> AsyncArtaSupport:
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:
        await self.aclose()

    async def aclose(self) -> None:
        if self._owns_client:
            await self._client.aclose()

    async def _request(
        self,
        method: str,
        path: str,
        *,
        json_body: Mapping[str, Any] | None = None,
    ) -> Mapping[str, Any]:
        headers = _build_auth_headers(self.api_key, self.api_version)
        try:
            response = await self._client.request(
                method=method,
                url=path,
                headers=headers,
                json=json_body,
            )
        except httpx.HTTPError as exc:
            raise APIConnectionError(f"Failed to connect to ArtaSupport API: {exc}") from exc

        if response.status_code >= 400:
            message, error_code, request_id, details, raw = _parse_error_from_response(response)
            raise build_api_error(
                status_code=response.status_code,
                message=message,
                error_code=error_code,
                request_id=request_id,
                details=details,
                raw=raw,
            )

        if response.status_code == 204 or not response.content:
            return {}

        try:
            payload = response.json()
        except ValueError as exc:
            raise APIError(
                "API returned a non-JSON success response.",
                status_code=response.status_code,
                raw=response.text,
            ) from exc

        if not isinstance(payload, Mapping):
            raise APIError(
                "API returned an unexpected response shape.",
                status_code=response.status_code,
                raw=payload,
            )

        return payload

    async def _create_message(
        self,
        *,
        message: str | Sequence[MessageInput],
        mode: str,
        model: str | None = None,
        session_id: str | None = None,
        max_tokens: int = 1024,
        temperature: float | None = 0.3,
    ) -> TSEMessageResponse:
        payload: dict[str, Any] = {
            "model": model or self.default_model,
            "messages": _normalize_messages(message),
            "mode": mode,
            "max_tokens": int(max_tokens),
        }
        if session_id:
            payload["session_id"] = session_id
        if temperature is not None:
            payload["temperature"] = float(temperature)

        response_payload = await self._request("POST", "/api/tse", json_body=payload)
        return message_from_payload(response_payload)

    async def tse_copilot(
        self,
        *,
        user_input: str | None = None,
        messages: Sequence[MessageInput] | None = None,
        model: str | None = None,
        session_id: str | None = None,
        max_tokens: int = 1024,
        temperature: float | None = 0.3,
    ) -> TSEMessageResponse:
        resolved_message = _resolve_user_input_or_messages(
            user_input=user_input,
            messages=messages,
        )
        return await self._create_message(
            message=resolved_message,
            mode="copilot",
            model=model,
            session_id=session_id,
            max_tokens=max_tokens,
            temperature=temperature,
        )

    async def tse_batch(
        self,
        *,
        user_input: str | None = None,
        messages: Sequence[MessageInput] | None = None,
        model: str | None = None,
        session_id: str | None = None,
        max_tokens: int = 1024,
        temperature: float | None = 0.3,
    ) -> TSEMessageResponse:
        resolved_message = _resolve_user_input_or_messages(
            user_input=user_input,
            messages=messages,
        )
        return await self._create_message(
            message=resolved_message,
            mode="batch",
            model=model,
            session_id=session_id,
            max_tokens=max_tokens,
            temperature=temperature,
        )

    async def list_models(self) -> list[ModelInfo]:
        payload = await self._request("GET", "/v1/models")
        data = payload.get("data")
        if not isinstance(data, list):
            raise APIError("Models response missing 'data' list.", raw=payload)
        return [model_from_payload(item) for item in data if isinstance(item, Mapping)]

    async def create_session(self) -> SessionInfo:
        payload = await self._request("POST", "/v1/sessions")
        return session_from_payload(payload)

    async def delete_session(self, session_id: str) -> SessionDeleteResult:
        normalized = (session_id or "").strip()
        if not normalized:
            raise ValueError("session_id is required.")
        payload = await self._request("DELETE", f"/v1/sessions/{normalized}")
        return session_delete_from_payload(payload)

    async def tse_ingest(self, file_path: str) -> IngestResponse:
        """
        Upload a PDF or Excel file for ingestion.

        Args:
            file_path: Path to the file to upload (.pdf, .xlsx, or .xls)

        Returns:
            IngestResponse with the ingestion status

        Raises:
            ValueError: If file_path is empty or file doesn't exist
            APIError: If the API returns an error
        """
        from pathlib import Path

        normalized = (file_path or "").strip()
        if not normalized:
            raise ValueError("file_path is required.")

        path = Path(normalized)
        if not path.exists():
            raise ValueError(f"File not found: {normalized}")

        if not path.is_file():
            raise ValueError(f"Path is not a file: {normalized}")

        filename = path.name
        headers = _build_auth_headers(self.api_key, self.api_version)

        try:
            with open(path, "rb") as f:
                files = {"file": (filename, f, "application/octet-stream")}
                response = await self._client.request(
                    method="POST",
                    url="/api/ingest",
                    headers=headers,
                    files=files,
                )
        except httpx.HTTPError as exc:
            raise APIConnectionError(f"Failed to connect to ArtaSupport API: {exc}") from exc

        if response.status_code >= 400:
            message, error_code, request_id, details, raw = _parse_error_from_response(response)
            raise build_api_error(
                status_code=response.status_code,
                message=message,
                error_code=error_code,
                request_id=request_id,
                details=details,
                raw=raw,
            )

        try:
            payload = response.json()
        except ValueError as exc:
            raise APIError(
                "API returned a non-JSON success response.",
                status_code=response.status_code,
                raw=response.text,
            ) from exc

        if not isinstance(payload, Mapping):
            raise APIError(
                "API returned an unexpected response shape.",
                status_code=response.status_code,
                raw=payload,
            )

        return ingest_from_payload(payload)


# User-facing async alias.
AsyncArtaSupportMessage = AsyncArtaSupport


def tse_copilot(
    *,
    user_input: str | None = None,
    messages: Sequence[MessageInput] | None = None,
    api_key: str | None = None,
    base_url: str | None = None,
    timeout: float = 120.0,
    api_version: str | None = None,
    model: str = DEFAULT_MODEL,
    session_id: str | None = None,
    max_tokens: int = 1024,
    temperature: float | None = 0.3,
) -> TSEMessageResponse:
    with ArtaSupportMessage(
        api_key=api_key,
        base_url=base_url,
        timeout=timeout,
        api_version=api_version,
        default_model=model,
    ) as client:
        return client.tse_copilot(
            user_input=user_input,
            messages=messages,
            model=model,
            session_id=session_id,
            max_tokens=max_tokens,
            temperature=temperature,
        )


def tse_batch(
    *,
    user_input: str | None = None,
    messages: Sequence[MessageInput] | None = None,
    api_key: str | None = None,
    base_url: str | None = None,
    timeout: float = 120.0,
    api_version: str | None = None,
    model: str = DEFAULT_MODEL,
    session_id: str | None = None,
    max_tokens: int = 1024,
    temperature: float | None = 0.3,
) -> TSEMessageResponse:
    with ArtaSupportMessage(
        api_key=api_key,
        base_url=base_url,
        timeout=timeout,
        api_version=api_version,
        default_model=model,
    ) as client:
        return client.tse_batch(
            user_input=user_input,
            messages=messages,
            model=model,
            session_id=session_id,
            max_tokens=max_tokens,
            temperature=temperature,
        )


def tse_ingest(
    file_path: str,
    *,
    api_key: str | None = None,
    base_url: str | None = None,
    timeout: float = 300.0,
    api_version: str | None = None,
) -> IngestResponse:
    """
    Upload a PDF or Excel file for ingestion.

    Args:
        file_path: Path to the file to upload (.pdf, .xlsx, or .xls)
        api_key: API key (or use ARTASUPPORT_API_KEY env var)
        base_url: Base URL (or use ARTASUPPORT_BASE_URL env var)
        timeout: Request timeout in seconds (default: 300)
        api_version: API version header

    Returns:
        IngestResponse with the ingestion status

    Raises:
        ValueError: If file_path is empty or file doesn't exist
        ConfigurationError: If API key or base URL is missing
        APIError: If the API returns an error

    Example:
        >>> from artasupport import tse_ingest
        >>> response = tse_ingest("runbook.pdf")
        >>> print(response.status)  # "completed"
    """
    with ArtaSupportMessage(
        api_key=api_key,
        base_url=base_url,
        timeout=timeout,
        api_version=api_version,
    ) as client:
        return client.tse_ingest(file_path)
