#!/usr/bin/env python3
"""
Dashboard Borrello.
Login simples + sidebar fixa.
Abas: Visao Geral, Meta Ads (real), Google Ads (placeholder),
Hotmart (real), Rastreamento.
"""
import os
import time
import threading
import traceback
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo

import requests
from flask import (
    Flask,
    render_template,
    request,
    redirect,
    url_for,
    session,
    flash,
    jsonify,
)

from auth import check_login, login_required, USERS  # noqa: F401
import tracking

# ============================================================
# Flask
# ============================================================
app = Flask(__name__)
app.secret_key = os.environ.get("DASHBOARD_SECRET", "borrello-dash-secret-change-me-2026")
app.config["SESSION_COOKIE_NAME"] = "borrellodash"

app.register_blueprint(tracking.bp)

# ============================================================
# Config Meta Ads
# ============================================================
META_ACCOUNT_ID = "act_517468148842987"
META_GRAPH_URL = f"https://graph.facebook.com/v21.0/{META_ACCOUNT_ID}/insights"
try:
    _META_TOKEN = Path("/opt/mia/config/meta_token_renato.txt").read_text().strip()
except Exception:
    _META_TOKEN = ""

TZ_BR = ZoneInfo("America/Sao_Paulo")
TZ_UTC = ZoneInfo("UTC")

_meta_cache: dict = {}
_META_TTL = 900  # 15 min

_overview_cache: dict = {}
_OVERVIEW_TTL = 900

# ============================================================
# Config Hotmart
# ============================================================
HOTMART_OAUTH_URL = "https://api-sec-vlc.hotmart.com/security/oauth/token"
HOTMART_API_BASE = "https://developers.hotmart.com/payments/api/v1"


def _load_hotmart_env() -> dict:
    env = {}
    path = Path("/opt/mia/config/hotmart_borrello.env")
    if not path.exists():
        return env
    for line in path.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        env[k.strip()] = v.strip()
    return env


_HOTMART_ENV = _load_hotmart_env()
_HOTMART_BASIC = _HOTMART_ENV.get("HOTMART_BASIC", "")

# token cache
_hotmart_token_cache: dict = {"token": "", "expires_at": 0.0}
_hotmart_token_lock = threading.Lock()

# response cache
_hotmart_cache: dict = {}
_HOTMART_TTL = 600  # 10 min

HOTMART_PERIOD_DAYS = {
    "7d": 7,
    "30d": 30,
    "90d": 90,
}


# ============================================================
# Helpers formatacao
# ============================================================
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 _fmt_int(value) -> str:
    try:
        v = int(float(value or 0))
    except (TypeError, ValueError):
        v = 0
    return f"{v:,}".replace(",", ".")


def _fmt_pct(value) -> str:
    try:
        v = float(value or 0)
    except (TypeError, ValueError):
        v = 0.0
    return f"{v:.2f}%"


def _extrair_leads(actions) -> int:
    if not actions:
        return 0
    total = 0
    for a in actions:
        t = (a.get("action_type") or "").strip()
        if t in ("lead", "offsite_conversion.fb_pixel_lead"):
            try:
                total += int(float(a.get("value") or 0))
            except (TypeError, ValueError):
                pass
    return total


# ============================================================
# Meta Ads
# ============================================================
META_PRESET_MAP = {
    "hoje": "today",
    "ultimos_7": "last_7d",
    "ultimos_30": "last_30d",
    "este_mes": "this_month",
    "mes_passado": "last_month",
    "ontem": "yesterday",
}

META_LABEL_MAP = {
    "today": "Hoje",
    "yesterday": "Ontem",
    "last_7d": "Ultimos 7 dias",
    "last_30d": "Ultimos 30 dias",
    "this_month": "Este mes",
    "last_month": "Mes passado",
}


def buscar_meta_insights(preset: str, custom_inicio: str, custom_fim: str) -> dict:
    if not _META_TOKEN:
        raise RuntimeError("Token Meta nao configurado em /opt/mia/config/meta_token_renato.txt")

    params = {
        "access_token": _META_TOKEN,
        "fields": "campaign_name,campaign_id,spend,impressions,clicks,ctr,cpm,cpc,actions,reach,objective",
        "level": "campaign",
        "limit": 100,
    }

    label = ""
    if preset == "custom" and custom_inicio and custom_fim:
        params["time_range"] = '{"since":"%s","until":"%s"}' % (custom_inicio, custom_fim)
        try:
            di = datetime.fromisoformat(custom_inicio).strftime("%d/%m/%Y")
            df = datetime.fromisoformat(custom_fim).strftime("%d/%m/%Y")
            label = f"{di} a {df}"
        except Exception:
            label = f"{custom_inicio} a {custom_fim}"
    else:
        date_preset = META_PRESET_MAP.get(preset, "last_7d")
        params["date_preset"] = date_preset
        label = META_LABEL_MAP.get(date_preset, "Ultimos 7 dias")

    r = requests.get(META_GRAPH_URL, params=params, timeout=30)
    if r.status_code != 200:
        raise RuntimeError(f"Meta API {r.status_code}: {r.text[:300]}")
    data = r.json()
    return {"campanhas_raw": data.get("data", []) or [], "periodo_label": label}


def agregar_meta(campanhas_raw: list) -> dict:
    gasto_total = 0.0
    impressoes_total = 0
    cliques_total = 0
    leads_total = 0
    reach_total = 0

    campanhas = []
    for c in campanhas_raw:
        try:
            spend = float(c.get("spend") or 0)
        except (TypeError, ValueError):
            spend = 0.0
        try:
            impressoes = int(float(c.get("impressions") or 0))
        except (TypeError, ValueError):
            impressoes = 0
        try:
            cliques = int(float(c.get("clicks") or 0))
        except (TypeError, ValueError):
            cliques = 0
        try:
            ctr = float(c.get("ctr") or 0)
        except (TypeError, ValueError):
            ctr = 0.0
        try:
            cpm = float(c.get("cpm") or 0)
        except (TypeError, ValueError):
            cpm = 0.0
        try:
            cpc = float(c.get("cpc") or 0)
        except (TypeError, ValueError):
            cpc = 0.0
        try:
            reach = int(float(c.get("reach") or 0))
        except (TypeError, ValueError):
            reach = 0

        leads = _extrair_leads(c.get("actions") or [])
        cpl = (spend / leads) if leads > 0 else 0.0

        gasto_total += spend
        impressoes_total += impressoes
        cliques_total += cliques
        leads_total += leads
        reach_total += reach

        campanhas.append({
            "nome": c.get("campaign_name") or "Sem nome",
            "objetivo": c.get("objective") or "-",
            "gasto": _fmt_brl(spend),
            "gasto_raw": spend,
            "impressoes": _fmt_int(impressoes),
            "cliques": _fmt_int(cliques),
            "leads": leads,
            "cpl": _fmt_brl(cpl) if leads > 0 else "-",
            "ctr": _fmt_pct(ctr),
            "cpm": _fmt_brl(cpm),
            "cpc": _fmt_brl(cpc) if cpc > 0 else "-",
        })

    campanhas.sort(key=lambda x: -x["gasto_raw"])

    cpl_total = (gasto_total / leads_total) if leads_total > 0 else 0.0
    ctr_total = (cliques_total / impressoes_total * 100) if impressoes_total > 0 else 0.0
    cpm_total = (gasto_total / impressoes_total * 1000) if impressoes_total > 0 else 0.0
    cpc_total = (gasto_total / cliques_total) if cliques_total > 0 else 0.0

    resumo = {
        "gasto_total": _fmt_brl(gasto_total),
        "impressoes": _fmt_int(impressoes_total),
        "alcance": _fmt_int(reach_total),
        "cliques": _fmt_int(cliques_total),
        "leads": leads_total,
        "cpl": _fmt_brl(cpl_total) if leads_total > 0 else "-",
        "ctr": _fmt_pct(ctr_total),
        "cpm": _fmt_brl(cpm_total),
        "cpc": _fmt_brl(cpc_total) if cliques_total > 0 else "-",
    }

    return {"resumo": resumo, "campanhas": campanhas}


def coletar_meta_cached(preset: str, custom_inicio: str, custom_fim: str):
    cache_key = f"meta|{preset}|{custom_inicio}|{custom_fim}"
    now = time.time()
    if cache_key in _meta_cache:
        entry = _meta_cache[cache_key]
        if now - entry["ts"] < _META_TTL:
            return entry["resumo"], entry["campanhas"], entry["periodo_label"]

    insights = buscar_meta_insights(preset, custom_inicio, custom_fim)
    agg = agregar_meta(insights["campanhas_raw"])
    _meta_cache[cache_key] = {
        "ts": now,
        "resumo": agg["resumo"],
        "campanhas": agg["campanhas"],
        "periodo_label": insights["periodo_label"],
    }
    return agg["resumo"], agg["campanhas"], insights["periodo_label"]


def buscar_meta_diario(preset: str, custom_inicio: str, custom_fim: str) -> list[dict]:
    if not _META_TOKEN:
        return []

    params = {
        "access_token": _META_TOKEN,
        "fields": "spend,actions",
        "level": "account",
        "time_increment": 1,
        "limit": 200,
    }

    if preset == "custom" and custom_inicio and custom_fim:
        params["time_range"] = '{"since":"%s","until":"%s"}' % (custom_inicio, custom_fim)
    else:
        date_preset = META_PRESET_MAP.get(preset, "last_7d")
        params["date_preset"] = date_preset

    try:
        r = requests.get(META_GRAPH_URL, params=params, timeout=30)
        if r.status_code != 200:
            return []
        data = r.json().get("data", []) or []
    except Exception:
        return []

    serie = []
    for row in data:
        date_start = row.get("date_start") or ""
        try:
            dt = datetime.fromisoformat(date_start)
            label = dt.strftime("%d/%m")
        except Exception:
            label = date_start
        try:
            spend = float(row.get("spend") or 0)
        except (TypeError, ValueError):
            spend = 0.0
        leads = _extrair_leads(row.get("actions") or [])
        serie.append({"data": label, "data_iso": date_start, "leads": leads, "gasto": round(spend, 2)})

    serie.sort(key=lambda x: x.get("data_iso", ""))
    return serie


# ============================================================
# Hotmart - OAuth + coleta
# ============================================================
def _hotmart_get_token(force: bool = False) -> str:
    if not _HOTMART_BASIC:
        raise RuntimeError("HOTMART_BASIC nao configurado em /opt/mia/config/hotmart_borrello.env")

    with _hotmart_token_lock:
        now = time.time()
        cached = _hotmart_token_cache.get("token") or ""
        expires_at = _hotmart_token_cache.get("expires_at") or 0.0
        if cached and not force and now < expires_at - 60:
            return cached

        r = requests.post(
            HOTMART_OAUTH_URL,
            headers={"Authorization": _HOTMART_BASIC},
            params={"grant_type": "client_credentials"},
            timeout=30,
        )
        if r.status_code != 200:
            raise RuntimeError(f"Hotmart OAuth {r.status_code}: {r.text[:200]}")
        data = r.json()
        token = data.get("access_token") or ""
        if not token:
            raise RuntimeError("Hotmart OAuth: resposta sem access_token")
        expires_in = int(data.get("expires_in") or 3600)
        _hotmart_token_cache["token"] = token
        _hotmart_token_cache["expires_at"] = now + expires_in
        return token


def _hotmart_get(path: str, params) -> dict:
    """params pode ser dict ou lista de tuplas (para valores repetidos)."""
    token = _hotmart_get_token()
    url = f"{HOTMART_API_BASE}{path}"
    r = requests.get(
        url,
        headers={"Authorization": f"Bearer {token}"},
        params=params,
        timeout=30,
    )
    # se token virou invalido, tenta uma vez com refresh
    if r.status_code == 401:
        token = _hotmart_get_token(force=True)
        r = requests.get(
            url,
            headers={"Authorization": f"Bearer {token}"},
            params=params,
            timeout=30,
        )
    if r.status_code != 200:
        raise RuntimeError(f"Hotmart {path} {r.status_code}: {r.text[:200]}")
    try:
        return r.json()
    except ValueError:
        raise RuntimeError(f"Hotmart {path}: resposta nao-JSON")


def _hotmart_period_ms(period: str):
    days = HOTMART_PERIOD_DAYS.get(period, 30)
    end_ms = int(time.time() * 1000)
    start_ms = int((time.time() - days * 86400) * 1000)
    return start_ms, end_ms, days


def _fmt_data_br(ms) -> str:
    try:
        v = int(ms)
        dt = datetime.fromtimestamp(v / 1000, tz=TZ_BR)
        return dt.strftime("%d/%m/%Y %H:%M")
    except Exception:
        return "-"


HOTMART_STATUS_LABEL = {
    "APPROVED": "Aprovada",
    "COMPLETE": "Concluida",
    "CANCELLED": "Cancelada",
    "REFUNDED": "Reembolsada",
    "PARTIALLY_REFUNDED": "Reembolso parcial",
    "CHARGEBACK": "Chargeback",
    "REFUSED": "Recusada",
    "EXPIRED": "Expirada",
    "WAITING_PAYMENT": "Aguardando pgto",
    "OVERDUE": "Vencida",
    "STARTED": "Checkout iniciado",
    "UNDER_ANALISYS": "Em analise",
    "PRINTED_BILLET": "Boleto impresso",
    "PROTESTED": "Protestada",
    "BLOCKED": "Bloqueada",
}


def _hotmart_coletar(period: str) -> dict:
    start_ms, end_ms, days = _hotmart_period_ms(period)

    # vendas do periodo (tudo)
    todas_items = []
    page_token = None
    max_pages = 6  # cap defensivo
    status_filter = [
        "APPROVED", "COMPLETE", "CANCELLED", "REFUNDED", "CHARGEBACK",
        "EXPIRED", "WAITING_PAYMENT", "OVERDUE", "UNDER_ANALISYS",
        "STARTED", "PRINTED_BILLET", "PROTESTED", "BLOCKED",
        "PARTIALLY_REFUNDED",
    ]
    for _ in range(max_pages):
        params = [
            ("max_results", 200),
            ("start_date", start_ms),
            ("end_date", end_ms),
        ]
        for s in status_filter:
            params.append(("transaction_status", s))
        if page_token:
            params.append(("page_token", page_token))
        data = _hotmart_get("/sales/history", params)
        items = data.get("items") or []
        todas_items.extend(items)
        page_token = ((data.get("page_info") or {}).get("next_page_token")) or None
        if not page_token:
            break

    # normaliza / agrega
    total_vendido_brl = 0.0
    total_vendido_usd = 0.0
    n_vendas = 0
    n_aprovadas = 0
    status_count: dict = {}
    vendas_recentes = []
    approved_states = {"APPROVED", "COMPLETE"}

    # separacao compras novas vs parcelas recorrentes
    n_vendas_novas = 0
    n_vendas_parcelas = 0
    receita_novas_brl = 0.0
    receita_parcelas_brl = 0.0

    for it in todas_items:
        purchase = it.get("purchase") or {}
        buyer = it.get("buyer") or {}
        product = it.get("product") or {}
        price = purchase.get("price") or {}
        payment = purchase.get("payment") or {}
        status = (purchase.get("status") or "").upper() or "?"
        currency = (price.get("currency_code") or "").upper()
        try:
            valor = float(price.get("value") or 0)
        except (TypeError, ValueError):
            valor = 0.0

        # ---- classificacao de parcela ----
        # recurrency_number: qual parcela recorrente (boleto/cartao recorrente) esta sendo cobrada.
        # None ou 1 => compra nova (primeira cobranca, mesmo que parcelado no cartao)
        # > 1       => cobranca de parcela subsequente (recorrente)
        recur_raw = purchase.get("recurrency_number")
        try:
            recur_num = int(recur_raw) if recur_raw is not None else None
        except (TypeError, ValueError):
            recur_num = None

        try:
            total_parcelas = int(payment.get("installments_number") or 0)
        except (TypeError, ValueError):
            total_parcelas = 0
        if total_parcelas <= 0:
            total_parcelas = 1

        if recur_num is None or recur_num <= 1:
            tipo = "nova"
            parcela_atual = 1
        else:
            tipo = "parcela"
            parcela_atual = recur_num

        # label amigavel
        if tipo == "parcela":
            parcela_label = f"{parcela_atual}/{total_parcelas}" if total_parcelas > 1 else f"{parcela_atual}"
        else:
            if total_parcelas <= 1:
                parcela_label = "À vista"
            else:
                # compra nova com cartao parcelado: mostra "1/N (nova)"
                parcela_label = f"1/{total_parcelas}"

        status_count[status] = status_count.get(status, 0) + 1
        n_vendas += 1
        if status in approved_states:
            n_aprovadas += 1
            if currency == "BRL":
                total_vendido_brl += valor
                if tipo == "nova":
                    n_vendas_novas += 1
                    receita_novas_brl += valor
                else:
                    n_vendas_parcelas += 1
                    receita_parcelas_brl += valor
            elif currency == "USD":
                total_vendido_usd += valor
                # nao entra no split BRL, mas conta a venda por tipo
                if tipo == "nova":
                    n_vendas_novas += 1
                else:
                    n_vendas_parcelas += 1

        vendas_recentes.append({
            "produto": product.get("name") or "-",
            "comprador": buyer.get("name") or (buyer.get("email") or "-"),
            "email": buyer.get("email") or "",
            "valor": valor,
            "moeda": currency or "BRL",
            "valor_fmt": (f"R$ {valor:,.2f}".replace(",", "@").replace(".", ",").replace("@", ".")
                          if currency == "BRL"
                          else f"{currency} {valor:,.2f}"),
            "status": status,
            "status_label": HOTMART_STATUS_LABEL.get(status, status.title()),
            "data_ms": purchase.get("order_date") or purchase.get("approved_date") or 0,
            "data_fmt": _fmt_data_br(purchase.get("order_date") or purchase.get("approved_date") or 0),
            "transacao": purchase.get("transaction") or "",
            "tipo": tipo,
            "parcela_atual": parcela_atual,
            "total_parcelas": total_parcelas,
            "parcela_label": parcela_label,
            "metodo_pagamento": payment.get("method") or "",
        })

    # ordena recentes pela data DESC
    vendas_recentes.sort(key=lambda x: x.get("data_ms", 0), reverse=True)

    # metricas derivadas
    ticket_medio_brl = (total_vendido_brl / n_aprovadas) if n_aprovadas > 0 and total_vendido_brl > 0 else 0.0
    taxa_aprovacao = (n_aprovadas / n_vendas * 100.0) if n_vendas > 0 else 0.0

    # breakdown ordenado
    breakdown = []
    for s, qtd in status_count.items():
        breakdown.append({
            "status": s,
            "label": HOTMART_STATUS_LABEL.get(s, s.title()),
            "qtd": qtd,
            "pct": round((qtd / n_vendas * 100.0), 1) if n_vendas > 0 else 0.0,
        })
    breakdown.sort(key=lambda x: -x["qtd"])

    resumo = {
        "total_vendido_brl": _fmt_brl(total_vendido_brl),
        "total_vendido_brl_raw": round(total_vendido_brl, 2),
        "total_vendido_usd": round(total_vendido_usd, 2),
        "total_vendido_usd_fmt": f"US$ {total_vendido_usd:,.2f}" if total_vendido_usd > 0 else "",
        "n_vendas": n_vendas,
        "n_aprovadas": n_aprovadas,
        "ticket_medio": _fmt_brl(ticket_medio_brl) if ticket_medio_brl > 0 else "-",
        "taxa_aprovacao": _fmt_pct(taxa_aprovacao),
        # split compras novas x parcelas recorrentes (baseado em vendas aprovadas)
        "vendas_novas": n_vendas_novas,
        "vendas_parcelas": n_vendas_parcelas,
        "receita_novas": _fmt_brl(receita_novas_brl),
        "receita_novas_raw": round(receita_novas_brl, 2),
        "receita_parcelas": _fmt_brl(receita_parcelas_brl),
        "receita_parcelas_raw": round(receita_parcelas_brl, 2),
    }

    return {
        "periodo": period,
        "periodo_label": f"Ultimos {days} dias",
        "resumo": resumo,
        "breakdown": breakdown,
        "vendas": vendas_recentes[:50],
    }


def _hotmart_coletar_cached(period: str) -> dict:
    now = time.time()
    entry = _hotmart_cache.get(period)
    if entry and now - entry["ts"] < _HOTMART_TTL:
        return entry["data"]
    data = _hotmart_coletar(period)
    _hotmart_cache[period] = {"ts": now, "data": data}
    return data


# ============================================================
# Context processor
# ============================================================
@app.context_processor
def inject_globals():
    return {
        "current_user": session.get("user"),
        "current_year": datetime.now().year,
    }


# ============================================================
# Routes
# ============================================================
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = (request.form.get("username") or "").strip().lower()
        password = (request.form.get("password") or "").strip()
        if check_login(username, password):
            session["user"] = username
            nxt = request.args.get("next") or url_for("index")
            return redirect(nxt)
        flash("Usuario ou senha invalidos.", "error")
    return render_template("login.html")


@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("login"))


@app.route("/")
@login_required
def index():
    preset = request.args.get("preset", "ultimos_7")
    custom_inicio = request.args.get("inicio", "")
    custom_fim = request.args.get("fim", "")
    return render_template(
        "index.html",
        active="home",
        preset=preset,
        custom_inicio=custom_inicio,
        custom_fim=custom_fim,
    )


@app.route("/api/overview")
@login_required
def api_overview():
    preset = request.args.get("preset", "ultimos_7")
    custom_inicio = request.args.get("inicio", "")
    custom_fim = request.args.get("fim", "")

    cache_key = f"overview|{preset}|{custom_inicio}|{custom_fim}"
    now = time.time()
    if cache_key in _overview_cache:
        entry = _overview_cache[cache_key]
        if now - entry["ts"] < _OVERVIEW_TTL:
            return jsonify(entry["data"])

    out = {
        "ok": True,
        "preset": preset,
        "periodo_label": "",
        "meta": {"ok": False, "erro": ""},
    }

    # Meta Ads
    try:
        resumo_meta, campanhas_meta, label_meta = coletar_meta_cached(preset, custom_inicio, custom_fim)
        serie_meta = buscar_meta_diario(preset, custom_inicio, custom_fim)
        top_camp = []
        for c in (campanhas_meta or [])[:5]:
            top_camp.append({
                "nome": c.get("nome") or "-",
                "leads": int(c.get("leads") or 0),
                "gasto": float(c.get("gasto_raw") or 0),
            })
        out["periodo_label"] = label_meta
        out["meta"] = {
            "ok": True,
            "resumo": resumo_meta,
            "serie_diaria": serie_meta,
            "top_campanhas": top_camp,
        }
    except Exception as e:
        app.logger.warning(f"overview meta: {e}")
        out["meta"] = {"ok": False, "erro": str(e)}

    # Rastreamento local
    try:
        clicks = tracking.carregar_clicks()
        agg_clicks = tracking.agregar_clicks(clicks)
        pvs = tracking.carregar_pageviews()
        agg_pvs = tracking.agregar_pageviews(pvs)
        out["rastreamento"] = {
            "ok": True,
            "clicks_total": agg_clicks["total"],
            "clicks_7d": agg_clicks["ultimos_7d"],
            "pageviews_total": agg_pvs["total"],
            "pageviews_7d": agg_pvs["ultimos_7d"],
        }
    except Exception as e:
        out["rastreamento"] = {"ok": False, "erro": str(e)}

    _overview_cache[cache_key] = {"ts": now, "data": out}
    return jsonify(out)


@app.route("/meta-ads")
@login_required
def meta_ads():
    preset = request.args.get("preset", "ultimos_7")
    custom_inicio = request.args.get("inicio", "")
    custom_fim = request.args.get("fim", "")
    return render_template(
        "meta_ads.html",
        active="meta_ads",
        section_title="Meta Ads",
        preset=preset,
        custom_inicio=custom_inicio,
        custom_fim=custom_fim,
    )


@app.route("/api/meta-ads")
@login_required
def api_meta_ads():
    preset = request.args.get("preset", "ultimos_7")
    custom_inicio = request.args.get("inicio", "")
    custom_fim = request.args.get("fim", "")

    try:
        resumo, campanhas, periodo_label = coletar_meta_cached(preset, custom_inicio, custom_fim)
    except Exception as e:
        app.logger.exception("erro api_meta_ads")
        return jsonify({"ok": False, "erro": str(e)}), 500

    return jsonify({
        "ok": True,
        "periodo_label": periodo_label,
        "resumo": resumo,
        "campanhas": campanhas,
    })


@app.route("/google-ads")
@login_required
def google_ads():
    return render_template(
        "google_ads.html",
        active="google_ads",
        section_title="Google Ads",
    )


@app.route("/hotmart")
@login_required
def hotmart():
    period = request.args.get("periodo", "30d")
    if period not in HOTMART_PERIOD_DAYS:
        period = "30d"
    return render_template(
        "hotmart.html",
        active="hotmart",
        section_title="Hotmart",
        periodo=period,
    )


@app.route("/api/hotmart-sales")
@login_required
def api_hotmart_sales():
    period = request.args.get("periodo", "30d")
    if period not in HOTMART_PERIOD_DAYS:
        period = "30d"
    try:
        data = _hotmart_coletar_cached(period)
    except Exception as e:
        app.logger.exception("erro api_hotmart_sales")
        return jsonify({"ok": False, "erro": str(e)}), 500
    return jsonify({
        "ok": True,
        "periodo": period,
        "periodo_label": data["periodo_label"],
        "vendas": data["vendas"],
    })


@app.route("/api/hotmart-summary")
@login_required
def api_hotmart_summary():
    period = request.args.get("periodo", "30d")
    if period not in HOTMART_PERIOD_DAYS:
        period = "30d"
    try:
        data = _hotmart_coletar_cached(period)
    except Exception as e:
        app.logger.exception("erro api_hotmart_summary")
        return jsonify({"ok": False, "erro": str(e)}), 500
    return jsonify({
        "ok": True,
        "periodo": period,
        "periodo_label": data["periodo_label"],
        "resumo": data["resumo"],
        "breakdown": data["breakdown"],
    })


@app.route("/rastreamento")
@login_required
def rastreamento():
    clicks = tracking.carregar_clicks()
    agg_clicks = tracking.agregar_clicks(clicks)

    pageviews = tracking.carregar_pageviews()
    agg_pvs = tracking.agregar_pageviews(pageviews)

    return render_template(
        "rastreamento.html",
        active="rastreamento",
        section_title="Rastreamento",
        agg_clicks=agg_clicks,
        agg_pvs=agg_pvs,
        base_url=request.host_url.rstrip("/"),
    )


@app.route("/healthz")
def healthz():
    return jsonify({"ok": True, "ts": int(time.time())})


# ============================================================
# Cache warmer
# ============================================================
def _warm_all_caches():
    presets = ["ultimos_7", "ultimos_30", "este_mes", "mes_passado", "hoje"]
    for preset in presets:
        try:
            coletar_meta_cached(preset, "", "")
        except Exception:
            traceback.print_exc()
    for period in ("7d", "30d", "90d"):
        try:
            _hotmart_coletar_cached(period)
        except Exception:
            traceback.print_exc()


def _start_cache_warmer():
    def _loop():
        while True:
            time.sleep(240)
            try:
                _warm_all_caches()
            except Exception:
                traceback.print_exc()

    t = threading.Thread(target=_loop, daemon=True, name="cache-warmer-loop")
    t.start()
    threading.Thread(target=_warm_all_caches, daemon=True, name="cache-warmer-initial").start()


_start_cache_warmer()


# ============================================================
# Entrypoint
# ============================================================
if __name__ == "__main__":
    port = int(os.environ.get("PORT", 8920))
    app.run(host="0.0.0.0", port=port, debug=False)
