"""LLM client abstraction — swap providers without touching business logic.

Provider is configured via CLASSIFIER_PROVIDER env var (default: "gemini").
API keys are loaded internally via config.load_api_key().

Usage:
    llm = create_client()              # uses CLASSIFIER_PROVIDER from config
    llm = create_client("groq")        # explicit override
    response = await llm.call("prompt", "text", max_tokens=300)
"""

import logging
import os
import sys
import time
from abc import ABC, abstractmethod

import httpx

from neo_cortex import config

logger = logging.getLogger(__name__)

_VERIFY_SSL = sys.platform != "win32"

_DATA_DIR = os.environ.get("CORTEX_DATA_DIR") or os.getcwd()
_LOG_DIR = os.path.join(_DATA_DIR, "cortex_db") if os.environ.get("CORTEX_DATA_DIR") else _DATA_DIR
_LOG_PATH = os.path.join(_LOG_DIR, "neo-cortex.log")


def _bl(msg: str) -> None:
    try:
        with open(_LOG_PATH, "a", encoding="utf-8") as f:
            f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} [LLM] {msg}\n")
            f.flush()
    except Exception:
        pass


class LLMClient(ABC):
    """Abstract LLM client — one method: call(prompt, text) -> str."""

    @abstractmethod
    async def call(
        self, prompt: str, text: str, max_tokens: int = 250, temperature: float = 0.1
    ) -> str: ...

    @abstractmethod
    async def close(self) -> None: ...

    @property
    @abstractmethod
    def name(self) -> str:
        """Provider name for logging."""
        ...


class GeminiClient(LLMClient):
    """Gemini 2.0 Flash — best for conflict resolution and MBEL formatting."""

    def __init__(self, api_key: str):
        self._api_key = api_key
        self._client = httpx.AsyncClient(timeout=30.0, verify=_VERIFY_SSL)
        _bl(f"GeminiClient init: model={config.GEMINI_MODEL}")

    @property
    def name(self) -> str:
        return f"gemini/{config.GEMINI_MODEL}"

    async def call(
        self, prompt: str, text: str, max_tokens: int = 250, temperature: float = 0.1
    ) -> str:
        url = f"{config.GEMINI_URL}/models/{config.GEMINI_MODEL}:generateContent?key={self._api_key}"
        resp = await self._client.post(
            url,
            json={
                "contents": [{"parts": [{"text": f'{prompt}"{text}"'}]}],
                "generationConfig": {
                    "temperature": temperature,
                    "maxOutputTokens": max_tokens,
                },
            },
        )
        resp.raise_for_status()
        data = resp.json()
        return data["candidates"][0]["content"]["parts"][0]["text"].strip()

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


class GroqClient(LLMClient):
    """Groq/llama-3.3-70b — fast but prone to slot contamination on conflict resolution."""

    def __init__(self, api_key: str):
        self._api_key = api_key
        self._client = httpx.AsyncClient(timeout=15.0, verify=_VERIFY_SSL)
        _bl(f"GroqClient init: model={config.GROQ_MODEL}")

    @property
    def name(self) -> str:
        return f"groq/{config.GROQ_MODEL}"

    async def call(
        self, prompt: str, text: str, max_tokens: int = 250, temperature: float = 0.1
    ) -> str:
        resp = await self._client.post(
            config.GROQ_URL,
            headers={
                "Authorization": f"Bearer {self._api_key}",
                "Content-Type": "application/json",
            },
            json={
                "model": config.GROQ_MODEL,
                "messages": [{"role": "user", "content": f'{prompt}"{text}"'}],
                "temperature": temperature,
                "max_tokens": max_tokens,
            },
        )
        resp.raise_for_status()
        data = resp.json()
        return data["choices"][0]["message"]["content"].strip()

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


_PROVIDERS = {
    "gemini": lambda: GeminiClient(config.load_api_key("gemini")),
    "groq": lambda: GroqClient(config.load_api_key("groq")),
}


def create_client(provider: str | None = None) -> LLMClient:
    """Create an LLM client based on config or explicit provider name.

    Provider is resolved from:
    1. Explicit `provider` argument
    2. CLASSIFIER_PROVIDER env var
    3. Default: "gemini"
    """
    name = provider or config.CLASSIFIER_PROVIDER
    factory = _PROVIDERS.get(name)
    if not factory:
        raise ValueError(f"Unknown LLM provider '{name}'. Available: {list(_PROVIDERS.keys())}")
    client = factory()
    _bl(f"create_client: provider={name} → {client.name}")
    return client
