from __future__ import annotations

import logging
import os
import sys
import time
from typing import TypeVar
from uuid import uuid4

import httpx
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel

load_dotenv()

T = TypeVar("T", bound=BaseModel)

LOGGER = logging.getLogger("resume_agent.model")


def _configure_logger() -> None:
    if not LOGGER.handlers:
        handler = logging.StreamHandler(sys.stdout)
        handler.setFormatter(
            logging.Formatter(
                "%(asctime)s %(levelname)s %(name)s %(message)s",
                datefmt="%Y-%m-%dT%H:%M:%S%z",
            )
        )
        LOGGER.addHandler(handler)
    level_name = os.getenv("RESUME_AGENT_LOG_LEVEL", "INFO").upper()
    LOGGER.setLevel(getattr(logging, level_name, logging.INFO))
    LOGGER.propagate = False


def _env_flag(name: str, default: bool = False) -> bool:
    value = os.getenv(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


_configure_logger()


class LLMError(RuntimeError):
    pass


class OpenAILLM:
    def __init__(self, model: str | None = None) -> None:
        api_key = os.getenv("RESUME_AGENT_API_KEY") or os.getenv("OPENAI_API_KEY")
        if not api_key:
            raise LLMError("Set RESUME_AGENT_API_KEY (or OPENAI_API_KEY), then rerun the command.")
        base_url = os.getenv("RESUME_AGENT_BASE_URL") or os.getenv("OPENAI_BASE_URL")
        configured_model = model or os.getenv("RESUME_AGENT_MODEL")
        self.model = configured_model or (None if base_url else "gpt-5.6-terra")
        self.api_style = os.getenv(
            "RESUME_AGENT_API_STYLE", "chat" if base_url else "responses"
        ).lower()
        if self.api_style not in {"chat", "responses"}:
            raise LLMError("RESUME_AGENT_API_STYLE must be either 'chat' or 'responses'.")
        try:
            self.timeout_seconds = float(
                os.getenv("RESUME_AGENT_LLM_TIMEOUT_SECONDS", "1800")
            )
        except ValueError as exc:
            raise LLMError("RESUME_AGENT_LLM_TIMEOUT_SECONDS must be a number.") from exc
        if self.timeout_seconds <= 0:
            raise LLMError("RESUME_AGENT_LLM_TIMEOUT_SECONDS must be greater than zero.")

        timeout = httpx.Timeout(
            timeout=self.timeout_seconds,
            connect=min(30.0, self.timeout_seconds),
        )
        self.client = OpenAI(api_key=api_key, base_url=base_url, timeout=timeout)
        self.log_payloads = _env_flag("RESUME_AGENT_LOG_MODEL_PAYLOADS", default=True)

    def parse(self, schema: type[T], instructions: str, input_text: str) -> T:
        if not self.model:
            raise LLMError(
                "RESUME_AGENT_MODEL is not set. Run `resume-agent models`, then add "
                "the selected model ID to .env."
            )
        query_id = uuid4().hex[:10]
        started = time.perf_counter()
        LOGGER.info(
            "query=%s event=start endpoint=%s model=%s api_style=%s schema=%s timeout_seconds=%g "
            "instructions_chars=%d input_chars=%d",
            query_id,
            self.client.base_url,
            self.model,
            self.api_style,
            schema.__name__,
            self.timeout_seconds,
            len(instructions),
            len(input_text),
        )
        if self.log_payloads:
            LOGGER.info(
                "query=%s event=payload\n"
                "----- SYSTEM INSTRUCTIONS -----\n%s\n"
                "----- USER INPUT -----\n%s\n"
                "----- END MODEL QUERY -----",
                query_id,
                instructions,
                input_text,
            )
        try:
            if self.api_style == "responses":
                response = self.client.responses.parse(
                    model=self.model,
                    instructions=instructions,
                    input=input_text,
                    text_format=schema,
                    reasoning={"effort": "medium"},
                )
                parsed = response.output_parsed
            else:
                completion = self.client.chat.completions.parse(
                    model=self.model,
                    messages=[
                        {"role": "system", "content": instructions},
                        {"role": "user", "content": input_text},
                    ],
                    response_format=schema,
                )
                parsed = completion.choices[0].message.parsed
            if parsed is None:
                raise LLMError("The model did not return a structured result.")
        except Exception as exc:
            elapsed = time.perf_counter() - started
            LOGGER.exception(
                "query=%s event=error elapsed_seconds=%.3f error_type=%s",
                query_id,
                elapsed,
                type(exc).__name__,
            )
            if isinstance(exc, LLMError):
                raise
            raise LLMError(
                f"Model query {query_id} failed: {type(exc).__name__}: {exc}"
            ) from exc

        elapsed = time.perf_counter() - started
        LOGGER.info(
            "query=%s event=success elapsed_seconds=%.3f schema=%s",
            query_id,
            elapsed,
            schema.__name__,
        )
        if self.log_payloads:
            LOGGER.info(
                "query=%s event=parsed_response\n%s\n----- END MODEL RESPONSE -----",
                query_id,
                parsed.model_dump_json(indent=2),
            )
        return parsed

    def list_models(self) -> list[str]:
        query_id = uuid4().hex[:10]
        started = time.perf_counter()
        LOGGER.info(
            "query=%s event=models_start endpoint=%s timeout_seconds=%g",
            query_id,
            self.client.base_url,
            self.timeout_seconds,
        )
        try:
            model_ids = sorted(model.id for model in self.client.models.list().data)
        except Exception as exc:
            LOGGER.exception(
                "query=%s event=models_error elapsed_seconds=%.3f error_type=%s",
                query_id,
                time.perf_counter() - started,
                type(exc).__name__,
            )
            raise LLMError(
                f"Model-list query {query_id} failed: {type(exc).__name__}: {exc}"
            ) from exc
        LOGGER.info(
            "query=%s event=models_success elapsed_seconds=%.3f model_count=%d",
            query_id,
            time.perf_counter() - started,
            len(model_ids),
        )
        return model_ids
