#!/usr/bin/env python3
"""
Modulo de VENDAS REAIS (CAPI PX3).

Parseia /opt/mia/logs/track_capi_px3.log procurando eventos Purchase enviados
ao Facebook. Retorna agregacoes por periodo, produto, vendedor e curva diaria.

- Live vs Backfill: eventos com event_id comecando por "backfill-vra-" sao
  considerados historicos (importacoes retroativas) e ficam SEPARADOS da
  contagem viva. O front escolhe se soma ou nao.
- Test mode: eventos com test_mode=true nao entram nas metricas de producao.
"""
from __future__ import annotations

import json
import logging
import re
import threading
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from zoneinfo import ZoneInfo

CAPI_LOG = Path("/opt/mia/logs/track_capi_px3.log")
TZ_BR = ZoneInfo("America/Sao_Paulo")
TZ_UTC = ZoneInfo("UTC")

# Regex robusta pra achar o JSON de cada linha CAPI ...
_CAPI_LINE_RE = re.compile(r"CAPI\s+(\{.*\})\s*$")

# Cache in-memory (invalida quando o arquivo cresce)
_CACHE_LOCK = threading.Lock()
_CACHE: dict = {
    "purchases": [],           # lista de dict de todos os Purchase parseados
    "log_size": 0,
    "log_mtime": 0.0,
    "ts": 0.0,
}
_TTL = 120  # 2 min como piso mesmo que arquivo nao mude


log = logging.getLogger("vendas")


# ============================================================
# Parse
# ============================================================
def _parse_line(line: str) -> dict | None:
    line = line.strip()
    if not line or "Purchase" not in line:
        return None
    m = _CAPI_LINE_RE.search(line)
    if not m:
        return None
    try:
        d = json.loads(m.group(1))
    except Exception:
        return None
    if d.get("event") != "Purchase":
        return None
    return d


def _load_purchases() -> list[dict]:
    """Le o log inteiro e retorna todos os Purchase encontrados.
    Cache invalidado se o arquivo cresceu ou mtime mudou (piso de 2 min)."""
    with _CACHE_LOCK:
        try:
            st = CAPI_LOG.stat()
        except FileNotFoundError:
            _CACHE["purchases"] = []
            _CACHE["log_size"] = 0
            _CACHE["log_mtime"] = 0.0
            _CACHE["ts"] = time.time()
            return []

        now = time.time()
        cache_valido = (
            st.st_size == _CACHE["log_size"]
            and st.st_mtime == _CACHE["log_mtime"]
            and (now - _CACHE["ts"]) < _TTL
        )
        if cache_valido and _CACHE["purchases"]:
            return _CACHE["purchases"]

        purchases: list[dict] = []
        try:
            with CAPI_LOG.open("r", encoding="utf-8", errors="replace") as f:
                for line in f:
                    d = _parse_line(line)
                    if d:
                        purchases.append(d)
        except Exception as e:
            log.warning(f"Falha lendo CAPI log: {e}")
            return _CACHE.get("purchases") or []

        _CACHE["purchases"] = purchases
        _CACHE["log_size"] = st.st_size
        _CACHE["log_mtime"] = st.st_mtime
        _CACHE["ts"] = now
        return purchases


# ============================================================
# Helpers
# ============================================================
def _parse_ts(ts: str) -> datetime | None:
    if not ts:
        return None
    try:
        dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=TZ_UTC)
        return dt
    except Exception:
        return None


def _norm_produto(content_name: str) -> str:
    """Normaliza content_name variantes (FOTODRIVE/FOTO DRIVE/PHOTO DRIVE -> PHOTO DRIVE)."""
    n = (content_name or "").strip().upper()
    if not n:
        return "SEM PRODUTO"
    # Colapsa variantes de PHOTO DRIVE
    if n.replace(" ", "") in ("FOTODRIVE", "PHOTODRIVE"):
        return "PHOTO DRIVE"
    return n


def _extrair_vendedor(event_id: str) -> str:
    """
    event_id observado:
      backfill-vra-r39-20260527
      vra-paulo-prod-1787593107
      vra-9483
      vra-public-1787593154
      vra-integracao-test-1787593825

    Extrai a "tag" logo apos "vra-" (paulo, public, integracao, 9483 etc.)
    """
    eid = (event_id or "").strip().lower()
    if not eid:
        return "desconhecido"
    if eid.startswith("backfill-vra"):
        return "backfill"
    if eid.startswith("vra-"):
        parts = eid.split("-")
        if len(parts) >= 2:
            tag = parts[1]
            # Se for so digitos (ex: 9483), e uma execucao anonima do n8n
            if tag.isdigit():
                return f"execucao {tag}"
            return tag
    return "desconhecido"


def _is_backfill(event_id: str) -> bool:
    return (event_id or "").lower().startswith("backfill-vra")


def _is_test(purchase: dict) -> bool:
    return bool(purchase.get("test_mode"))


# ============================================================
# Agregacoes
# ============================================================
def filtrar_periodo(
    purchases: list[dict],
    inicio_utc: datetime,
    fim_utc: datetime,
    incluir_backfill: bool = False,
    incluir_test: bool = False,
) -> list[dict]:
    out = []
    for p in purchases:
        dt = _parse_ts(p.get("ts", ""))
        if not dt:
            continue
        if not (inicio_utc <= dt <= fim_utc):
            continue
        if not incluir_backfill and _is_backfill(p.get("event_id", "")):
            continue
        if not incluir_test and _is_test(p):
            continue
        out.append(p)
    return out


def _fmt_brl(value) -> str:
    try:
        v = float(value or 0)
    except (TypeError, ValueError):
        v = 0.0
    s = f"{v:,.2f}"
    s = s.replace(",", "@").replace(".", ",").replace("@", ".")
    return f"R$ {s}"


def agregar_vendas(vendas: list[dict]) -> dict:
    """Agrega KPIs, produtos, curva diaria, vendedores."""
    total = len(vendas)
    receita = 0.0
    por_produto: dict[str, dict] = {}
    por_dia: dict[str, dict] = {}   # YYYY-MM-DD (BR)
    por_vendedor: dict[str, dict] = {}

    for p in vendas:
        val = 0.0
        try:
            val = float(p.get("value") or 0)
        except (TypeError, ValueError):
            val = 0.0
        receita += val

        prod = _norm_produto(p.get("content_name", ""))
        if prod not in por_produto:
            por_produto[prod] = {"vendas": 0, "receita": 0.0}
        por_produto[prod]["vendas"] += 1
        por_produto[prod]["receita"] += val

        dt = _parse_ts(p.get("ts", ""))
        if dt:
            data_br = dt.astimezone(TZ_BR).strftime("%Y-%m-%d")
            if data_br not in por_dia:
                por_dia[data_br] = {"vendas": 0, "receita": 0.0}
            por_dia[data_br]["vendas"] += 1
            por_dia[data_br]["receita"] += val

        vend = _extrair_vendedor(p.get("event_id", ""))
        if vend not in por_vendedor:
            por_vendedor[vend] = {"vendas": 0, "receita": 0.0}
        por_vendedor[vend]["vendas"] += 1
        por_vendedor[vend]["receita"] += val

    ticket_medio = (receita / total) if total else 0.0

    # Ordenacoes
    produtos_lista = [
        {"produto": k, "vendas": v["vendas"], "receita": round(v["receita"], 2),
         "receita_fmt": _fmt_brl(v["receita"])}
        for k, v in por_produto.items()
    ]
    produtos_lista.sort(key=lambda x: -x["receita"])

    dias_lista = []
    for d in sorted(por_dia.keys()):
        info = por_dia[d]
        try:
            label = datetime.strptime(d, "%Y-%m-%d").strftime("%d/%m")
        except Exception:
            label = d
        dias_lista.append({
            "data": label,
            "data_iso": d,
            "vendas": info["vendas"],
            "receita": round(info["receita"], 2),
        })

    vendedores_lista = [
        {"vendedor": k, "vendas": v["vendas"], "receita": round(v["receita"], 2),
         "receita_fmt": _fmt_brl(v["receita"])}
        for k, v in por_vendedor.items()
    ]
    vendedores_lista.sort(key=lambda x: -x["receita"])

    return {
        "kpis": {
            "total_vendas": total,
            "receita_total": round(receita, 2),
            "receita_total_fmt": _fmt_brl(receita),
            "ticket_medio": round(ticket_medio, 2),
            "ticket_medio_fmt": _fmt_brl(ticket_medio),
        },
        "por_produto": produtos_lista,
        "curva_diaria": dias_lista,
        "vendedores": vendedores_lista,
    }


def comparar_hoje_media(purchases: list[dict], janela_dias: int = 7) -> dict:
    """Compara vendas de HOJE vs media dos ultimos `janela_dias` dias anteriores.
    Ignora backfill e test.
    """
    now_br = datetime.now(TZ_BR)
    hoje_ini = now_br.replace(hour=0, minute=0, second=0, microsecond=0)
    hoje_fim = now_br.replace(hour=23, minute=59, second=59, microsecond=0)

    janela_fim = hoje_ini - timedelta(seconds=1)
    janela_ini = janela_fim.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=janela_dias - 1)

    hoje = filtrar_periodo(purchases, hoje_ini.astimezone(TZ_UTC), hoje_fim.astimezone(TZ_UTC))
    janela = filtrar_periodo(purchases, janela_ini.astimezone(TZ_UTC), janela_fim.astimezone(TZ_UTC))

    agg_hoje = agregar_vendas(hoje)
    agg_janela = agregar_vendas(janela)

    media_vendas = (agg_janela["kpis"]["total_vendas"] / janela_dias) if janela_dias else 0.0
    media_ticket = agg_janela["kpis"]["ticket_medio"]

    diff_vendas_pct = 0.0
    if media_vendas > 0:
        diff_vendas_pct = ((agg_hoje["kpis"]["total_vendas"] - media_vendas) / media_vendas * 100)

    diff_ticket_pct = 0.0
    if media_ticket > 0:
        diff_ticket_pct = ((agg_hoje["kpis"]["ticket_medio"] - media_ticket) / media_ticket * 100)

    return {
        "hoje": agg_hoje["kpis"],
        "media_janela": {
            "dias": janela_dias,
            "vendas_dia": round(media_vendas, 2),
            "ticket_medio": round(media_ticket, 2),
            "ticket_medio_fmt": _fmt_brl(media_ticket),
        },
        "diff": {
            "vendas_pct": round(diff_vendas_pct, 1),
            "ticket_pct": round(diff_ticket_pct, 1),
        },
    }


def coletar_vendas(
    inicio_utc: datetime,
    fim_utc: datetime,
    incluir_backfill: bool = False,
) -> dict:
    """Ponto de entrada usado pelo app.py.
    Retorna dict pronto com KPIs + curva + produtos + vendedores + alertas.
    """
    purchases = _load_purchases()
    live = filtrar_periodo(purchases, inicio_utc, fim_utc, incluir_backfill=incluir_backfill)
    agg = agregar_vendas(live)

    # Bloco historico (backfill) - referencia sempre visivel.
    # Inclui backfill test_mode (a maioria dos backfills atuais foram enviados em teste).
    historico_puro = [
        p for p in purchases
        if _is_backfill(p.get("event_id", "")) and _parse_ts(p.get("ts", "")) and inicio_utc <= _parse_ts(p.get("ts", "")) <= fim_utc
    ]
    agg_historico = agregar_vendas(historico_puro)

    alerta = comparar_hoje_media(purchases, janela_dias=7)

    return {
        "kpis": agg["kpis"],
        "por_produto": agg["por_produto"],
        "curva_diaria": agg["curva_diaria"],
        "vendedores": agg["vendedores"],
        "historico_backfill": {
            "total_vendas": agg_historico["kpis"]["total_vendas"],
            "receita_total": agg_historico["kpis"]["receita_total"],
            "receita_total_fmt": agg_historico["kpis"]["receita_total_fmt"],
        },
        "alerta": alerta,
        "total_no_log": len(purchases),
        "gerado_em": datetime.now(TZ_UTC).isoformat(timespec="seconds"),
    }


# ============================================================
# Atribuicao vendas -> canal (compras GHL x tag origem_*)
# ============================================================
def _classificar_contato_por_modo(contato: dict, modo: str) -> str:
    """Reusa a mesma logica do tracking mas escolhe entre first e last touch."""
    # Import local pra evitar ciclo em tempo de import inicial
    from tracking import _classificar_canal  # type: ignore
    attributions = contato.get("attributions")
    return _classificar_canal(attributions, modo=modo)


def _mapa_valor_por_produto(tags_cfg: dict) -> dict:
    """
    Tenta montar um mapa {tag_compra: valor_padrao_produto} a partir do
    tags_cfg (arquivo /tmp/px3_tags_linkia.json). Se nao houver valor,
    retorna dict vazio (o cruzamento cai no fallback proporcional).

    Suporta duas fontes:
    - tags_cfg['compra_produtos'][tag] = 'nome_produto' -> usa mapa produto->valor abaixo
    - hardcoded fallback pra produtos PX3 conhecidos
    """
    # Precos aproximados conhecidos - fonte: content_name mais frequente no log CAPI
    # Estes valores servem so pra atribuicao last-touch quando nao tem CAPI 1-to-1.
    PRECO_APROX = {
        "PHOTO DRIVE": 297.0,
        "CLOUD PHOTORF": 700.0,
        "E-PHOTORF": 297.0,
        "CHECKOUT PHOTORF": 397.0,
        "FAST ALBUM": 297.0,
    }
    tag_produto = tags_cfg.get("compra_produtos", {}) or {}
    mapa = {}
    for tag, produto in tag_produto.items():
        # Tenta bater o produto no dict acima (case-insensitive)
        for chave, valor in PRECO_APROX.items():
            if produto and chave.lower() in produto.lower():
                mapa[tag.lower()] = valor
                break
    return mapa


def cruzar_com_atribuicao(
    vendas_periodo: list[dict],
    atribuicao_por_canal: dict,
    gasto_meta: float,
    gasto_google: float,
    contatos_ghl: list[dict] | None = None,
    tags_cfg: dict | None = None,
    metodo: str = "proporcional",
) -> dict:
    """
    Combina CAPI (receita real) com atribuicao GHL pra calcular ROAS por canal.

    Metodos:
    - "proporcional" (padrao, legado): receita CAPI eh distribuida entre canais
      seguindo a proporcao de compras GHL (share_compras). Simples, sem depender
      de bater 1-to-1.
    - "last_touch": para cada contato GHL que comprou, reclassifica o canal pelo
      last-touch (attribution mais recente) e soma valor aproximado por tag de
      compra (a partir de tags_cfg + precos conhecidos). Fica mais fiel para
      canal, mas o valor TOTAL pode divergir do CAPI (calibra pra CAPI ao final
      via factor de correcao).

    O metodo "last_touch" precisa de `contatos_ghl` e `tags_cfg`. Se nao vier,
    cai automaticamente pro "proporcional".
    """
    total_ghl_compras = sum(
        (dados.get("compras", 0) or 0) for dados in atribuicao_por_canal.values()
    )
    total_receita_capi = sum(float(p.get("value") or 0) for p in vendas_periodo)
    total_vendas_capi = len(vendas_periodo)

    metodo_efetivo = metodo
    canais_last_touch: dict = {}

    if metodo == "last_touch" and contatos_ghl and tags_cfg:
        tags_compra = set(t.lower() for t in tags_cfg.get("compra", []))
        valor_por_tag = _mapa_valor_por_produto(tags_cfg)

        canais_last_touch = {
            c: {"compradores": 0, "compras": 0, "receita_bruta": 0.0}
            for c in ("meta", "google", "whatsapp", "crm", "direto")
        }
        contatos_com_compra_por_canal = {c: 0 for c in canais_last_touch}
        for contato in contatos_ghl:
            tags = [str(t).lower() for t in (contato.get("tags") or [])]
            compras_do_contato = [t for t in tags if t in tags_compra]
            if not compras_do_contato:
                continue
            canal = _classificar_contato_por_modo(contato, "last_touch")
            if canal not in canais_last_touch:
                canal = "direto"
            canais_last_touch[canal]["compradores"] += 1
            for tag in compras_do_contato:
                canais_last_touch[canal]["compras"] += 1
                # valor aproximado ou 0 se nao souber
                canais_last_touch[canal]["receita_bruta"] += valor_por_tag.get(tag, 0.0)
            contatos_com_compra_por_canal[canal] += 1

        # Calibra pra bater com o total CAPI (se soubermos valores parciais)
        soma_bruta = sum(v["receita_bruta"] for v in canais_last_touch.values())
        if soma_bruta > 0 and total_receita_capi > 0:
            fator = total_receita_capi / soma_bruta
        else:
            fator = 0.0
            metodo_efetivo = "proporcional"  # fallback
    else:
        metodo_efetivo = "proporcional"

    canais_out = []
    for canal, dados in atribuicao_por_canal.items():
        compras_c = int(dados.get("compras", 0) or 0)
        leads_c = int(dados.get("leads", 0) or 0)

        if metodo_efetivo == "last_touch" and canal in canais_last_touch:
            # receita bruta baseada em precos aproximados de produto,
            # calibrada pelo fator pra bater no CAPI total.
            lt = canais_last_touch[canal]
            receita_atribuida = round(lt["receita_bruta"] * fator, 2)
            compras_last = lt["compras"]
            vendas_atribuidas = compras_last
            share = compras_last / max(sum(v["compras"] for v in canais_last_touch.values()), 1)
        else:
            # metodo proporcional (legado): share_compras vezes receita total
            if total_ghl_compras > 0:
                share = compras_c / total_ghl_compras
            else:
                share = 0.0
            receita_atribuida = round(total_receita_capi * share, 2)
            vendas_atribuidas = round(total_vendas_capi * share, 1)

        gasto = 0.0
        if canal == "meta":
            gasto = gasto_meta
        elif canal == "google":
            gasto = gasto_google

        roas = (receita_atribuida / gasto) if gasto > 0 else 0.0
        cpa = (gasto / compras_c) if compras_c > 0 else 0.0

        canais_out.append({
            "canal": canal,
            "leads": leads_c,
            "compras_ghl": compras_c,
            "share_compras": round(share * 100, 1),
            "vendas_atribuidas": vendas_atribuidas,
            "receita_atribuida": receita_atribuida,
            "gasto": round(gasto, 2),
            "roas": round(roas, 2),
            "cpa": round(cpa, 2),
        })

    # ordena por receita desc
    canais_out.sort(key=lambda x: -x["receita_atribuida"])

    return {
        "canais": canais_out,
        "metodo": metodo_efetivo,
        "totais": {
            "receita_capi": round(total_receita_capi, 2),
            "vendas_capi": total_vendas_capi,
            "compras_ghl": total_ghl_compras,
            "gasto_meta": round(gasto_meta, 2),
            "gasto_google": round(gasto_google, 2),
            "roas_meta": round((sum(c["receita_atribuida"] for c in canais_out if c["canal"] == "meta") / gasto_meta), 2) if gasto_meta > 0 else 0.0,
            "roas_google": round((sum(c["receita_atribuida"] for c in canais_out if c["canal"] == "google") / gasto_google), 2) if gasto_google > 0 else 0.0,
        },
    }
