from __future__ import annotations

from collections.abc import Mapping
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Literal

from openai import APIConnectionError, AuthenticationError, OpenAI, OpenAIError

from cli_demo.config import (
    get_effective_api_key,
    get_effective_base_url,
    get_effective_model,
    load_config,
    normalize_optional_value,
)


MessageRole = Literal["user", "assistant"]
InputMessage = dict[str, str]


@dataclass(frozen=True, slots=True)
class ChatMessage:
    role: MessageRole
    content: str


@dataclass(frozen=True, slots=True)
class ResolvedAISettings:
    api_key: str
    base_url: str | None
    model: str


class AIServiceError(RuntimeError):
    pass


class EmptyPromptError(AIServiceError):
    pass


class MissingAPIKeyError(AIServiceError):
    pass


class EmptyResponseError(AIServiceError):
    pass


class OpenAIRequestError(AIServiceError):
    pass


def ask_model(*, prompt: str, model: str | None, system_prompt: str | None = None) -> str:
    normalized_prompt = _normalize_required(prompt, field_name="prompt")
    normalized_system = normalize_optional_value(system_prompt, field_name="system")

    input_items = _build_input_messages(
        system_prompt=normalized_system,
        history=[],
        prompt=normalized_prompt,
    )
    return _request_output_text(input_items=input_items, model=model)


def chat_model(
    *,
    history: Sequence[ChatMessage],
    prompt: str,
    model: str | None,
    system_prompt: str | None = None,
) -> str:
    normalized_prompt = _normalize_required(prompt, field_name="prompt")
    normalized_system = normalize_optional_value(system_prompt, field_name="system")
    normalized_history = _normalize_history(history)

    input_items = _build_input_messages(
        system_prompt=normalized_system,
        history=normalized_history,
        prompt=normalized_prompt,
    )
    return _request_output_text(input_items=input_items, model=model)


def ping_model(*, model: str | None = None) -> str:
    input_items = [
        {
            "role": "user",
            "content": "Reply with OK.",
        }
    ]
    return _request_output_text(input_items=input_items, model=model, max_output_tokens=16)


def resolve_ai_settings(*, model: str | None = None) -> ResolvedAISettings:
    config = load_config()
    normalized_model = get_effective_model(config, override=model)
    api_key = get_effective_api_key(config)
    base_url = get_effective_base_url(config)

    if not api_key:
        raise MissingAPIKeyError(
            'API key is not configured. Run "cli-demo config set --api-key <your-api-key>" or set OPENAI_API_KEY.'
        )

    return ResolvedAISettings(
        api_key=api_key,
        base_url=base_url,
        model=normalized_model,
    )


def _request_output_text(
    *,
    input_items: Sequence[InputMessage],
    model: str | None,
    max_output_tokens: int | None = None,
) -> str:
    settings = resolve_ai_settings(model=model)
    client = _create_client(settings)
    create_kwargs: dict[str, object] = {
        "model": settings.model,
        "input": list(input_items),
    }
    if max_output_tokens is not None:
        create_kwargs["max_output_tokens"] = max_output_tokens

    try:
        response = client.responses.create(**create_kwargs)
    except AuthenticationError as exc:
        raise OpenAIRequestError(
            "OpenAI authentication failed. Check your configured api_key and try again."
        ) from exc
    except APIConnectionError as exc:
        raise OpenAIRequestError(
            "Could not reach the OpenAI API. Check your network connection or configured base_url."
        ) from exc
    except OpenAIError as exc:
        raise OpenAIRequestError(f"OpenAI API error: {exc}") from exc

    output_text = _extract_output_text(response, base_url=settings.base_url)
    if not output_text:
        raise EmptyResponseError("The model returned no printable text.")

    return output_text


def _create_client(settings: ResolvedAISettings) -> OpenAI:
    client_kwargs: dict[str, str] = {"api_key": settings.api_key}
    if settings.base_url:
        client_kwargs["base_url"] = settings.base_url
    client = OpenAI(**client_kwargs)
    return client


def _build_input_messages(
    *,
    system_prompt: str | None,
    history: Sequence[ChatMessage],
    prompt: str,
) -> list[InputMessage]:
    input_items: list[InputMessage] = []
    if system_prompt:
        input_items.append({"role": "system", "content": system_prompt})
    input_items.extend({"role": message.role, "content": message.content} for message in history)
    input_items.append({"role": "user", "content": prompt})
    return input_items


def _normalize_history(history: Sequence[ChatMessage]) -> list[ChatMessage]:
    normalized: list[ChatMessage] = []
    for message in history:
        normalized_content = _normalize_required(message.content, field_name=message.role)
        normalized.append(ChatMessage(role=message.role, content=normalized_content))
    return normalized


def _normalize_required(value: str, *, field_name: str) -> str:
    normalized = normalize_optional_value(value, field_name=field_name)
    if normalized is None:
        raise EmptyPromptError(f"{field_name} cannot be empty or whitespace.")
    return normalized


def _extract_output_text(response: object, *, base_url: str | None) -> str:
    if isinstance(response, str):
        snippet = response.strip()[:120].lower()
        if "<!doctype html" in snippet or "<html" in snippet:
            raise OpenAIRequestError(
                "The configured base_url returned an HTML page instead of an OpenAI API response. "
                "Check that base_url points to an OpenAI-compatible API endpoint, for example "
                '"https://api.openai.com/v1".'
            )
        raise OpenAIRequestError(
            "The configured base_url returned plain text instead of an OpenAI API response."
        )

    output_text = getattr(response, "output_text", None)
    if isinstance(output_text, str):
        return output_text.strip()

    if isinstance(response, Mapping):
        value = response.get("output_text")
        if isinstance(value, str):
            return value.strip()

    model_dump = getattr(response, "model_dump", None)
    if callable(model_dump):
        dumped = model_dump()
        if isinstance(dumped, Mapping):
            value = dumped.get("output_text")
            if isinstance(value, str):
                return value.strip()
            raise OpenAIRequestError(
                "The API response did not include output_text. Verify that your base_url is compatible "
                "with the OpenAI Responses API."
            )

    response_type = type(response).__name__
    raise OpenAIRequestError(
        f"Unsupported API response type: {response_type}. Verify that your base_url points to an "
        "OpenAI-compatible Responses API endpoint."
    )
