#!/usr/bin/env python3
"""
Meta CAPI - PX3 Lab (PageView + Purchase)
=========================================

Servico HTTP interno que envia eventos server-side pro pixel Meta
1509133033542161 (PX3 Lab / Cloud PhotoRF / Server PhotoRF).

Endpoints publicos:
  POST/GET  https://track-capi-px3.agentesclimb.us/pageview
  POST      https://track-capi-px3.agentesclimb.us/purchase
  GET       https://track-capi-px3.agentesclimb.us/health

Porta local: 8918  (Traefik -> 172.18.0.1:8918)

Fluxo /pageview:
  1. Recebe GET ou POST com fbclid, gclid, UTMs, client_ip, client_ua,
     event_source_url.
  2. Dedupe SQLite por fbclid (janela 1h).
  3. Monta payload CAPI padrao Meta (fbc, user_data, event_id).
  4. POST em graph.facebook.com/v22.0/{pixel}/events?access_token=...
  5. Marca (event_type=pageview, key=fbclid) na tabela events_sent.

Fluxo /purchase:
  1. Recebe POST JSON com email, phone, first_name, last_name, city,
     state, zip, country, value, currency, content_name, content_ids,
     num_items, event_id, fbclid (opcional), client_ip (opcional),
     client_user_agent (opcional).
  2. Dedupe por event_id (janela 7 dias) - o N8N garante unicidade
     via padrao vra-<execution_id>.
  3. Monta payload Meta Purchase com user_data hasheado SHA-256
     (em, ph, fn, ln, ct, st, zp, country) + custom_data (value,
     currency, content_*).
  4. Envia pra Graph API e grava dedupe.

Config: /opt/mia/config/meta_capi_px3.env
Log:    /opt/mia/logs/track_capi_px3.log
DB:     /opt/mia/workspace/clientes/px3lab/capi_pageview/dedupe.db
"""

from __future__ import annotations

import hashlib
import json
import logging
import os
import re
import sqlite3
import sys
import threading
import time
import unicodedata
from datetime import datetime, timezone
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any

import requests
from flask import Flask, jsonify, request

BASE_DIR = Path(__file__).resolve().parent

# ---------------------------------------------------------------------------
# Logging (rotativo + stdout)
# ---------------------------------------------------------------------------
LOG_FILE = Path("/opt/mia/logs/track_capi_px3.log")
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)

_fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
_file_handler = RotatingFileHandler(
    LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8"
)
_file_handler.setFormatter(_fmt)
_stream_handler = logging.StreamHandler(sys.stdout)
_stream_handler.setFormatter(_fmt)
logging.basicConfig(level=logging.INFO, handlers=[_file_handler, _stream_handler])
log = logging.getLogger("capi_px3")

# ---------------------------------------------------------------------------
# Env loading (dotenv com fallback manual)
# ---------------------------------------------------------------------------
try:
    from dotenv import load_dotenv
    load_dotenv("/opt/mia/config/meta_capi_px3.env")
except ImportError:
    envfile = "/opt/mia/config/meta_capi_px3.env"
    if os.path.exists(envfile):
        with open(envfile, encoding="utf-8") as f:
            for raw in f:
                line = raw.strip()
                if not line or line.startswith("#") or "=" not in line:
                    continue
                k, v = line.split("=", 1)
                os.environ.setdefault(k.strip(), v.strip())

META_API_VERSION = os.environ.get("META_API_VERSION", "v22.0")
META_PIXEL_ID = os.environ.get("META_PIXEL_ID_PX3", "").strip()
META_TOKEN = os.environ.get("META_CAPI_TOKEN", "").strip()
META_TEST_EVENT_CODE = os.environ.get("META_TEST_EVENT_CODE", "").strip()

if not META_PIXEL_ID or not META_TOKEN:
    log.critical("Config incompleta: META_PIXEL_ID_PX3 ou META_CAPI_TOKEN vazio")
    sys.exit(1)

META_ENDPOINT = (
    f"https://graph.facebook.com/{META_API_VERSION}/{META_PIXEL_ID}/events"
)

# ---------------------------------------------------------------------------
# SQLite dedupe (multi-evento)
# ---------------------------------------------------------------------------
DB_PATH = BASE_DIR / "dedupe.db"

# TTL da JANELA de dedupe por tipo (skip se ja enviou dentro desse tempo).
DEDUPE_TTL_BY_TYPE = {
    "pageview": 60 * 60,               # 1h  - conforme spec original
    "purchase": 7 * 24 * 60 * 60,      # 7 dias - janela de atribuicao Meta
    "lead":     7 * 24 * 60 * 60,      # 7 dias - janela suficiente pra dedupe browser<>server
}
DEDUPE_TTL_DEFAULT = 60 * 60

# TTL da RETENCAO no DB (limpeza defensiva na boot).
CLEANUP_TTL_SECONDS = 30 * 24 * 60 * 60  # 30 dias

_db_lock = threading.Lock()


def _db():
    conn = sqlite3.connect(str(DB_PATH), timeout=5.0)
    conn.execute("PRAGMA journal_mode=WAL")
    return conn


def _init_db() -> None:
    with _db_lock, _db() as conn:
        # tabela legada (mantida por compat; nao e mais consultada, so escrita
        # no /pageview enquanto migramos, pra permitir rollback rapido).
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS sent (
                fbclid TEXT PRIMARY KEY,
                ts     INTEGER NOT NULL
            )
            """
        )
        conn.execute("CREATE INDEX IF NOT EXISTS idx_sent_ts ON sent(ts)")

        # nova tabela multi-evento: chave composta event_type + event_key.
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS events_sent (
                event_type TEXT NOT NULL,
                event_key  TEXT NOT NULL,
                ts         INTEGER NOT NULL,
                PRIMARY KEY (event_type, event_key)
            )
            """
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_events_sent_ts ON events_sent(ts)"
        )

        # limpeza defensiva > 30 dias em ambas
        cutoff = int(time.time()) - CLEANUP_TTL_SECONDS
        d1 = conn.execute("DELETE FROM sent WHERE ts < ?", (cutoff,)).rowcount
        d2 = conn.execute(
            "DELETE FROM events_sent WHERE ts < ?", (cutoff,)
        ).rowcount
        conn.commit()
        if d1 or d2:
            log.info(
                "dedupe cleanup: sent=%d events_sent=%d registros removidos",
                d1,
                d2,
            )


def _is_duplicate(event_type: str, event_key: str) -> bool:
    """True se (event_type, event_key) ja foi enviado dentro do TTL do tipo."""
    if not event_key:
        return False
    ttl = DEDUPE_TTL_BY_TYPE.get(event_type, DEDUPE_TTL_DEFAULT)
    cutoff = int(time.time()) - ttl
    with _db_lock, _db() as conn:
        row = conn.execute(
            "SELECT ts FROM events_sent "
            "WHERE event_type = ? AND event_key = ? AND ts >= ?",
            (event_type, event_key, cutoff),
        ).fetchone()
    return row is not None


def _mark_sent(event_type: str, event_key: str) -> None:
    if not event_key:
        return
    now = int(time.time())
    with _db_lock, _db() as conn:
        conn.execute(
            "INSERT OR REPLACE INTO events_sent "
            "(event_type, event_key, ts) VALUES (?, ?, ?)",
            (event_type, event_key, now),
        )
        # espelha no legado enquanto /pageview usa fbclid como chave
        if event_type == "pageview":
            conn.execute(
                "INSERT OR REPLACE INTO sent (fbclid, ts) VALUES (?, ?)",
                (event_key, now),
            )
        conn.commit()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _client_ip_from_request() -> str:
    """Extrai IP real considerando proxies (X-Forwarded-For)."""
    xff = request.headers.get("X-Forwarded-For", "")
    if xff:
        return xff.split(",")[0].strip()
    return request.remote_addr or ""


def _build_fbc(fbclid: str, ts: int | None = None) -> str:
    """Formato Meta: fb.1.<ts_ms_or_s>.<fbclid>"""
    if not fbclid:
        return ""
    if ts is None:
        ts = int(time.time())
    return f"fb.1.{ts}.{fbclid}"


def _event_id(fbclid: str, ip: str, ua: str, ts_min: int) -> str:
    """Se tiver fbclid, ele e o event_id (garante dedupe com pixel browser).
    Senao, hash(ip+ua+minuto) pra permitir dedupe entre chamadas repetidas
    do mesmo user no mesmo minuto."""
    if fbclid:
        return fbclid
    raw = f"{ip}|{ua}|{ts_min}".encode("utf-8")
    return hashlib.sha256(raw).hexdigest()[:32]


def _collect_params() -> dict[str, str]:
    """Aceita GET (query string) ou POST (JSON ou form)."""
    src: dict[str, Any] = {}
    if request.is_json:
        body = request.get_json(silent=True) or {}
        if isinstance(body, dict):
            src.update(body)
    if request.form:
        for k in request.form:
            src.setdefault(k, request.form.get(k))
    for k in request.args:
        src.setdefault(k, request.args.get(k))

    out: dict[str, str] = {}
    for key in (
        "fbclid",
        "gclid",
        "utm_source",
        "utm_medium",
        "utm_campaign",
        "utm_content",
        "utm_term",
        "client_ip",
        "client_ua",
        "event_source_url",
    ):
        v = src.get(key)
        if v is None:
            continue
        out[key] = str(v).strip()[:2048]
    return out


# ---------------------------------------------------------------------------
# CAPI payload + envio
# ---------------------------------------------------------------------------
def _build_payload(params: dict[str, str], client_ip: str, client_ua: str) -> dict:
    now = int(time.time())
    fbclid = params.get("fbclid", "")
    event_source_url = params.get(
        "event_source_url", "https://www.px3lab.com.br/server-photorf/"
    )

    user_data: dict[str, Any] = {
        "client_ip_address": client_ip,
        "client_user_agent": client_ua,
    }
    if fbclid:
        user_data["fbc"] = _build_fbc(fbclid, now)

    custom_data: dict[str, Any] = {
        "content_name": "Server PhotoRF",
    }
    # UTMs vao em custom_data como referencia (nao entram em user_data)
    for k in ("utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"):
        v = params.get(k)
        if v:
            custom_data[k] = v

    ts_min = now // 60
    evt = {
        "event_name": "PageView",
        "event_time": now,
        "action_source": "website",
        "event_source_url": event_source_url,
        "event_id": _event_id(fbclid, client_ip, client_ua, ts_min),
        "user_data": user_data,
        "custom_data": custom_data,
    }

    payload: dict[str, Any] = {"data": [evt]}
    if META_TEST_EVENT_CODE:
        payload["test_event_code"] = META_TEST_EVENT_CODE
    return payload


def _send_meta(payload: dict) -> tuple[int, str]:
    try:
        r = requests.post(
            META_ENDPOINT,
            params={"access_token": META_TOKEN},
            json=payload,
            timeout=6,
        )
        return r.status_code, r.text
    except requests.RequestException as e:
        return -1, f"REQ_ERROR: {e}"


# ---------------------------------------------------------------------------
# Normalizacao + hashing (Meta CAPI - Purchase)
# ---------------------------------------------------------------------------
def _strip_accents(s: str) -> str:
    """Remove acentos: 'Sao Paulo' -> 'sao paulo'."""
    if not s:
        return ""
    nfkd = unicodedata.normalize("NFKD", s)
    return "".join(c for c in nfkd if not unicodedata.combining(c))


def _sha256(value: str) -> str:
    return hashlib.sha256(value.encode("utf-8")).hexdigest()


def _norm_email(v: str) -> str:
    return (v or "").strip().lower()


def _norm_phone(v: str) -> str:
    """So digitos (Meta espera E.164 sem '+', ex: 5511999999999)."""
    return re.sub(r"\D", "", v or "")


def _norm_name(v: str) -> str:
    """Lowercase + strip + remove acentos. Nao remove espaco (nome composto)."""
    return _strip_accents((v or "").strip().lower())


def _norm_city(v: str) -> str:
    """Lowercase + strip acentos + remove espacos e pontuacao."""
    base = _strip_accents((v or "").strip().lower())
    return re.sub(r"[^a-z0-9]", "", base)


def _norm_state(v: str) -> str:
    """Lowercase (Meta aceita sigla; 'rs' funciona)."""
    return _strip_accents((v or "").strip().lower())


def _norm_zip(v: str) -> str:
    """So digitos."""
    return re.sub(r"\D", "", v or "")


def _norm_country(v: str) -> str:
    """Lowercase ISO 3166-1 alpha-2 ('br')."""
    return (v or "").strip().lower()


def _hash_if(value: str) -> list[str]:
    """Retorna [sha256(value)] se value for truthy, senao lista vazia.
    Meta CAPI espera array pra em/ph/fn/ln/ct/st/zp/country."""
    return [_sha256(value)] if value else []


# ---------------------------------------------------------------------------
# CAPI payload - Purchase
# ---------------------------------------------------------------------------
def _build_purchase_payload(body: dict[str, Any]) -> tuple[dict, dict]:
    """Retorna (payload_meta, log_info). Nao envia."""
    now = int(time.time())

    email = _norm_email(str(body.get("email", "")))
    phone = _norm_phone(str(body.get("phone", "")))
    first_name = _norm_name(str(body.get("first_name", "")))
    last_name = _norm_name(str(body.get("last_name", "")))
    city = _norm_city(str(body.get("city", "")))
    state = _norm_state(str(body.get("state", "")))
    zip_code = _norm_zip(str(body.get("zip", "")))
    country = _norm_country(str(body.get("country", "BR")))

    fbclid = str(body.get("fbclid", "")).strip()
    client_ip = str(body.get("client_ip", "")).strip()
    client_ua = str(body.get("client_user_agent", "")).strip()

    event_id = str(body.get("event_id", "")).strip()
    if not event_id:
        # fallback defensivo - N8N deveria mandar sempre
        event_id = f"purchase-fallback-{now}-{_sha256(email)[:8]}"

    user_data: dict[str, Any] = {}
    if email:
        user_data["em"] = _hash_if(email)
    if phone:
        user_data["ph"] = _hash_if(phone)
    if first_name:
        user_data["fn"] = _hash_if(first_name)
    if last_name:
        user_data["ln"] = _hash_if(last_name)
    if city:
        user_data["ct"] = _hash_if(city)
    if state:
        user_data["st"] = _hash_if(state)
    if zip_code:
        user_data["zp"] = _hash_if(zip_code)
    if country:
        user_data["country"] = _hash_if(country)
    if client_ip:
        user_data["client_ip_address"] = client_ip
    if client_ua:
        user_data["client_user_agent"] = client_ua
    if fbclid:
        user_data["fbc"] = _build_fbc(fbclid, now)

    # custom_data
    try:
        value = float(body.get("value", 0) or 0)
    except (TypeError, ValueError):
        value = 0.0
    currency = str(body.get("currency", "BRL")).strip().upper() or "BRL"
    content_name = str(body.get("content_name", "")).strip()
    content_ids = body.get("content_ids") or []
    if isinstance(content_ids, str):
        content_ids = [content_ids]
    try:
        num_items = int(body.get("num_items", 0) or 0)
    except (TypeError, ValueError):
        num_items = 0

    custom_data: dict[str, Any] = {
        "currency": currency,
        "value": value,
        "content_type": "product",
        "content_category": "software_credit",
    }
    if content_name:
        custom_data["content_name"] = content_name
    if content_ids:
        custom_data["content_ids"] = list(content_ids)
    if num_items:
        custom_data["num_items"] = num_items

    evt = {
        "event_name": "Purchase",
        "event_time": now,
        "action_source": "system_generated",
        "event_source_url": "https://sistema.app.px3lab.com.br/",
        "event_id": event_id,
        "user_data": user_data,
        "custom_data": custom_data,
    }

    payload: dict[str, Any] = {"data": [evt]}
    # Prioridade: test_event_code do body (override pontual, ex: backfill)
    # senao usa o META_TEST_EVENT_CODE global do env.
    body_test_code = str(body.get("test_event_code", "")).strip()
    if body_test_code:
        payload["test_event_code"] = body_test_code
    elif META_TEST_EVENT_CODE:
        payload["test_event_code"] = META_TEST_EVENT_CODE

    log_info = {
        "event_id": event_id,
        "value": value,
        "currency": currency,
        "content_name": content_name,
        "num_items": num_items,
        "has_fbclid": bool(fbclid),
        "has_ip": bool(client_ip),
        "has_ua": bool(client_ua),
        "test_event_code": payload.get("test_event_code", ""),
    }
    return payload, log_info


# ---------------------------------------------------------------------------
# CAPI payload - Lead (waitlist / lista de espera)
# ---------------------------------------------------------------------------
def _build_lead_payload(body: dict[str, Any]) -> tuple[dict, dict]:
    """Constroi payload Meta Lead server-side.

    Espera:
      event_id (obrigatorio - MESMO do fbq browser p/ dedupe),
      email, phone, first_name (opcionais mas fortemente recomendados),
      client_ip, client_user_agent, event_source_url,
      fbclid, fbp (cookie _fbp), utms.
    """
    now = int(time.time())

    email = _norm_email(str(body.get("email", "")))
    phone = _norm_phone(str(body.get("phone", "")))
    first_name = _norm_name(str(body.get("first_name", "")))
    last_name = _norm_name(str(body.get("last_name", "")))
    country = _norm_country(str(body.get("country", "BR")))

    fbclid = str(body.get("fbclid", "")).strip()
    fbp = str(body.get("fbp", "")).strip()
    client_ip = str(body.get("client_ip", "")).strip()
    client_ua = str(body.get("client_user_agent", "")).strip()
    event_source_url = str(
        body.get("event_source_url", "https://espera.px3lab.com.br/")
    ).strip() or "https://espera.px3lab.com.br/"

    event_id = str(body.get("event_id", "")).strip()
    if not event_id:
        event_id = f"lead-fallback-{now}-{_sha256(email or phone)[:8]}"

    user_data: dict[str, Any] = {}
    if email:
        user_data["em"] = _hash_if(email)
    if phone:
        user_data["ph"] = _hash_if(phone)
    if first_name:
        user_data["fn"] = _hash_if(first_name)
    if last_name:
        user_data["ln"] = _hash_if(last_name)
    if country:
        user_data["country"] = _hash_if(country)
    if client_ip:
        user_data["client_ip_address"] = client_ip
    if client_ua:
        user_data["client_user_agent"] = client_ua
    if fbclid:
        user_data["fbc"] = _build_fbc(fbclid, now)
    if fbp:
        user_data["fbp"] = fbp

    custom_data: dict[str, Any] = {
        "content_name": str(body.get("content_name", "Waitlist Cloud PhotoRF 2.0")),
        "content_category": "waitlist",
    }
    # UTMs opcionais
    for k in ("utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"):
        v = body.get(k)
        if v:
            custom_data[k] = str(v)[:512]

    evt = {
        "event_name": "Lead",
        "event_time": now,
        "action_source": "website",
        "event_source_url": event_source_url,
        "event_id": event_id,
        "user_data": user_data,
        "custom_data": custom_data,
    }

    payload: dict[str, Any] = {"data": [evt]}
    body_test_code = str(body.get("test_event_code", "")).strip()
    if body_test_code:
        payload["test_event_code"] = body_test_code
    elif META_TEST_EVENT_CODE:
        payload["test_event_code"] = META_TEST_EVENT_CODE

    log_info = {
        "event_id": event_id,
        "has_email": bool(email),
        "has_phone": bool(phone),
        "has_fbclid": bool(fbclid),
        "has_fbp": bool(fbp),
        "has_ip": bool(client_ip),
        "has_ua": bool(client_ua),
        "event_source_url": event_source_url,
        "test_event_code": payload.get("test_event_code", ""),
    }
    return payload, log_info


# ---------------------------------------------------------------------------
# Flask app
# ---------------------------------------------------------------------------
app = Flask(__name__)


@app.get("/health")
def health():
    return jsonify(
        {
            "ok": True,
            "service": "capi_pageview_px3",
            "pixel": META_PIXEL_ID,
            "test_mode": bool(META_TEST_EVENT_CODE),
            "api_version": META_API_VERSION,
        }
    )


@app.route("/pageview", methods=["GET", "POST"])
def pageview():
    params = _collect_params()

    # IP/UA: preferir o que veio do dashboard-px3 (client_ip/client_ua),
    # senao usar o do proprio request (fallback).
    client_ip = params.get("client_ip") or _client_ip_from_request()
    client_ua = params.get("client_ua") or request.headers.get("User-Agent", "")

    fbclid = params.get("fbclid", "")

    # Dedupe
    if fbclid and _is_duplicate("pageview", fbclid):
        log.info(
            "SKIP dedupe pageview fbclid=%s utm_source=%s",
            fbclid[:32],
            params.get("utm_source", ""),
        )
        return jsonify({"ok": True, "dedupe": True})

    payload = _build_payload(params, client_ip, client_ua)
    status, body = _send_meta(payload)

    log_line = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "status": status,
        "fbclid": fbclid[:32] if fbclid else "",
        "utm_source": params.get("utm_source", ""),
        "utm_campaign": params.get("utm_campaign", ""),
        "ip": client_ip,
        "test_mode": bool(META_TEST_EVENT_CODE),
        "resp": body[:400],
    }
    log.info("CAPI %s", json.dumps(log_line, ensure_ascii=False))

    if 200 <= status < 300:
        _mark_sent("pageview", fbclid)
        return jsonify({"ok": True, "meta_status": status})

    return jsonify({"ok": False, "meta_status": status, "resp": body[:400]}), 502


@app.post("/purchase")
def purchase():
    """Recebe compra real (N8N do PX3) e envia como Purchase server-side.

    Body JSON esperado (campos minimos: email OU phone, value, event_id):
      {
        "email": "...", "phone": "...",
        "first_name": "...", "last_name": "...",
        "city": "...", "state": "...", "zip": "...", "country": "BR",
        "value": 900.00, "currency": "BRL",
        "content_name": "CLOUD PHOTORF",
        "content_ids": ["CLOUD_PHOTORF"],
        "num_items": 10000,
        "event_id": "vra-9436",
        "fbclid": "(opcional)",
        "client_ip": "(opcional)",
        "client_user_agent": "(opcional)"
      }
    """
    body = request.get_json(silent=True) or {}
    if not isinstance(body, dict):
        return jsonify({"ok": False, "error": "invalid_json"}), 400

    event_id = str(body.get("event_id", "")).strip()
    email = str(body.get("email", "")).strip()
    phone = str(body.get("phone", "")).strip()
    if not email and not phone:
        return jsonify({"ok": False, "error": "email_or_phone_required"}), 400

    try:
        value = float(body.get("value", 0) or 0)
    except (TypeError, ValueError):
        value = 0.0
    if value <= 0:
        return jsonify({"ok": False, "error": "value_required"}), 400

    # Dedupe por event_id (janela 7 dias)
    if event_id and _is_duplicate("purchase", event_id):
        log.info("SKIP dedupe purchase event_id=%s", event_id)
        return jsonify({"ok": True, "dedupe": True, "event_id": event_id})

    payload, meta = _build_purchase_payload(body)
    status, resp_body = _send_meta(payload)

    log_line = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "event": "Purchase",
        "status": status,
        "test_mode": bool(META_TEST_EVENT_CODE),
        **meta,
        "resp": resp_body[:400],
    }
    log.info("CAPI %s", json.dumps(log_line, ensure_ascii=False))

    if 200 <= status < 300:
        _mark_sent("purchase", meta["event_id"])
        return jsonify(
            {
                "ok": True,
                "meta_status": status,
                "event_id": meta["event_id"],
                "resp": resp_body[:400],
            }
        )

    return (
        jsonify(
            {
                "ok": False,
                "meta_status": status,
                "event_id": meta["event_id"],
                "resp": resp_body[:400],
            }
        ),
        502,
    )


@app.post("/lead")
def lead():
    """Recebe Lead da landing (server-side) e envia ao Meta CAPI.

    Body JSON esperado:
      {
        "event_id": "uuid gerado no browser (fbq eventID)",   # obrigatorio p/ dedupe
        "email":    "...",
        "phone":    "5511987654321",
        "first_name": "...", "last_name": "...",
        "client_ip": "1.2.3.4",           # opcional (senao pega do request)
        "client_user_agent": "...",       # opcional (senao pega do request)
        "event_source_url": "https://espera.px3lab.com.br/",
        "fbclid": "(opcional)",
        "fbp":    "(opcional, cookie _fbp)",
        "utm_source": ..., "utm_medium": ..., "utm_campaign": ...,
        "test_event_code": "(opcional, override)"
      }
    """
    body = request.get_json(silent=True) or {}
    if not isinstance(body, dict):
        return jsonify({"ok": False, "error": "invalid_json"}), 400

    email = str(body.get("email", "")).strip()
    phone = str(body.get("phone", "")).strip()
    if not email and not phone:
        return jsonify({"ok": False, "error": "email_or_phone_required"}), 400

    event_id = str(body.get("event_id", "")).strip()

    # Se o caller nao passou IP/UA, cai pro request atual
    if not body.get("client_ip"):
        body["client_ip"] = _client_ip_from_request()
    if not body.get("client_user_agent"):
        body["client_user_agent"] = request.headers.get("User-Agent", "")

    # Dedupe por event_id (janela 7 dias)
    if event_id and _is_duplicate("lead", event_id):
        log.info("SKIP dedupe lead event_id=%s", event_id)
        return jsonify({"ok": True, "dedupe": True, "event_id": event_id})

    payload, meta = _build_lead_payload(body)
    status, resp_body = _send_meta(payload)

    log_line = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "event": "Lead",
        "status": status,
        "test_mode": bool(META_TEST_EVENT_CODE),
        **meta,
        "resp": resp_body[:400],
    }
    log.info("CAPI %s", json.dumps(log_line, ensure_ascii=False))

    if 200 <= status < 300:
        _mark_sent("lead", meta["event_id"])
        return jsonify(
            {
                "ok": True,
                "meta_status": status,
                "event_id": meta["event_id"],
                "resp": resp_body[:400],
            }
        )

    return (
        jsonify(
            {
                "ok": False,
                "meta_status": status,
                "event_id": meta["event_id"],
                "resp": resp_body[:400],
            }
        ),
        502,
    )


# ---------------------------------------------------------------------------
# Boot
# ---------------------------------------------------------------------------
_init_db()

if __name__ == "__main__":
    port = int(os.environ.get("PORT", "8918"))
    host = os.environ.get("HOST", "0.0.0.0")
    log.info(
        "capi_px3 subindo: host=%s port=%s pixel=%s test_mode=%s routes=[/pageview,/purchase,/lead,/health]",
        host,
        port,
        META_PIXEL_ID,
        bool(META_TEST_EVENT_CODE),
    )
    app.run(host=host, port=port, debug=False, threaded=True)
