"""LLM providers — Spec 029 T1 (Ollama) + 034 KIMI + Groq free tier."""
from __future__ import annotations

import json
import os
import re
from collections.abc import Iterator

import httpx

OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://10.10.10.123:11434").rstrip("/")
AGENTIC_LLM_MODEL = os.getenv("AGENTIC_LLM_MODEL", "qwen2.5:7b-instruct")
AGENTIC_EMBED_MODEL = os.getenv("AGENTIC_EMBED_MODEL", "nomic-embed-text")
AGENTIC_LLM_ENABLED = os.getenv("AGENTIC_LLM_ENABLED", "false").lower() in ("1", "true", "yes")
KIMI_API_KEY = os.getenv("KIMI_API_KEY", "").strip()
KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.moonshot.ai/v1").strip().rstrip("/")
KIMI_MODEL = os.getenv("KIMI_MODEL", "kimi-k2.5").strip()
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "").strip()
GROQ_BASE_URL = os.getenv("GROQ_BASE_URL", "https://api.groq.com/openai/v1").strip().rstrip("/")
GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.1-8b-instant").strip()
AGENTIC_LLM_PROVIDER = os.getenv("AGENTIC_LLM_PROVIDER", "auto").lower().strip()
AGENTIC_LLM_FALLBACK = os.getenv("AGENTIC_LLM_FALLBACK", "ollama").lower().strip()


def _clean_env(value: str) -> str:
    return re.split(r"\s+#", value or "", maxsplit=1)[0].strip()


def resolve_provider() -> str:
    if not AGENTIC_LLM_ENABLED:
        return "t0"
    provider = _clean_env(AGENTIC_LLM_PROVIDER).lower() or "auto"
    if provider == "auto":
        if GROQ_API_KEY:
            return "groq"
        if KIMI_API_KEY:
            return "kimi"
        return "ollama"
    return provider


def active_model() -> str:
    p = resolve_provider()
    if p == "groq":
        return _clean_env(GROQ_MODEL) or "llama-3.1-8b-instant"
    if p == "kimi":
        return _clean_env(KIMI_MODEL) or "kimi-k2.5"
    if p == "ollama":
        return AGENTIC_LLM_MODEL
    return "t0"


def llm_status() -> dict:
    provider = resolve_provider()
    return {
        "tier": "t1" if AGENTIC_LLM_ENABLED else "t0",
        "provider": provider,
        "model": active_model(),
        "groq_configured": bool(GROQ_API_KEY),
        "groq_base_url": _clean_env(GROQ_BASE_URL),
        "kimi_configured": bool(KIMI_API_KEY),
        "kimi_base_url": _clean_env(KIMI_BASE_URL),
        "ollama": ollama_available(),
        "ollama_url": OLLAMA_BASE_URL,
        "ollama_model": AGENTIC_LLM_MODEL,
        "embed_model": AGENTIC_EMBED_MODEL,
        "fallback": AGENTIC_LLM_FALLBACK,
    }


def ollama_available() -> bool:
    try:
        with httpx.Client(timeout=3.0) as c:
            return c.get(f"{OLLAMA_BASE_URL}/api/tags").status_code == 200
    except Exception:
        return False


def _openai_compatible_chat(
    *,
    base_url: str,
    api_key: str,
    model: str,
    messages: list[dict],
    max_tokens: int,
    timeout: float,
    prefix: str,
) -> tuple[str, str]:
    try:
        with httpx.Client(timeout=timeout) as c:
            r = c.post(
                f"{base_url.rstrip('/')}/chat/completions",
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "application/json",
                },
                json={
                    "model": model,
                    "messages": messages,
                    "temperature": 0.3,
                    "max_tokens": max_tokens,
                },
            )
        if r.status_code == 200:
            data = r.json()
            choice = (data.get("choices") or [{}])[0]
            msg = choice.get("message") or {}
            txt = (msg.get("content") or "").strip()
            if txt:
                return txt, model
        if r.status_code == 429:
            return ("", f"{prefix}-quota")
        return ("", f"{prefix}-error-{r.status_code}")
    except Exception:
        return ("", f"{prefix}-error")


def _groq_chat(messages: list[dict], *, max_tokens: int = 1200) -> tuple[str, str]:
    if not GROQ_API_KEY:
        return ("", "groq-unconfigured")
    model = _clean_env(GROQ_MODEL) or "llama-3.1-8b-instant"
    base = _clean_env(GROQ_BASE_URL) or "https://api.groq.com/openai/v1"
    return _openai_compatible_chat(
        base_url=base,
        api_key=GROQ_API_KEY,
        model=model,
        messages=messages,
        max_tokens=max_tokens,
        timeout=30.0,
        prefix="groq",
    )


def _kimi_chat(messages: list[dict], *, max_tokens: int = 1200) -> tuple[str, str]:
    if not KIMI_API_KEY:
        return ("", "kimi-unconfigured")
    model = _clean_env(KIMI_MODEL) or "kimi-k2.5"
    base = _clean_env(KIMI_BASE_URL) or "https://api.moonshot.ai/v1"
    return _openai_compatible_chat(
        base_url=base,
        api_key=KIMI_API_KEY,
        model=model,
        messages=messages,
        max_tokens=max_tokens,
        timeout=45.0,
        prefix="kimi",
    )


def _ollama_chat(messages: list[dict], *, max_tokens: int = 800) -> tuple[str, str]:
    if not ollama_available():
        return ("", "ollama-offline")
    try:
        with httpx.Client(timeout=120.0) as c:
            r = c.post(
                f"{OLLAMA_BASE_URL}/api/chat",
                json={"model": AGENTIC_LLM_MODEL, "messages": messages, "stream": False},
            )
        if r.status_code == 200:
            txt = (r.json().get("message") or {}).get("content", "").strip()
            if txt:
                return txt, AGENTIC_LLM_MODEL
    except Exception:
        pass
    return ("", "ollama-error")


def _try_fallback(messages: list[dict], *, max_tokens: int, skip: str) -> tuple[str, str]:
    order = []
    if AGENTIC_LLM_FALLBACK == "ollama" and skip != "ollama":
        order.append("ollama")
    if skip != "groq" and GROQ_API_KEY:
        order.append("groq")
    if skip != "kimi" and KIMI_API_KEY:
        order.append("kimi")
    for fb in order:
        if fb == "ollama":
            txt, model = _ollama_chat(messages, max_tokens=max_tokens)
        elif fb == "groq":
            txt, model = _groq_chat(messages, max_tokens=max_tokens)
        else:
            txt, model = _kimi_chat(messages, max_tokens=max_tokens)
        if txt:
            return txt, f"{fb}:{model}"
    return ("", "t0-fallback")


def _llm_chat(messages: list[dict], *, max_tokens: int = 2000) -> tuple[str, str]:
    if not AGENTIC_LLM_ENABLED:
        return ("", "t0")
    provider = resolve_provider()
    dispatch = {
        "groq": _groq_chat,
        "kimi": _kimi_chat,
        "ollama": _ollama_chat,
    }
    fn = dispatch.get(provider)
    if not fn:
        return ("", "t0")
    txt, model = fn(messages, max_tokens=max_tokens)
    if txt:
        return txt, f"{provider}:{model.split(':')[-1] if ':' in model else model}"
    if model.endswith("-quota"):
        return ("", model)
    return _try_fallback(messages, max_tokens=max_tokens, skip=provider)


def _openai_compatible_stream(
    *,
    base_url: str,
    api_key: str,
    model: str,
    messages: list[dict],
    max_tokens: int,
    timeout: float,
) -> Iterator[str]:
    try:
        with httpx.Client(timeout=timeout) as c:
            with c.stream(
                "POST",
                f"{base_url.rstrip('/')}/chat/completions",
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "application/json",
                },
                json={
                    "model": model,
                    "messages": messages,
                    "temperature": 0.5,
                    "max_tokens": max_tokens,
                    "stream": True,
                },
            ) as r:
                if r.status_code != 200:
                    return
                for line in r.iter_lines():
                    if not line or not line.startswith("data: "):
                        continue
                    payload = line[6:].strip()
                    if payload == "[DONE]":
                        break
                    try:
                        data = json.loads(payload)
                    except json.JSONDecodeError:
                        continue
                    delta = (data.get("choices") or [{}])[0].get("delta") or {}
                    txt = delta.get("content") or ""
                    if txt:
                        yield txt
    except Exception:
        return


def _groq_stream(messages: list[dict], *, max_tokens: int = 1200) -> Iterator[str]:
    if not GROQ_API_KEY:
        return
    model = _clean_env(GROQ_MODEL) or "llama-3.1-8b-instant"
    base = _clean_env(GROQ_BASE_URL) or "https://api.groq.com/openai/v1"
    yield from _openai_compatible_stream(
        base_url=base,
        api_key=GROQ_API_KEY,
        model=model,
        messages=messages,
        max_tokens=max_tokens,
        timeout=60.0,
    )


def _kimi_stream(messages: list[dict], *, max_tokens: int = 1200) -> Iterator[str]:
    if not KIMI_API_KEY:
        return
    model = _clean_env(KIMI_MODEL) or "kimi-k2.5"
    base = _clean_env(KIMI_BASE_URL) or "https://api.moonshot.ai/v1"
    yield from _openai_compatible_stream(
        base_url=base,
        api_key=KIMI_API_KEY,
        model=model,
        messages=messages,
        max_tokens=max_tokens,
        timeout=60.0,
    )


def _ollama_stream(messages: list[dict], *, max_tokens: int = 800) -> Iterator[str]:
    if not ollama_available():
        return
    try:
        with httpx.Client(timeout=120.0) as c:
            with c.stream(
                "POST",
                f"{OLLAMA_BASE_URL}/api/chat",
                json={"model": AGENTIC_LLM_MODEL, "messages": messages, "stream": True},
            ) as r:
                for line in r.iter_lines():
                    if not line:
                        continue
                    try:
                        data = json.loads(line)
                    except json.JSONDecodeError:
                        continue
                    chunk = (data.get("message") or {}).get("content") or ""
                    if chunk:
                        yield chunk
                    if data.get("done"):
                        break
    except Exception:
        return


def stream_llm_chat(messages: list[dict], *, max_tokens: int = 2000) -> tuple[Iterator[str], str]:
    """Returns (token iterator, provider label). Falls back to burst if stream empty."""
    if not AGENTIC_LLM_ENABLED:
        return iter(()), "t0"

    provider = resolve_provider()
    dispatch = {"groq": _groq_stream, "kimi": _kimi_stream, "ollama": _ollama_stream}
    fn = dispatch.get(provider)
    if not fn:
        return iter(()), "t0"

    def _gen():
        got = False
        for tok in fn(messages, max_tokens=max_tokens):
            got = True
            yield tok
        if not got:
            txt, model = _llm_chat(messages, max_tokens=max_tokens)
            if txt:
                yield txt

    label = f"{provider}:{active_model()}"
    return _gen(), label


def build_chat_messages(
    *,
    question: str,
    kb_snippets: list[str] | None = None,
    findings_summary: str | None = None,
    ops_context: str | None = None,
    user_role: str = "technician",
    target_agent: str = "A6",
    agent_name: str = "Copiloto",
    agent_role: str = "Assistência tickets e janela humana",
    history_messages: list[dict] | None = None,
) -> list[dict]:
    from app.agents.chat_context_builder import RESPONSE_FORMAT_INSTRUCTION

    ctx_block = ops_context or ""
    if not ctx_block:
        ctx_parts = [f"Operador: {user_role} (Ligbox Desk)"]
        if findings_summary:
            ctx_parts.append(f"Findings abertos:\n{findings_summary[:2000]}")
        if kb_snippets:
            ctx_parts.append("Base de conhecimento relevante:\n" + "\n---\n".join(kb_snippets[:6])[:4000])
        ctx_block = "\n\n".join(ctx_parts)

    system = (
        f"Você é **{agent_name}** ({target_agent}) — {agent_role}.\n"
        "Actua como **investigador DevOps sénior** no ecossistema Ligbox, não como FAQ genérico.\n\n"
        "## Missão\n"
        "Analisar o contexto operacional real, formular hipóteses fundamentadas e propor "
        "**acções concretas** que o operador possa executar agora.\n\n"
        "## Dados operacionais injectados\n"
        f"{ctx_block}\n\n"
        f"{RESPONSE_FORMAT_INSTRUCTION}"
    )
    messages: list[dict] = [{"role": "system", "content": system}]
    for hm in history_messages or []:
        role = hm.get("role")
        content = (hm.get("content") or "").strip()
        if role in ("user", "assistant") and content:
            messages.append({"role": role, "content": content})
    messages.append({
        "role": "user",
        "content": (
            f"Pergunta do operador ({user_role}):\n{question}\n\n"
            "Responda no formato investigativo obrigatório. "
            "Use os incidentes/findings/cenários do contexto quando relevantes."
        ),
    })
    return messages


def advise_human_action(
    *, finding_title: str, finding_detail: str, kb_snippets: list[str] | None = None
) -> tuple[str, str]:
    kb = "---".join(kb_snippets or [])[:2500] or "N/A"
    messages = [
        {
            "role": "system",
            "content": "Advisor Agentic Ops Ligbox. Português BR, máx 6 frases. O que fazer AGORA?",
        },
        {
            "role": "user",
            "content": f"Problema: {finding_title}\nDetalhe: {finding_detail}\nKB: {kb}",
        },
    ]
    txt, model = _llm_chat(messages, max_tokens=400)
    if txt:
        return txt, model
    return (f"Investigar manualmente: {finding_title}", "t0")


def chat_context(
    *,
    question: str,
    kb_snippets: list[str] | None = None,
    findings_summary: str | None = None,
    ops_context: str | None = None,
    user_role: str = "technician",
    target_agent: str = "A6",
    agent_name: str = "Copiloto",
    agent_role: str = "Assistência tickets e janela humana",
    history: str | None = None,
    history_messages: list[dict] | None = None,
) -> tuple[str, str]:
    # Legacy string history → single user turn if no structured history
    hist_msgs = list(history_messages or [])
    if not hist_msgs and history:
        hist_msgs = [{"role": "user", "content": f"[Histórico resumido]\n{history[:3000]}"}]

    messages = build_chat_messages(
        question=question,
        kb_snippets=kb_snippets,
        findings_summary=findings_summary,
        ops_context=ops_context,
        user_role=user_role,
        target_agent=target_agent,
        agent_name=agent_name,
        agent_role=agent_role,
        history_messages=hist_msgs,
    )
    txt, model = _llm_chat(messages, max_tokens=1200)
    if txt:
        return txt, model
    if model == "groq-quota":
        return (
            "Quota Groq esgotada (free tier). Tente amanhã ou use fallback Ollama local.",
            "groq-quota",
        )
    if model == "kimi-quota":
        return (
            "Conta KIMI sem saldo. Groq/Ollama disponíveis como alternativa.",
            "kimi-quota",
        )
    return (
        "Modo T0 activo — LLM indisponível. Consulte findings e audit log no painel Agentic Ops.",
        "t0",
    )
