#!/usr/bin/env python3
"""
Modulo de rastreamento PX3 Lab.

- Endpoint /track/wa: registra clique em link de WhatsApp (com UTMs) e redireciona pro wa.me.
- Endpoint /track/lp: beacon pra registrar pageview da landing (chamado por GTM/JS).
- Helpers de agregacao usados pela aba /rastreamento do dashboard.
- Helpers de atribuicao real cruzando contatos GHL (tags) com gasto Meta/Google.

Os registros de clique/pageview sao persistidos em JSONL (uma linha por evento), pra ler sem
dependencia de banco. Arquivos em /opt/mia/workspace/clientes/px3lab/tracking/.
"""
from __future__ import annotations

import json
import os
import re
import time
import urllib.parse
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Iterable

import requests
from flask import Blueprint, request, redirect, jsonify

# ============================================================
# Config
# ============================================================
TRACKING_DIR = Path(
    os.environ.get(
        "PX3_TRACKING_DIR",
        "/opt/mia/workspace/clientes/px3lab/tracking",
    )
)
TRACKING_DIR.mkdir(parents=True, exist_ok=True)

CLICKS_FILE = TRACKING_DIR / "clicks.jsonl"
PAGEVIEWS_FILE = TRACKING_DIR / "pageviews.jsonl"

# Numero padrao do WhatsApp PX3 (sem mascara, com DDI)
DEFAULT_WA_PHONE = os.environ.get("PX3_WA_PHONE", "5511934313883")

# Campos UTM que registramos nos JSONL de clique/pageview
UTM_FIELDS = (
    "utm_source",
    "utm_medium",
    "utm_campaign",
    "utm_content",
    "utm_term",
)

# Arquivo com tags de compra/nutricao mantido pela Amanda (CRM)
# Enquanto o arquivo nao existir, agregamos usando as tags padrao abaixo.
TAGS_FILE = Path("/tmp/px3_tags_linkia.json")

TAGS_COMPRA_DEFAULT = [
    "comprou-claud-photorf",
    "comprou-checkout-photorf",
    "comprou-e-photorf",
    "comprou-fast-album",
    "comprou-photo-drive",
]
TAGS_NUTRICAO_DEFAULT = [
    "nutricao_cloud_photorf",
]
TAGS_INICIOU_FLUXO_DEFAULT = [
    "iniciou_fluxo_compra_cloud",
    "iniciou_fluxo_compra_checkoutphotorf",
    "iniciou_fluxo_compra_ephotorf",
    "iniciou_fluxo_compra_fastalbum",
    "iniciou_fluxo_compra_photodrive",
    "iniciou_fluxo_trial_cloud",
    "iniciou_fluxo_trial_checkoutphotorf",
    "iniciou_fluxo_trial_ephotorf",
    "iniciou_fluxo_trial_fastalbum",
    "iniciou_fluxo_trial_photodrive",
]

bp = Blueprint("tracking", __name__)


# ============================================================
# Helpers
# ============================================================
def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


def _client_ip() -> str:
    """Pega o IP real do cliente respeitando proxy (Traefik/Nginx)."""
    fwd = request.headers.get("X-Forwarded-For", "")
    if fwd:
        return fwd.split(",")[0].strip()
    return request.headers.get("X-Real-IP") or request.remote_addr or ""


def _collect_utms() -> dict:
    """Coleta UTMs da querystring."""
    out = {}
    for k in UTM_FIELDS:
        v = (request.args.get(k) or "").strip()
        if v:
            out[k] = v
    return out


def _append_jsonl(path: Path, record: dict) -> None:
    """Append seguro (uma linha JSON por evento)."""
    try:
        with path.open("a", encoding="utf-8") as f:
            f.write(json.dumps(record, ensure_ascii=False) + "\n")
    except Exception:
        # Tracking nunca pode quebrar o request
        pass


def _clean_phone(raw: str | None) -> str:
    if not raw:
        return DEFAULT_WA_PHONE
    digits = re.sub(r"\D", "", raw)
    return digits or DEFAULT_WA_PHONE


def _build_wa_url(phone: str, text: str) -> str:
    base = f"https://wa.me/{phone}"
    if text:
        return f"{base}?text={urllib.parse.quote(text)}"
    return base


# ============================================================
# Routes de captura
# ============================================================
@bp.route("/track/wa", methods=["GET"])
def track_wa():
    """
    Registra clique e redireciona pro WhatsApp.
    """
    utms = _collect_utms()
    phone = _clean_phone(request.args.get("phone"))
    text = (request.args.get("text") or "").strip()

    record = {
        "ts": _now_iso(),
        "ip": _client_ip(),
        "user_agent": request.headers.get("User-Agent", ""),
        "referer": request.headers.get("Referer", ""),
        "phone": phone,
        **utms,
    }
    _append_jsonl(CLICKS_FILE, record)

    return redirect(_build_wa_url(phone, text), code=302)


@bp.route("/track/lp", methods=["GET", "POST"])
def track_lp():
    """
    Beacon de pageview da landing (GTM/JS chama via fetch).
    """
    utms = _collect_utms()

    extra = {}
    if request.is_json:
        try:
            body = request.get_json(silent=True) or {}
            for k in UTM_FIELDS:
                if not utms.get(k) and body.get(k):
                    utms[k] = str(body[k]).strip()
            for k in ("page", "title", "path", "event"):
                if body.get(k):
                    extra[k] = str(body[k])[:300]
        except Exception:
            pass

    for k in ("page", "title", "path", "event"):
        v = (request.args.get(k) or "").strip()
        if v and k not in extra:
            extra[k] = v[:300]

    record = {
        "ts": _now_iso(),
        "ip": _client_ip(),
        "user_agent": request.headers.get("User-Agent", ""),
        "referer": request.headers.get("Referer", ""),
        **utms,
        **extra,
    }
    _append_jsonl(PAGEVIEWS_FILE, record)

    resp = jsonify({"ok": True})
    resp.headers["Access-Control-Allow-Origin"] = "*"
    resp.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
    resp.headers["Access-Control-Allow-Headers"] = "Content-Type"
    return resp


@bp.route("/track/lp", methods=["OPTIONS"])
def track_lp_options():
    resp = jsonify({"ok": True})
    resp.headers["Access-Control-Allow-Origin"] = "*"
    resp.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
    resp.headers["Access-Control-Allow-Headers"] = "Content-Type"
    return resp


# ============================================================
# Agregacoes de cliques/pageviews (JSONL local)
# ============================================================
def _iter_jsonl(path: Path) -> Iterable[dict]:
    if not path.exists():
        return
    try:
        with path.open("r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    yield json.loads(line)
                except Exception:
                    continue
    except Exception:
        return


def _parse_ts(ts: str) -> datetime | None:
    if not ts:
        return None
    try:
        return datetime.fromisoformat(ts.replace("Z", "+00:00"))
    except Exception:
        return None


def carregar_clicks() -> list[dict]:
    return list(_iter_jsonl(CLICKS_FILE))


def carregar_pageviews() -> list[dict]:
    return list(_iter_jsonl(PAGEVIEWS_FILE))


def agregar_clicks(clicks: list[dict]) -> dict:
    agora = datetime.now(timezone.utc)
    h24 = agora - timedelta(hours=24)
    d7 = agora - timedelta(days=7)

    total = 0
    last_24 = 0
    last_7 = 0
    por_canal: dict[str, int] = {}
    por_campanha: dict[str, int] = {}

    for c in clicks:
        total += 1
        ts = _parse_ts(c.get("ts", ""))
        if ts:
            if ts >= h24:
                last_24 += 1
            if ts >= d7:
                last_7 += 1

        src = (c.get("utm_source") or "direto").strip().lower()
        med = (c.get("utm_medium") or "-").strip().lower()
        canal = f"{src} / {med}"
        por_canal[canal] = por_canal.get(canal, 0) + 1

        camp = (c.get("utm_campaign") or "sem-campanha").strip().lower()
        por_campanha[camp] = por_campanha.get(camp, 0) + 1

    def _ranking(d: dict[str, int], key_name: str) -> list[dict]:
        if not d:
            return []
        soma = sum(d.values()) or 1
        rows = [
            {key_name: k, "qtd": v, "pct": v / soma * 100}
            for k, v in d.items()
        ]
        rows.sort(key=lambda x: -x["qtd"])
        return rows

    return {
        "total": total,
        "ultimas_24h": last_24,
        "ultimos_7d": last_7,
        "por_canal": _ranking(por_canal, "canal"),
        "por_campanha": _ranking(por_campanha, "campanha"),
    }


def agregar_pageviews(pvs: list[dict]) -> dict:
    agora = datetime.now(timezone.utc)
    h24 = agora - timedelta(hours=24)
    d7 = agora - timedelta(days=7)
    total = 0
    last_24 = 0
    last_7 = 0
    for p in pvs:
        total += 1
        ts = _parse_ts(p.get("ts", ""))
        if ts:
            if ts >= h24:
                last_24 += 1
            if ts >= d7:
                last_7 += 1
    return {
        "total": total,
        "ultimas_24h": last_24,
        "ultimos_7d": last_7,
    }


# ============================================================
# Atribuicao real cruzando contatos GHL x tags x anuncios
# ============================================================
def carregar_tags_config() -> dict:
    """
    Le /tmp/px3_tags_linkia.json (mantido pela Amanda) com estrutura:
        {
          "compra":   [{"nome": "...", "id": "...", "produto": "..."}],
          "nutricao": [{"nome": "...", "id": "..."}],
          "iniciou_fluxo": [{"nome": "...", "id": "..."}]
        }
    Se nao existir, retorna default embutido.
    """
    if TAGS_FILE.exists():
        try:
            data = json.loads(TAGS_FILE.read_text(encoding="utf-8"))
            return {
                "compra": [t["nome"] for t in data.get("compra", [])] or list(TAGS_COMPRA_DEFAULT),
                "nutricao": [t["nome"] for t in data.get("nutricao", [])] or list(TAGS_NUTRICAO_DEFAULT),
                "iniciou_fluxo": [t["nome"] for t in data.get("iniciou_fluxo", [])] or list(TAGS_INICIOU_FLUXO_DEFAULT),
                "compra_produtos": {t["nome"]: t.get("produto", "") for t in data.get("compra", [])},
                "source": "file",
            }
        except Exception:
            pass
    return {
        "compra": list(TAGS_COMPRA_DEFAULT),
        "nutricao": list(TAGS_NUTRICAO_DEFAULT),
        "iniciou_fluxo": list(TAGS_INICIOU_FLUXO_DEFAULT),
        "compra_produtos": {},
        "source": "default",
    }


def _classificar_canal(attributions: list[dict] | None) -> str:
    """
    Classifica o canal do lead com base nas atribuicoes do GHL.

    Retorna: 'meta' | 'google' | 'whatsapp' | 'crm' | 'direto'
    """
    if not attributions:
        return "direto"
    # Percorre da mais antiga (isFirst=True) primeiro
    ordered = sorted(attributions, key=lambda a: 0 if a.get("isFirst") else 1)
    for att in ordered:
        us = (att.get("utmSource") or "").lower()
        um = (att.get("utmMedium") or att.get("medium") or "").lower()
        ss = (att.get("utmSessionSource") or att.get("sessionSource") or "").lower()

        # UTM explicita (quando existir)
        if "facebook" in us or "meta" in us or "instagram" in us:
            return "meta"
        if "google" in us or "adwords" in us:
            return "google"

        # Medium sinalizando canal social vindo do Facebook/IG
        if um in ("facebook", "instagram", "meta") or um == "fb":
            return "meta"
        if um in ("google", "adwords"):
            return "google"
        if um in ("whatsapp", "whatsapp_coex", "wa"):
            return "whatsapp"

        # SessionSource "Social media" no GHL geralmente e Meta (IG/FB DM ou Lead Ad)
        if "social media" in ss and um and um != "manual":
            return "meta"

        if ss.startswith("crm") or um == "manual":
            return "crm"

    return "direto"


def buscar_todos_contatos_ghl(
    headers: dict,
    location_id: str,
    limit_pages: int = 40,
    pausa: float = 0.4,
) -> list[dict]:
    """
    Paginacao completa via GET /contacts/?locationId=...
    Cada pagina retorna ate 100. Ate `limit_pages` paginas => ~100*limit_pages contatos.

    Faz retry inteligente em 429 (rate limit). Se um erro persistir, retorna o que
    ja conseguiu carregar (nao explode).
    """
    url = "https://services.leadconnectorhq.com/contacts/"
    params = {"locationId": location_id, "limit": 100}
    contatos: list[dict] = []

    for page_i in range(limit_pages):
        # Retry para 429
        d = None
        for tentativa in range(4):
            try:
                r = requests.get(url, headers=headers, params=params, timeout=45)
            except Exception:
                time.sleep(2 + tentativa * 2)
                continue
            if r.status_code == 429:
                # Rate limit: espera crescente
                time.sleep(5 + tentativa * 5)
                continue
            if r.status_code != 200:
                d = None
                break
            try:
                d = r.json()
            except Exception:
                d = None
            break

        if not d:
            # Persistiu erro - retorna o que temos ate agora
            break

        batch = d.get("contacts", []) or []
        if not batch:
            break
        contatos.extend(batch)

        meta = d.get("meta", {}) or {}
        next_start = meta.get("startAfter")
        next_id = meta.get("startAfterId")
        if not next_start and not meta.get("nextPageUrl"):
            break
        if not next_start:
            break
        params["startAfter"] = next_start
        params["startAfterId"] = next_id
        time.sleep(pausa)
    return contatos


def _parse_dt(v: str) -> datetime | None:
    if not v:
        return None
    s = v.replace("Z", "+00:00")
    try:
        return datetime.fromisoformat(s)
    except Exception:
        return None


def analisar_atribuicao(contatos: list[dict], tags_cfg: dict) -> dict:
    """
    Recebe lista de contatos GHL crus e retorna dicionario com:

    - por_canal: {'meta': {...}, 'google': {...}, 'whatsapp': {...}, 'crm': {...}, 'direto': {...}}
        cada valor contem: leads, compras, taxa_conv, tempos_dias (list),
        tempo_medio_dias, tempo_mediano_dias, distrib_faixas
    - serie_diaria: [{data:'YYYY-MM-DD', meta:X, google:Y, whatsapp:Z, crm:A, direto:B}]
    - total: {'leads': N, 'compras': N, 'com_nutricao': N, 'iniciou_fluxo': N}
    - tempos_globais: {'media': X, 'mediana': X, 'faixas': {'<7':N, '7-30':N, '30-90':N, '>90':N}}
    """
    canais = ["meta", "google", "whatsapp", "crm", "direto"]

    por_canal: dict[str, dict] = {
        c: {
            "leads": 0,
            "compras": 0,
            "com_nutricao": 0,
            "iniciou_fluxo": 0,
            "tempos_dias": [],
        }
        for c in canais
    }

    serie_map: dict[str, dict[str, int]] = {}  # data -> {canal: qtd}

    total_com_nutricao = 0
    total_iniciou_fluxo = 0
    total_compras = 0
    tempos_globais: list[float] = []

    tags_compra = set(t.lower() for t in tags_cfg.get("compra", []))
    tags_nutricao = set(t.lower() for t in tags_cfg.get("nutricao", []))
    tags_iniciou = set(t.lower() for t in tags_cfg.get("iniciou_fluxo", []))

    for c in contatos:
        tags = [str(t).lower() for t in (c.get("tags") or [])]
        canal = _classificar_canal(c.get("attributions"))
        if canal not in por_canal:
            canal = "direto"

        por_canal[canal]["leads"] += 1

        # Serie diaria por dateAdded
        dt_add = _parse_dt(c.get("dateAdded") or "")
        if dt_add:
            data_iso = dt_add.astimezone(timezone.utc).strftime("%Y-%m-%d")
            if data_iso not in serie_map:
                serie_map[data_iso] = {k: 0 for k in canais}
            serie_map[data_iso][canal] += 1

        tem_nutricao = any(t in tags_nutricao for t in tags)
        tem_iniciou = any(t in tags_iniciou for t in tags)
        comprou = any(t in tags_compra for t in tags)

        if tem_nutricao:
            por_canal[canal]["com_nutricao"] += 1
            total_com_nutricao += 1
        if tem_iniciou:
            por_canal[canal]["iniciou_fluxo"] += 1
            total_iniciou_fluxo += 1
        if comprou:
            por_canal[canal]["compras"] += 1
            total_compras += 1
            # Tempo entre entrada e compra: dateAdded -> dateUpdated (aproximacao)
            # GHL nao expoe timestamp de tag; usamos dateUpdated como proxy da ultima acao (tag de compra)
            dt_upd = _parse_dt(c.get("dateUpdated") or "")
            if dt_add and dt_upd and dt_upd >= dt_add:
                dias = (dt_upd - dt_add).total_seconds() / 86400.0
                por_canal[canal]["tempos_dias"].append(dias)
                tempos_globais.append(dias)

    # Consolidar por canal
    def _faixas(tempos: list[float]) -> dict:
        b = {"<7": 0, "7-30": 0, "30-90": 0, ">90": 0}
        for d in tempos:
            if d < 7:
                b["<7"] += 1
            elif d < 30:
                b["7-30"] += 1
            elif d < 90:
                b["30-90"] += 1
            else:
                b[">90"] += 1
        return b

    def _mediana(vals: list[float]) -> float:
        if not vals:
            return 0.0
        s = sorted(vals)
        n = len(s)
        if n % 2:
            return s[n // 2]
        return (s[n // 2 - 1] + s[n // 2]) / 2.0

    for canal, dados in por_canal.items():
        tempos = dados["tempos_dias"]
        dados["tempo_medio_dias"] = round(sum(tempos) / len(tempos), 1) if tempos else 0.0
        dados["tempo_mediano_dias"] = round(_mediana(tempos), 1) if tempos else 0.0
        dados["distrib_faixas"] = _faixas(tempos)
        dados["taxa_conv"] = round(dados["compras"] / dados["leads"] * 100, 2) if dados["leads"] else 0.0
        # Remover a lista bruta antes de serializar (fica pesada e nao usada no front)
        dados["amostra_tempos"] = len(tempos)
        del dados["tempos_dias"]

    # Serie diaria ordenada
    serie_diaria = []
    for data_iso in sorted(serie_map.keys()):
        row = {"data": data_iso, **serie_map[data_iso]}
        serie_diaria.append(row)

    total_leads = sum(por_canal[c]["leads"] for c in canais)

    return {
        "por_canal": por_canal,
        "canais": canais,
        "serie_diaria": serie_diaria,
        "total": {
            "leads": total_leads,
            "compras": total_compras,
            "com_nutricao": total_com_nutricao,
            "iniciou_fluxo": total_iniciou_fluxo,
        },
        "tempos_globais": {
            "media": round(sum(tempos_globais) / len(tempos_globais), 1) if tempos_globais else 0.0,
            "mediana": round(_mediana(tempos_globais), 1) if tempos_globais else 0.0,
            "faixas": _faixas(tempos_globais),
            "amostra": len(tempos_globais),
        },
        "tags_source": tags_cfg.get("source", "default"),
        "gerado_em": datetime.now(timezone.utc).isoformat(timespec="seconds"),
    }


# Cache in-memory: analise de atribuicao (custosa por causa da paginacao GHL)
_ATRIB_CACHE: dict = {"data": None, "ts": 0.0}
_ATRIB_TTL = 1800  # 30 min


def coletar_atribuicao_cached(
    headers: dict,
    location_id: str,
    force: bool = False,
) -> dict:
    """Cache de 30 min para nao esgotar rate limit do GHL.

    Se a nova coleta trouxer muito menos contatos que o cache anterior
    (ex: rate limit), mantem o cache antigo pra nao regredir os numeros.
    """
    now = time.time()
    if not force and _ATRIB_CACHE["data"] and (now - _ATRIB_CACHE["ts"]) < _ATRIB_TTL:
        return _ATRIB_CACHE["data"]
    tags_cfg = carregar_tags_config()
    contatos = buscar_todos_contatos_ghl(headers, location_id, limit_pages=100)
    resultado = analisar_atribuicao(contatos, tags_cfg)
    resultado["contatos_analisados"] = len(contatos)

    # Se o cache anterior tem mais contatos, preserva
    old = _ATRIB_CACHE.get("data")
    if old and old.get("contatos_analisados", 0) > len(contatos) * 1.3 and len(contatos) < 200:
        # Marca no old que houve tentativa parcial
        old["ultima_tentativa_ts"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
        old["ultima_tentativa_size"] = len(contatos)
        _ATRIB_CACHE["ts"] = now  # segura o TTL pra nao ficar retentando
        return old

    _ATRIB_CACHE["data"] = resultado
    _ATRIB_CACHE["ts"] = now
    return resultado


def calcular_cpa_real(
    atribuicao: dict,
    gasto_meta: float,
    gasto_google: float,
) -> dict:
    """
    Retorna:
      {
        'meta':   {'gasto': X, 'compras': N, 'cpa_real': Y, 'cpl_real': Z, 'leads': N},
        'google': {...},
        'total':  {'gasto': X, 'compras': N, 'cpa_real': Y}
      }
    """
    por_canal = atribuicao.get("por_canal", {})
    meta = por_canal.get("meta", {})
    google = por_canal.get("google", {})

    def _bloco(gasto: float, leads: int, compras: int) -> dict:
        return {
            "gasto": round(gasto, 2),
            "leads": leads,
            "compras": compras,
            "cpa_real": round(gasto / compras, 2) if compras else 0.0,
            "cpl_real": round(gasto / leads, 2) if leads else 0.0,
        }

    bloco_meta = _bloco(gasto_meta, meta.get("leads", 0), meta.get("compras", 0))
    bloco_google = _bloco(gasto_google, google.get("leads", 0), google.get("compras", 0))

    gasto_total = gasto_meta + gasto_google
    compras_total = bloco_meta["compras"] + bloco_google["compras"]
    leads_total = bloco_meta["leads"] + bloco_google["leads"]

    return {
        "meta": bloco_meta,
        "google": bloco_google,
        "total": {
            "gasto": round(gasto_total, 2),
            "leads": leads_total,
            "compras": compras_total,
            "cpa_real": round(gasto_total / compras_total, 2) if compras_total else 0.0,
            "cpl_real": round(gasto_total / leads_total, 2) if leads_total else 0.0,
        },
    }
