import json
import re
from typing import Any, Dict, Tuple

import httpx

from app.action_prompt import build_action_system_prompt, build_action_user_prompt
from app.action_catalog import TRIAGE_ACTION_CODES
from app.action_mapper import ACTION_CODE_ALIASES
from app.config import settings
from app.schemas import ActionDecision, AnalyzeRequest, UsageInfo


STRUCTURED_REVIEW_THRESHOLD = 0.80


def extract_first_json_object(raw: str) -> str:
    """Extrai o primeiro objeto JSON de uma resposta LLM."""
    s = str(raw or "").strip()

    if s.startswith("```"):
        lines = s.splitlines()
        if lines and lines[0].strip().startswith("```"):
            lines = lines[1:]
        if lines and lines[-1].strip().startswith("```"):
            lines = lines[:-1]
        s = "\n".join(lines).strip()

    start = s.find("{")
    if start == -1:
        raise ValueError(f"no JSON object found: {s[:300]}")

    in_string = False
    escaped = False
    depth = 0

    for i in range(start, len(s)):
        ch = s[i]

        if escaped:
            escaped = False
            continue

        if ch == "\\":
            escaped = True
            continue

        if ch == '"':
            in_string = not in_string
            continue

        if in_string:
            continue

        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                return s[start:i + 1]

    raise ValueError(f"incomplete JSON object: {s[:500]}")


def fallback_parse_decision_text(raw: str) -> Dict[str, Any]:
    """Fallback técnico: só extrai campos explícitos, não inventa decisão."""
    s = str(raw or "")

    action_match = re.search(
        r'["\']action_code["\']\s*:\s*["\']([A-Z0-9_]+)["\']',
        s,
        flags=re.I,
    )
    confidence_match = re.search(
        r'["\']confidence["\']\s*:\s*([0-9]+(?:\.[0-9]+)?)',
        s,
        flags=re.I,
    )

    if not action_match:
        raise ValueError(f"could not fallback-parse action_code: {s[:500]}")

    confidence = 0.0
    if confidence_match:
        try:
            confidence = float(confidence_match.group(1))
        except Exception:
            confidence = 0.0

    return {
        "action_code": action_match.group(1).upper(),
        "confidence": confidence,
        "note": "",
        "customer_intent": "",
        "evidence": "",
        "needs_human_review": confidence < STRUCTURED_REVIEW_THRESHOLD,
        "history_used": False,
        "payment_intent": None,
    }


def extract_json(text: str) -> Dict[str, Any]:
    raw = str(text or "").strip()

    try:
        data = json.loads(raw)
        if isinstance(data, dict):
            return data
    except Exception:
        pass

    try:
        candidate = extract_first_json_object(raw)
        data = json.loads(candidate)
        if isinstance(data, dict):
            return data
    except Exception:
        pass

    return fallback_parse_decision_text(raw)


def _clean_str(value: Any, limit: int = 500) -> str:
    text = str(value or "").strip()
    text = re.sub(r"\s+", " ", text)
    if len(text) > limit:
        text = text[: limit - 1].rstrip() + "…"
    return text


def _as_confidence(value: Any) -> float:
    try:
        confidence = float(value)
    except Exception:
        return 0.0
    if confidence < 0:
        return 0.0
    if confidence > 1:
        return 1.0
    return confidence


def _as_bool(value: Any) -> bool:
    if isinstance(value, bool):
        return value
    return str(value or "").strip().lower() in {"1", "true", "yes", "sim"}


def parse_action_code_response(raw: str) -> ActionDecision:
    """Parse estruturado do classificador LLM.

    v4928.1.5.27: o LLM deve devolver JSON validado contra allow-list.
    Se o formato/código/confiança não forem válidos, a ação é REVIEW_MANUALLY.
    Não há regras comerciais diretas neste parser.
    """
    text = str(raw or "").strip()

    try:
        data = extract_json(text)
    except Exception as exc:
        return ActionDecision(
            action_code="REVIEW_MANUALLY",
            note=f"Resposta LLM inválida/sem JSON estruturado: {str(exc)[:160]}",
            confidence=0.0,
            needs_human_review=True,
        )

    raw_code = str(data.get("action_code") or "").strip().upper()
    code = ACTION_CODE_ALIASES.get(raw_code, raw_code)
    confidence = _as_confidence(data.get("confidence"))
    needs_human_review = _as_bool(data.get("needs_human_review"))

    customer_intent = _clean_str(data.get("customer_intent"), 300)
    evidence = _clean_str(data.get("evidence"), 300)
    note = _clean_str(data.get("note"), 500)
    history_used = _as_bool(data.get("history_used"))
    payment_intent = data.get("payment_intent")
    if payment_intent is not None:
        payment_intent = _clean_str(payment_intent, 80) or None

    if code not in TRIAGE_ACTION_CODES:
        return ActionDecision(
            action_code="REVIEW_MANUALLY",
            note=f"Resposta LLM com action_code fora da lista: {raw_code or 'vazio'}",
            confidence=0.0,
            customer_intent=customer_intent,
            evidence=evidence,
            needs_human_review=True,
            history_used=history_used,
            payment_intent=payment_intent,
        )

    # Se o LLM sinaliza revisão ou não tem confiança suficiente, mantemos a
    # proposta em metadados mas a task operacional vai para REVIEW_MANUALLY.
    if needs_human_review or confidence < STRUCTURED_REVIEW_THRESHOLD:
        review_note = note or customer_intent or "Classificação LLM com confiança baixa ou revisão pedida."
        if code != "REVIEW_MANUALLY":
            review_note = f"Sugestão LLM: {code} ({confidence:.2f}). {review_note}".strip()
        return ActionDecision(
            action_code="REVIEW_MANUALLY",
            note=review_note,
            confidence=confidence,
            customer_intent=customer_intent,
            evidence=evidence,
            needs_human_review=True,
            history_used=history_used,
            payment_intent=payment_intent,
        )

    if not note:
        note = customer_intent or evidence or "Ação classificada pelo LLM."

    return ActionDecision(
        action_code=code,
        note=note,
        confidence=confidence,
        customer_intent=customer_intent,
        evidence=evidence,
        needs_human_review=False,
        history_used=history_used,
        payment_intent=payment_intent if code == "CONFIRM_PAYMENT" else None,
    )


def parse_decision(data: Dict[str, Any]) -> ActionDecision:
    # Mantido para compatibilidade com imports/testes antigos.
    return ActionDecision(
        action_code=str(data.get("action_code") or "REVIEW_MANUALLY"),
        note=str(data.get("note") or "").strip(),
        confidence=float(data.get("confidence") or 0.0),
        customer_intent=str(data.get("customer_intent") or "").strip(),
        evidence=str(data.get("evidence") or "").strip(),
        needs_human_review=bool(data.get("needs_human_review") or False),
        history_used=bool(data.get("history_used") or False),
        payment_intent=data.get("payment_intent"),
    )


async def decide_action_with_llm(request: AnalyzeRequest) -> Tuple[ActionDecision, UsageInfo, Dict[str, Any]]:
    payload = {
        "model": settings.openrouter_model,
        "messages": [
            {"role": "system", "content": build_action_system_prompt()},
            {
                "role": "user",
                "content": build_action_user_prompt(
                    request.last_customer_message,
                    request.previous_context,
                    request.current_state,
                ),
            },
        ],
        "temperature": 0,
        "max_tokens": 320,
        "reasoning": {
            "effort": "none",
            "exclude": True,
        },
    }

    headers = {
        "Authorization": f"Bearer {settings.openrouter_api_key}",
        "Content-Type": "application/json",
    }

    async with httpx.AsyncClient(timeout=60) as client:
        response = await client.post(
            settings.openrouter_url,
            headers=headers,
            json=payload,
        )

    response.raise_for_status()
    raw_response = response.json()

    content = raw_response["choices"][0]["message"].get("content") or ""
    decision = parse_action_code_response(content)

    usage_data = raw_response.get("usage") or {}

    usage = UsageInfo(
        id=raw_response.get("id"),
        model=raw_response.get("model") or settings.openrouter_model,
        provider=raw_response.get("provider"),
        prompt_tokens=usage_data.get("prompt_tokens") or 0,
        completion_tokens=usage_data.get("completion_tokens") or 0,
        total_tokens=usage_data.get("total_tokens") or 0,
        cost=usage_data.get("cost") or 0.0,
    )

    return decision, usage, raw_response
