"""
Local LLM client and agent backend for Gar

로컬 LLM 엔드포인트를 OpenAI Chat Completions 호환 형태로 감싸고,
에이전트가 function calling(tool calling)을 사용할 수 있도록 추상화 제공
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Protocol

import httpx


def list_available_models(base_url: str) -> list[str]:
    """로컬 LLM 서버에서 사용 가능한 모델 목록을 조회

    Args:
        base_url: LLM 서버의 base URL

    Returns:
        사용 가능한 모델 ID 리스트

    Raises:
        RuntimeError: 서버 연결 실패 또는 응답 파싱 실패
    """
    try:
        client = httpx.Client(base_url=base_url, timeout=10.0)
        response = client.get("/models")
        response.raise_for_status()
        data = response.json()
        # OpenAI 호환 형식: {"data": [{"id": "model1"}, {"id": "model2"}]}
        if "data" in data:
            return [model.get("id") for model in data["data"] if model.get("id")]
        # 다른 형식: {"models": ["model1", "model2"]}
        if "models" in data:
            return data["models"]
        # 직접 리스트: ["model1", "model2"]
        if isinstance(data, list):
            return [m.get("id") if isinstance(m, dict) else str(m) for m in data]
        return []
    except httpx.RequestError as e:
        raise RuntimeError(f"LLM 서버 연결 실패: {e}") from e
    except Exception as e:
        raise RuntimeError(f"모델 목록 조회 실패: {e}") from e


def get_default_model(base_url: str) -> str:
    """사용 가능한 모델 중 첫 번째를 기본 모델로 반환

    Args:
        base_url: LLM 서버의 base URL

    Returns:
        기본 모델 이름

    Raises:
        RuntimeError: 모델 목록을 가져올 수 없거나 모델이 없는 경우
    """
    models = list_available_models(base_url)
    if not models:
        raise RuntimeError("사용 가능한 모델이 없습니다. LLM 서버를 확인하세요.")
    return models[0]


class LocalLLMClient:
    """로컬 LLM(OpenAI 호환) 엔드포인트 클라이언트

    /chat/completions 엔드포인트를 사용한다고 가정
    """

    def __init__(self, base_url: str, api_key: str | None = None, timeout: float = 180.0) -> None:
        self._client = httpx.Client(base_url=base_url, timeout=timeout)
        # 일부 서버는 Authorization 헤더를 무시하므로, 더미 키라도 허용
        self._api_key = api_key or "gar-local-key"

    def chat(
        self,
        *,
        model: str,
        messages: list[dict[str, str]],
        tools: list[dict[str, Any]] | None = None,
        temperature: float = 0.2,
        max_tokens: int = 1024,
        stream_callback: Callable[[str], None] | None = None,
    ) -> dict[str, Any]:
        payload: dict[str, Any] = {
            "model": model,
            "messages": messages,
            "temperature": temperature,
            "max_tokens": max_tokens,
        }
        if tools:
            payload["tools"] = tools
            payload["tool_choice"] = "auto"

        response = self._client.post(
            "/chat/completions",
            json=payload,
            headers={"Authorization": f"Bearer {self._api_key}"},
        )
        response.raise_for_status()
        return response.json()


@dataclass
class AgentStepResult:
    """단일 에이전트 스텝 결과"""

    messages: list[dict[str, str]]
    tool_calls: list[dict[str, Any]]
    is_final: bool


class AgentLLMBackend(Protocol):
    """에이전트가 의존하는 LLM 백엔드 추상화.

    구현체를 갈아끼우면 모델/엔드포인트를 쉽게 교체할 수 있습니다.
    """

    def run_step(
        self,
        *,
        messages: list[dict[str, str]],
        tools: list[dict[str, Any]],
        model: str,
        temperature: float,
        max_tokens: int,
        stream_callback: Callable[[str], None] | None = None,
    ) -> AgentStepResult: ...


class LocalOpenAIAgentBackend:
    """로컬 OpenAI 호환 서버를 사용하는 AgentLLMBackend 구현"""

    def __init__(self, client: LocalLLMClient | None = None, base_url: str | None = None) -> None:
        if client is None:
            if base_url is None:
                raise ValueError("base_url은 필수입니다. LLM 서버 주소를 설정해주세요.")
            client = LocalLLMClient(base_url=base_url)
        self._client = client

    def run_step(
        self,
        *,
        messages: list[dict[str, str]],
        tools: list[dict[str, Any]],
        model: str,
        temperature: float,
        max_tokens: int,
        stream_callback: Callable[[str], None] | None = None,
    ) -> AgentStepResult:
        try:
            response = self._client.chat(
                model=model,
                messages=messages,
                tools=tools,
                temperature=temperature,
                max_tokens=max_tokens,
                stream_callback=stream_callback,
            )
        except httpx.HTTPStatusError as e:
            raise RuntimeError(f"LLM API 호출 실패: {e.response.status_code} - {e.response.text}") from e
        except httpx.RequestError as e:
            raise RuntimeError(f"LLM 서버 연결 실패: {e}") from e

        choices = response.get("choices", [])
        if not choices:
            raise RuntimeError("LLM 응답에 choices가 없습니다.")

        choice = choices[0].get("message", {})
        content = choice.get("content")
        tool_calls = choice.get("tool_calls") or []
        
        # 디버깅: LLM 응답 로깅
        import logging
        logger = logging.getLogger(__name__)
        logger.debug(f"LLM 응답 - content: {content[:100] if content else None}, tool_calls: {len(tool_calls)}개")
        if tool_calls:
            for tc in tool_calls:
                func_name = tc.get("function", {}).get("name", "unknown")
                logger.debug(f"  - tool_call: {func_name}")

        new_messages = list(messages)
        if content:
            new_messages.append({"role": "assistant", "content": content})

        # tool_calls가 있다면 아직 최종 응답이 아님
        is_final = not tool_calls

        return AgentStepResult(messages=new_messages, tool_calls=tool_calls, is_final=is_final)

