"""
Bruno — SDR TORQ Brasil.

Combina persona ALMA + dossie TORQ + campos custom sdr__ + pipeline TESTE RAFA.

ARQUITETURA (importante):
  - NAO usa a API paga da Anthropic (ANTHROPIC_API_KEY).
  - Usa a assinatura Claude Code (Max plan) do Renato via CLI headless.
  - Binario: /usr/bin/claude (npm i -g @anthropic-ai/claude-code).
  - Auth: /home/mia/.claude/.credentials.json (OAuth da assinatura).
  - Prompt caching e aplicado automaticamente pelo CLI no --append-system-prompt.

Estrategia de contexto:
  - --append-system-prompt = dossie ALMA + instrucoes operacionais (imutavel -> CACHE)
  - --print (stdin/arg)    = contexto CRM + historico da conversa + nova msg do lead
  - --output-format json   = retorna {"result": "...", "session_id": "...", "usage": {...}}

O agente devolve:
  - texto natural (msg pro WhatsApp) — usa [[BREAK]] como separador de blocos
  - BLOCO opcional no formato:
      ---SDR-META---
      {"campos": {...}, "stage": "...", "notificar_renato": bool}
      ---FIM---
    O app.py parseia isso, executa as acoes GHL, e envia so o texto natural.
"""

from __future__ import annotations

import json
import logging
import os
import re
import shutil
import subprocess
from pathlib import Path
from typing import Any

log = logging.getLogger("bruno_agent")

BASE_DIR = Path(__file__).resolve().parent
HISTORICO_DIR = BASE_DIR / "historico"
HISTORICO_DIR.mkdir(parents=True, exist_ok=True)

MAX_HISTORY_MESSAGES = 30
MODEL_ID = os.getenv("BRUNO_MODEL", "claude-sonnet-4-6")

# Timeout do subprocess CLI. Sonnet 4.6 responde entre 5-40s dependendo do adaptive thinking.
CLI_TIMEOUT = int(os.getenv("BRUNO_CLI_TIMEOUT", "120"))

# Binario do CLI Claude Code (assinatura Max plan do Renato)
CLAUDE_BIN = os.getenv("CLAUDE_BIN") or shutil.which("claude") or "/usr/bin/claude"

DOSSIE_PATH = Path("/opt/mia/workspace/clientes/torq-brasil/sdr_alma_torqbrasil.md")

# Prompt fixo: instrucoes de formato + protocolo de saida (curto, especifico)
INSTRUCOES_OPERACIONAIS = """
## PROTOCOLO DE RESPOSTA (obrigatorio)

Voce e o Bruno. Toda mensagem tua vai direto pro WhatsApp do lead.

### FORMATO da mensagem
- Cada bloco max 3 linhas
- Divida blocos com [[BREAK]] (max 3 blocos)
- 1 pergunta por bloco, no fim
- NUNCA use travessao (- - ou --). Use virgula, ponto, parenteses
- Tom semiformal com pitada mineira leve
- NUNCA revele que voce e IA / agente / bot

### PRIMEIRA MENSAGEM (regra critica)
Quando o HISTORICO DA CONVERSA vier VAZIO (lead esta te falando pela primeira vez):
- Responda APENAS 1 bloco, curto e acolhedor
- Nao entre em catalogo, preco, modelos, uso proprio vs empresa/revenda
- Nao qualifique nada de cara
- Objetivo: apenas cumprimentar e ABRIR ESPACO pro lead falar o que quer
- Ex: "Oi! Aqui e o Bruno da TORQ Brasil, tudo bem? Que bom que voce chegou ate aqui, me conta o que trouxe voce aqui?"
- SO va pra fase L (Levantar) do ALMA quando o lead responder essa segunda msg pedindo algo ou dando contexto
- Nao inclua bloco META na primeira msg (nao ha nada pra atualizar ainda)

### METADADOS (opcional, apenas quando houver mudanca)
Ao fim da mensagem, se voce identificou dados novos ou quer atualizar pipeline,
adicione UM bloco JSON exatamente neste formato:

---SDR-META---
{"campos":{"sdr__nome_lead":"Marcos","sdr__cidade":"BH","sdr__perfil":"b2c_urbano"},"stage":"qualificado","notificar_renato":false}
---FIM---

Regras do bloco META:
- `campos`: obj com keys sdr__* que voce quer setar/atualizar (use APENAS keys do Bloco 7 do dossie)
- `stage`: um de [novo_lead, qualificado, test_drive_agendado, proposta, perdido] — omitir se nao mudou
- `notificar_renato`: true SOMENTE quando lead virou QUENTE + handoff (ver Bloco 8.3)
- OMITA o bloco META inteiro se nao ha nada pra atualizar
- O lead NUNCA ve esse bloco (ele fica so nos logs)

### QUANDO NOTIFICAR RENATO (notificar_renato: true)
- B2C ticket >R$ 15k
- B2B com CNPJ + volume >= 5 unidades
- B2B revenda 26+ unidades (Revenda Oficial)
- Lead pediu explicitamente falar com humano
- Marcou test drive presencial
- Pediu proposta formal / contrato
- Situacao fora do escopo do dossie

### CAMPOS OBRIGATORIOS ATE FIM DA FASE L (Levantar)
sdr__nome_lead, sdr__cidade, sdr__uf, sdr__perfil, sdr__uso_pretendido,
sdr__prazo_compra, sdr__categoria_interesse. CNH apenas se scooter/triciclo.

### STAGES DISPONIVEIS (nao pule etapas)
- novo_lead → qualificado → test_drive_agendado / proposta → ganhamos / perdemos

### FORMATO DE ENTRADA
A tua "user message" vem estruturada assim:
  [CONTEXTO CRM]
  ...campos do lead...

  [HISTORICO DA CONVERSA]
  lead: ...
  bruno: ...
  (mais turnos)

  [NOVA MENSAGEM DO LEAD]
  ...texto que ele acabou de mandar...

Responda APENAS com a mensagem pro lead + (opcional) bloco META. Nada mais.
Nada de "Ola! Como posso ajudar?" — voce ja esta no meio de uma conversa real.
""".strip()


def _load_dossie() -> str:
    if not DOSSIE_PATH.exists():
        log.error("dossie ALMA nao encontrado em %s", DOSSIE_PATH)
        return ""
    return DOSSIE_PATH.read_text(encoding="utf-8")


# Carrega uma vez em memoria (o processo Flask reinicia via systemd se mudar)
_DOSSIE_CACHE: str = _load_dossie()


def _build_system_prompt() -> str:
    """
    System prompt completo: dossie ALMA + instrucoes operacionais.
    Passado via --append-system-prompt. O CLI aplica prompt caching automaticamente.
    """
    return f"{_DOSSIE_CACHE}\n\n---\n\n{INSTRUCOES_OPERACIONAIS}"


# ---------------------------------------------------------------------------
# Historico local (backup — o oficial fica no GHL Conversations)
# ---------------------------------------------------------------------------

def _history_path(contact_id: str) -> Path:
    safe = "".join(c for c in contact_id if c.isalnum() or c in ("-", "_"))
    return HISTORICO_DIR / f"{safe or 'unknown'}.json"


def load_history(contact_id: str) -> list[dict[str, str]]:
    p = _history_path(contact_id)
    if not p.exists():
        return []
    try:
        data = json.loads(p.read_text(encoding="utf-8"))
        if isinstance(data, list):
            return data
    except (json.JSONDecodeError, OSError) as e:
        log.warning("historico corrompido contact=%s err=%s", contact_id, e)
    return []


def save_history(contact_id: str, history: list[dict[str, str]]) -> None:
    trimmed = history[-MAX_HISTORY_MESSAGES:]
    try:
        _history_path(contact_id).write_text(
            json.dumps(trimmed, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
    except OSError as e:
        log.warning("nao salvei historico contact=%s err=%s", contact_id, e)


# ---------------------------------------------------------------------------
# Parsing da resposta
# ---------------------------------------------------------------------------

META_REGEX = re.compile(
    r"---SDR-META---\s*(\{.*?\})\s*---FIM---",
    re.DOTALL | re.IGNORECASE,
)


def parse_response(raw: str) -> tuple[str, dict[str, Any]]:
    """
    Separa a resposta em (texto_lead, meta_dict).
    Se nao houver bloco META, meta_dict = {}.
    """
    if not raw:
        return "", {}

    meta: dict[str, Any] = {}
    match = META_REGEX.search(raw)
    if match:
        try:
            meta = json.loads(match.group(1))
        except json.JSONDecodeError as e:
            log.warning("meta JSON invalido: %s | raw=%s", e, match.group(1)[:200])
            meta = {}
        # Remove o bloco META do texto que vai pro lead
        texto = META_REGEX.sub("", raw).strip()
    else:
        texto = raw.strip()

    return texto, meta


# ---------------------------------------------------------------------------
# Construcao do prompt de usuario (contexto + historico + nova msg)
# ---------------------------------------------------------------------------

def _build_context_hint(
    contact_meta: dict[str, Any],
    custom_fields: dict[str, Any] | None,
) -> str:
    """Contexto do CRM em texto plano — vai no comeco do user prompt."""
    linhas = ["[CONTEXTO CRM]"]
    for k in ("first_name", "last_name", "phone", "contact_id", "conversation_id"):
        v = contact_meta.get(k)
        if v:
            linhas.append(f"- {k}: {v}")
    if custom_fields:
        linhas.append("Campos sdr__ ja preenchidos:")
        for k, v in custom_fields.items():
            if v not in (None, "", []):
                linhas.append(f"- {k}: {v}")
    else:
        linhas.append("Campos sdr__: nenhum preenchido ainda.")
    return "\n".join(linhas)


def _build_history_block(history: list[dict[str, str]]) -> str:
    """Historico serializado em texto (lead: / bruno:)."""
    if not history:
        return "[HISTORICO DA CONVERSA]\n(vazio — primeira interacao)"
    linhas = ["[HISTORICO DA CONVERSA]"]
    for msg in history[-MAX_HISTORY_MESSAGES:]:
        role = msg.get("role")
        content = (msg.get("content") or "").strip()
        if not content or role not in ("user", "assistant"):
            continue
        prefix = "lead" if role == "user" else "bruno"
        linhas.append(f"{prefix}: {content}")
    return "\n".join(linhas)


def _build_user_prompt(
    contact_meta: dict[str, Any],
    custom_fields: dict[str, Any] | None,
    history: list[dict[str, str]],
    mensagem_lead: str,
) -> str:
    return (
        f"{_build_context_hint(contact_meta, custom_fields)}\n\n"
        f"{_build_history_block(history)}\n\n"
        f"[NOVA MENSAGEM DO LEAD]\n{mensagem_lead}"
    )


# ---------------------------------------------------------------------------
# Chamada Claude via CLI headless (assinatura Max plan)
# ---------------------------------------------------------------------------

def _call_claude_cli(system_prompt: str, user_prompt: str) -> tuple[str, dict[str, Any]]:
    """
    Chama o CLI Claude Code em modo headless com output JSON.

    Retorna (texto_bruto, usage_dict).
    Levanta RuntimeError em qualquer falha.
    """
    if not Path(CLAUDE_BIN).exists() and not shutil.which(CLAUDE_BIN):
        raise RuntimeError(f"CLI claude nao encontrado em {CLAUDE_BIN}")

    cmd = [
        CLAUDE_BIN,
        "--print",  # headless (nao interativo)
        "--output-format", "json",
        "--append-system-prompt", system_prompt,
        "--model", MODEL_ID,
        # Sem permissoes de tool: Bruno so precisa gerar texto
        "--permission-mode", "default",
        "--disallowedTools", "Bash,Edit,Write,Read,Task,WebFetch,WebSearch",
    ]

    # Passa o user prompt via stdin (mais robusto que como arg — sem limite de tamanho)
    env = os.environ.copy()
    # Garante que o CLI ache as credenciais OAuth do user mia
    env.setdefault("HOME", os.path.expanduser("~"))
    # Remove qualquer ANTHROPIC_API_KEY do ambiente pra forcar uso da assinatura
    env.pop("ANTHROPIC_API_KEY", None)

    try:
        proc = subprocess.run(
            cmd,
            input=user_prompt,
            capture_output=True,
            text=True,
            timeout=CLI_TIMEOUT,
            env=env,
        )
    except subprocess.TimeoutExpired as e:
        raise RuntimeError(f"CLI timeout apos {CLI_TIMEOUT}s") from e

    if proc.returncode != 0:
        raise RuntimeError(
            f"CLI retornou exit={proc.returncode} stderr={proc.stderr[:500]!r}"
        )

    stdout = (proc.stdout or "").strip()
    if not stdout:
        raise RuntimeError(f"CLI retornou stdout vazio stderr={proc.stderr[:200]!r}")

    try:
        payload = json.loads(stdout)
    except json.JSONDecodeError as e:
        raise RuntimeError(
            f"CLI retornou nao-JSON: {e} | stdout={stdout[:400]!r}"
        ) from e

    if payload.get("is_error") or payload.get("subtype") != "success":
        raise RuntimeError(
            f"CLI status error subtype={payload.get('subtype')} "
            f"api_err={payload.get('api_error_status')} "
            f"result={str(payload.get('result'))[:300]!r}"
        )

    raw_text = payload.get("result") or ""
    usage = payload.get("usage") or {}
    # Enriquece com dados de custo/session pra log
    usage["_session_id"] = payload.get("session_id")
    usage["_duration_ms"] = payload.get("duration_ms")
    usage["_cost_usd"] = payload.get("total_cost_usd")
    return raw_text, usage


def gerar_resposta(
    contact_id: str,
    mensagem_lead: str,
    contact_meta: dict[str, Any] | None = None,
    custom_fields: dict[str, Any] | None = None,
    historico_ghl: list[dict[str, str]] | None = None,
) -> tuple[str, dict[str, Any]]:
    """
    Gera resposta do Bruno via CLI Claude Code (assinatura Max plan).

    Retorna (texto_pro_lead, meta_dict).
    """
    if not _DOSSIE_CACHE:
        log.error("dossie ALMA vazio — impossivel gerar resposta")
        return "Oi, deu um probleminha aqui do meu lado. Ja te chamo em instantes.", {}

    # Historico: prioriza o do GHL (fonte da verdade); fallback pro local
    if historico_ghl:
        history = list(historico_ghl)
    else:
        history = load_history(contact_id)

    system_prompt = _build_system_prompt()
    user_prompt = _build_user_prompt(
        contact_meta=contact_meta or {},
        custom_fields=custom_fields,
        history=history,
        mensagem_lead=mensagem_lead,
    )

    try:
        raw, usage = _call_claude_cli(system_prompt, user_prompt)
    except Exception as e:
        log.exception("CLI Claude falhou: %s", e)
        return "Oi, deu um probleminha aqui do meu lado. Ja te chamo em instantes.", {}

    # Log de cache hit/miss + custo (util pra verificar economia)
    log.info(
        "claude_cli usage input=%s cache_read=%s cache_creation=%s output=%s "
        "cost_usd=%s duration_ms=%s session=%s",
        usage.get("input_tokens", 0),
        usage.get("cache_read_input_tokens", 0),
        usage.get("cache_creation_input_tokens", 0),
        usage.get("output_tokens", 0),
        usage.get("_cost_usd"),
        usage.get("_duration_ms"),
        usage.get("_session_id"),
    )

    if not raw:
        log.warning("Claude CLI retornou texto vazio")
        return "Deixa eu conferir aqui e ja te respondo.", {}

    texto, meta = parse_response(raw)

    if not texto:
        # Se so veio bloco META (nunca deveria), manda algo minimo
        log.warning("Bruno so respondeu META, sem texto pro lead")
        texto = "Deixa eu conferir aqui e ja te respondo."

    # Salva historico local (backup)
    history.append({"role": "user", "content": mensagem_lead})
    history.append({"role": "assistant", "content": texto})
    save_history(contact_id, history)

    return texto, meta
