from __future__ import annotations

import json
import os
from typing import Any

from pydantic import ValidationError

from .knowledge import ranked_candidates
from .prompts import SYSTEM_PROMPT
from .schemas import (
    ModelRecommendationSet,
    PlanRecommendationRequest,
    PlanningMode,
)


class LLMError(RuntimeError):
    pass


class LLMTimeout(LLMError):
    pass


class LLMNotConfigured(LLMError):
    pass


class LLMService:
    def __init__(self) -> None:
        mode = os.getenv("LLM_MODE", "mock").strip().lower()
        if mode not in {"mock", "real"}:
            raise ValueError("LLM_MODE must be 'mock' or 'real'")
        self.mode = mode
        self.timeout = float(os.getenv("LLM_TIMEOUT_SECONDS", "45"))

    @property
    def configured(self) -> bool:
        return bool(os.getenv("OPENAI_API_KEY") and os.getenv("OPENAI_MODEL"))

    @property
    def ready(self) -> bool:
        return self.mode == "mock" or self.configured

    async def recommend(
        self, request: PlanRecommendationRequest
    ) -> ModelRecommendationSet:
        if self.mode == "mock":
            return self._mock_recommend(request)
        return await self._real_recommend(request)

    async def _real_recommend(
        self, request: PlanRecommendationRequest
    ) -> ModelRecommendationSet:
        if not self.configured:
            raise LLMNotConfigured("真实模型未配置，请在服务端设置模型凭据")
        try:
            from openai import APITimeoutError, AsyncOpenAI
        except ImportError as exc:
            raise LLMError("服务端缺少 openai 依赖") from exc

        client = AsyncOpenAI(
            api_key=os.environ["OPENAI_API_KEY"],
            base_url=os.getenv("OPENAI_BASE_URL") or None,
            timeout=self.timeout,
        )
        payload = request.model_dump(by_alias=True, mode="json")
        feedback = ""
        allowed_ids = {candidate.id for candidate in request.candidates}

        for attempt in range(2):
            try:
                response = await client.chat.completions.create(
                    model=os.environ["OPENAI_MODEL"],
                    temperature=0.2,
                    response_format={"type": "json_object"},
                    messages=[
                        {"role": "system", "content": SYSTEM_PROMPT},
                        {
                            "role": "user",
                            "content": json.dumps(payload, ensure_ascii=False) + feedback,
                        },
                    ],
                )
            except APITimeoutError as exc:
                raise LLMTimeout("模型调用超时") from exc
            except Exception as exc:
                # Provider exceptions can include request metadata; do not echo them.
                raise LLMError("模型调用失败") from exc

            try:
                content = response.choices[0].message.content or "{}"
                generated = ModelRecommendationSet.model_validate_json(content)
                self._validate_ids(generated, allowed_ids)
                return generated
            except (ValidationError, ValueError, json.JSONDecodeError) as exc:
                if attempt == 1:
                    raise LLMError("模型输出未通过 POI 白名单校验") from exc
                feedback = (
                    "\n上次输出不符合契约。请只使用候选白名单 ID，"
                    "并重新输出完整 JSON。"
                )

        raise LLMError("模型未返回有效推荐")

    def _mock_recommend(
        self, request: PlanRecommendationRequest
    ) -> ModelRecommendationSet:
        candidates_by_id = {candidate.id: candidate for candidate in request.candidates}
        if request.mode == PlanningMode.CUSTOM:
            selected = [candidates_by_id[poi_id] for poi_id in request.selected_poi_ids]
        else:
            # This count is only an AI recommendation hint. The client remains
            # responsible for calculating transfers and final itinerary time.
            desired_count = max(1, min(8, request.duration_minutes // 75))
            selected = ranked_candidates(request)[:desired_count]

        return ModelRecommendationSet.model_validate(
            {
                "recommendations": [
                    {
                        "poiId": candidate.id,
                        "reason": self._mock_reason(candidate.name, candidate.tag_codes),
                        "order": index,
                    }
                    for index, candidate in enumerate(selected, start=1)
                ]
            }
        )

    @staticmethod
    def _mock_reason(name: str, tag_codes: list[str]) -> str:
        if tag_codes:
            return f"{name}与当前偏好标签较匹配，可作为路线候选。"
        return f"{name}的推荐指数较高，可作为路线候选。"

    @staticmethod
    def _validate_ids(
        generated: ModelRecommendationSet, allowed_ids: set[str]
    ) -> None:
        ids = [item.poi_id for item in generated.recommendations]
        unknown = set(ids) - allowed_ids
        if unknown:
            raise ValueError(f"unknown POI ids: {sorted(unknown)}")
        if len(ids) != len(set(ids)):
            raise ValueError("duplicate POI ids")

