#!/usr/bin/env python3
"""
Dashboard Chacara Sonho Verde.
Login simples + sidebar fixa. Puxa Meta Ads (act_2321456848658947) ao vivo.
Abas: Visao Geral, Meta Ads, Conjuntos, Anuncios, Health.

v2 (2026-08-18): filtro de periodo dinamico via query string (?start=&end= ou ?preset=)
"""
import os
import time
import threading
import traceback
from datetime import datetime, timedelta, date
from pathlib import Path
from zoneinfo import ZoneInfo

import requests
from flask import (
    Flask,
    render_template,
    jsonify,
    request,
)

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

# ============================================================
# Config Meta Ads
# ============================================================
META_ACCOUNT_ID = "act_2321456848658947"
META_API_VERSION = "v21.0"
META_BASE = f"https://graph.facebook.com/{META_API_VERSION}"
META_INSIGHTS_URL = f"{META_BASE}/{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")

# cache indexado por (since, until)
_meta_cache: dict = {}
_META_TTL = 300  # 5 min
_MAX_LOOKBACK_DAYS = 180  # 6 meses de trás pra frente é o limite pra evitar buscas absurdas


# ============================================================
# 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 _fmt_delta(atual: float, anterior: float) -> dict:
    """Calcula variacao percentual entre dois periodos. Retorna dict com valor formatado e sinal."""
    if anterior <= 0:
        if atual > 0:
            return {"pct": "+ infinito", "sign": "up", "raw": None}
        return {"pct": "-", "sign": "flat", "raw": 0.0}
    delta = ((atual - anterior) / anterior) * 100.0
    if abs(delta) < 0.05:
        return {"pct": "0,0%", "sign": "flat", "raw": 0.0}
    signal = "up" if delta > 0 else "down"
    prefix = "+" if delta > 0 else ""
    return {"pct": f"{prefix}{delta:.1f}%".replace(".", ","), "sign": signal, "raw": round(delta, 2)}


def _extrair_leads(actions) -> int:
    """Soma acoes tipo 'lead' e conversoes de pixel."""
    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", "onsite_conversion.lead_grouped"):
            try:
                total += int(float(a.get("value") or 0))
            except (TypeError, ValueError):
                pass
    return total


def _extrair_msg(actions) -> int:
    """Conversas iniciadas (msg apps)."""
    if not actions:
        return 0
    total = 0
    for a in actions:
        t = (a.get("action_type") or "").strip()
        if t in ("onsite_conversion.messaging_conversation_started_7d",
                 "onsite_conversion.total_messaging_connection"):
            try:
                total += int(float(a.get("value") or 0))
            except (TypeError, ValueError):
                pass
    return total


# ============================================================
# Resolvedor de periodo (query string -> since/until)
# ============================================================
def _hoje_br() -> date:
    return datetime.now(TZ_BR).date()


def _preset_range(preset: str) -> tuple[str, str] | None:
    """Retorna (since, until) ISO para um preset conhecido, ou None se inválido."""
    hoje = _hoje_br()
    p = (preset or "").lower().strip()

    if p == "today":
        return hoje.isoformat(), hoje.isoformat()
    if p == "yesterday":
        y = hoje - timedelta(days=1)
        return y.isoformat(), y.isoformat()
    if p == "last7":
        ini = hoje - timedelta(days=6)
        return ini.isoformat(), hoje.isoformat()
    if p == "last14":
        ini = hoje - timedelta(days=13)
        return ini.isoformat(), hoje.isoformat()
    if p == "last30":
        ini = hoje - timedelta(days=29)
        return ini.isoformat(), hoje.isoformat()
    if p == "thismonth":
        ini = hoje.replace(day=1)
        return ini.isoformat(), hoje.isoformat()
    if p == "lastmonth":
        primeiro_mes_atual = hoje.replace(day=1)
        ultimo_mes_passado = primeiro_mes_atual - timedelta(days=1)
        primeiro_mes_passado = ultimo_mes_passado.replace(day=1)
        return primeiro_mes_passado.isoformat(), ultimo_mes_passado.isoformat()
    return None


def _parse_iso_date(s: str) -> date | None:
    if not s:
        return None
    try:
        return datetime.strptime(s, "%Y-%m-%d").date()
    except (ValueError, TypeError):
        return None


def _resolver_periodo(args) -> dict:
    """
    Lê query string (start, end, preset) e devolve dict:
    {
      "since": "YYYY-MM-DD",
      "until": "YYYY-MM-DD",
      "preset": "last7" | "custom" | ...,
      "days": int,                 # duracao em dias
      "since_prev": ..., "until_prev": ...,  # janela imediatamente anterior de mesma duracao
      "label": "01/08 a 20/08",
      "label_prev": "12/07 a 31/07",
      "warnings": [str, ...]
    }

    Regras:
      - preset explicito manda; senao usa start/end; senao padrao = last7.
      - start > end -> troca e warn.
      - end no futuro -> clamp em hoje.
      - since anterior a hoje - _MAX_LOOKBACK_DAYS -> clamp.
      - datas invalidas -> fallback pro last7 + warn.
    """
    warnings: list[str] = []
    hoje = _hoje_br()
    lookback_min = hoje - timedelta(days=_MAX_LOOKBACK_DAYS)

    preset_req = (args.get("preset") or "").lower().strip()
    start_req = (args.get("start") or "").strip()
    end_req = (args.get("end") or "").strip()

    since: date | None = None
    until: date | None = None
    preset_final = ""

    # 1) tenta preset explicito
    if preset_req and preset_req != "custom":
        r = _preset_range(preset_req)
        if r:
            since = _parse_iso_date(r[0])
            until = _parse_iso_date(r[1])
            preset_final = preset_req
        else:
            warnings.append(f"preset desconhecido: {preset_req}, usando last7")

    # 2) senao, tenta start/end
    if since is None or until is None:
        s = _parse_iso_date(start_req)
        u = _parse_iso_date(end_req)
        if s and u:
            since, until = s, u
            preset_final = "custom"
        elif start_req or end_req:
            warnings.append("start/end invalidos, usando last7")

    # 3) fallback: last7
    if since is None or until is None:
        r = _preset_range("last7")
        since = _parse_iso_date(r[0])
        until = _parse_iso_date(r[1])
        preset_final = preset_final or "last7"

    # validacoes / clamps
    if since > until:
        since, until = until, since
        warnings.append("data inicial > final: intervalo trocado")

    if until > hoje:
        until = hoje
        warnings.append("data final no futuro: ajustada pra hoje")

    if since < lookback_min:
        since = lookback_min
        warnings.append(f"data inicial mais antiga que {_MAX_LOOKBACK_DAYS}d: ajustada")

    if since > hoje:
        since = hoje

    days = (until - since).days + 1

    # comparativo: janela imediatamente anterior de mesma duracao
    until_prev = since - timedelta(days=1)
    since_prev = until_prev - timedelta(days=days - 1)
    # clamp inferior
    if since_prev < lookback_min:
        since_prev = lookback_min

    return {
        "since": since.isoformat(),
        "until": until.isoformat(),
        "preset": preset_final,
        "days": days,
        "since_prev": since_prev.isoformat(),
        "until_prev": until_prev.isoformat(),
        "label": f"{since.strftime('%d/%m/%Y')} a {until.strftime('%d/%m/%Y')}",
        "label_short": f"{since.strftime('%d/%m')} a {until.strftime('%d/%m')}",
        "label_prev": f"{since_prev.strftime('%d/%m/%Y')} a {until_prev.strftime('%d/%m/%Y')}",
        "label_prev_short": f"{since_prev.strftime('%d/%m')} a {until_prev.strftime('%d/%m')}",
        "warnings": warnings,
    }


# ============================================================
# Meta Ads - fetchers de baixo nivel
# ============================================================
def _meta_get(url: str, params: dict, timeout: int = 30) -> dict:
    if not _META_TOKEN:
        raise RuntimeError("Token Meta nao configurado em /opt/mia/config/meta_token_renato.txt")
    params = dict(params)
    params["access_token"] = _META_TOKEN
    r = requests.get(url, params=params, timeout=timeout)
    if r.status_code != 200:
        raise RuntimeError(f"Meta API {r.status_code}: {r.text[:300]}")
    return r.json()


def _mes_atual_range() -> tuple[str, str]:
    hoje = _hoje_br()
    ini = hoje.replace(day=1)
    return ini.isoformat(), hoje.isoformat()


# ============================================================
# Insights por conta (agregado)
# ============================================================
def _buscar_insights_conta(since: str, until: str) -> dict:
    """Retorna resumo agregado para a conta no periodo."""
    params = {
        "fields": "spend,impressions,clicks,ctr,cpm,cpc,actions,reach,frequency",
        "level": "account",
        "time_range": '{"since":"%s","until":"%s"}' % (since, until),
    }
    data = _meta_get(META_INSIGHTS_URL, params)
    rows = data.get("data") or []
    if not rows:
        return {
            "gasto": 0.0, "impressoes": 0, "cliques": 0, "leads": 0,
            "ctr": 0.0, "cpm": 0.0, "cpc": 0.0, "cpl": 0.0, "reach": 0, "freq": 0.0,
        }
    r = rows[0]
    spend = float(r.get("spend") or 0)
    impressoes = int(float(r.get("impressions") or 0))
    cliques = int(float(r.get("clicks") or 0))
    ctr = float(r.get("ctr") or 0)
    cpm = float(r.get("cpm") or 0)
    cpc = float(r.get("cpc") or 0)
    reach = int(float(r.get("reach") or 0))
    freq = float(r.get("frequency") or 0)
    leads = _extrair_leads(r.get("actions"))
    cpl = (spend / leads) if leads > 0 else 0.0
    return {
        "gasto": round(spend, 2), "impressoes": impressoes, "cliques": cliques,
        "leads": leads, "ctr": round(ctr, 2), "cpm": round(cpm, 2), "cpc": round(cpc, 2),
        "cpl": round(cpl, 2), "reach": reach, "freq": round(freq, 3),
    }


def _serie_diaria_conta(since: str, until: str) -> list[dict]:
    """Serie diaria (gasto + leads) por dia para o range."""
    params = {
        "fields": "spend,actions",
        "level": "account",
        "time_range": '{"since":"%s","until":"%s"}' % (since, until),
        "time_increment": 1,
        "limit": 500,
    }
    try:
        data = _meta_get(META_INSIGHTS_URL, params)
    except Exception:
        return []
    out = []
    for row in data.get("data", []) or []:
        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 [])
        cpl = (spend / leads) if leads > 0 else 0.0
        out.append({
            "data": label,
            "data_iso": date_start,
            "leads": leads,
            "gasto": round(spend, 2),
            "cpl": round(cpl, 2),
        })
    out.sort(key=lambda x: x.get("data_iso", ""))
    return out


def _insights_por_adset(since: str, until: str) -> list[dict]:
    params = {
        "fields": "adset_name,adset_id,spend,impressions,clicks,ctr,cpm,cpc,actions,frequency,reach",
        "level": "adset",
        "time_range": '{"since":"%s","until":"%s"}' % (since, until),
        "limit": 100,
    }
    data = _meta_get(META_INSIGHTS_URL, params)
    out = []
    for r in data.get("data") or []:
        spend = float(r.get("spend") or 0)
        impressoes = int(float(r.get("impressions") or 0))
        cliques = int(float(r.get("clicks") or 0))
        leads = _extrair_leads(r.get("actions"))
        cpl = (spend / leads) if leads > 0 else 0.0
        out.append({
            "adset_id": r.get("adset_id"),
            "nome": r.get("adset_name") or "-",
            "gasto_raw": round(spend, 2),
            "gasto": _fmt_brl(spend),
            "impressoes": _fmt_int(impressoes),
            "cliques": _fmt_int(cliques),
            "leads": leads,
            "cpl": _fmt_brl(cpl) if leads > 0 else "-",
            "cpl_raw": round(cpl, 2),
            "ctr": _fmt_pct(float(r.get("ctr") or 0)),
            "ctr_raw": float(r.get("ctr") or 0),
            "cpm": _fmt_brl(float(r.get("cpm") or 0)),
            "cpc": _fmt_brl(float(r.get("cpc") or 0)) if float(r.get("cpc") or 0) > 0 else "-",
            "freq": round(float(r.get("frequency") or 0), 2),
            "reach": _fmt_int(int(float(r.get("reach") or 0))),
        })
    out.sort(key=lambda x: -x["gasto_raw"])
    return out


def _insights_por_ad(since: str, until: str) -> list[dict]:
    params = {
        "fields": "ad_name,ad_id,adset_name,spend,impressions,clicks,ctr,cpm,actions,frequency",
        "level": "ad",
        "time_range": '{"since":"%s","until":"%s"}' % (since, until),
        "limit": 200,
    }
    data = _meta_get(META_INSIGHTS_URL, params)
    out = []
    for r in data.get("data") or []:
        spend = float(r.get("spend") or 0)
        impressoes = int(float(r.get("impressions") or 0))
        cliques = int(float(r.get("clicks") or 0))
        leads = _extrair_leads(r.get("actions"))
        cpl = (spend / leads) if leads > 0 else 0.0
        freq = round(float(r.get("frequency") or 0), 2)
        ctr = float(r.get("ctr") or 0)
        out.append({
            "ad_id": r.get("ad_id"),
            "nome": r.get("ad_name") or "-",
            "adset": r.get("adset_name") or "-",
            "gasto_raw": round(spend, 2),
            "gasto": _fmt_brl(spend),
            "impressoes": _fmt_int(impressoes),
            "cliques": _fmt_int(cliques),
            "leads": leads,
            "cpl": _fmt_brl(cpl) if leads > 0 else "-",
            "cpl_raw": round(cpl, 2),
            "ctr": _fmt_pct(ctr),
            "ctr_raw": ctr,
            "cpm": _fmt_brl(float(r.get("cpm") or 0)),
            "freq": freq,
            "fadiga": freq > 2.0,  # sinaliza fadiga
        })
    out.sort(key=lambda x: -x["gasto_raw"])
    return out


# ============================================================
# Health check - saldo, status ads, pixel
# ============================================================
def _health_check() -> dict:
    out = {"ok": True, "erros": []}

    # 1. saldo da conta
    try:
        d = _meta_get(
            f"{META_BASE}/{META_ACCOUNT_ID}",
            {"fields": "name,account_status,balance,amount_spent,currency,timezone_name,disable_reason"},
        )
        # balance vem em centavos como string
        balance_raw = d.get("balance") or "0"
        try:
            balance_brl = float(balance_raw) / 100.0
        except (TypeError, ValueError):
            balance_brl = 0.0
        amount_spent_raw = d.get("amount_spent") or "0"
        try:
            amount_spent_brl = float(amount_spent_raw) / 100.0
        except (TypeError, ValueError):
            amount_spent_brl = 0.0

        out["conta"] = {
            "nome": d.get("name") or "-",
            "status_code": d.get("account_status"),
            "status_label": "Ativa" if d.get("account_status") == 1 else "Inativa/Bloqueada",
            "saldo_brl": round(balance_brl, 2),
            "saldo_fmt": _fmt_brl(balance_brl),
            "saldo_baixo": balance_brl < 50.0,
            "saldo_critico": balance_brl < 10.0,
            "gasto_total_conta": _fmt_brl(amount_spent_brl),
            "moeda": d.get("currency") or "BRL",
            "timezone": d.get("timezone_name") or "-",
        }
    except Exception as e:
        out["conta"] = {"erro": str(e)}
        out["erros"].append(f"conta: {e}")

    # 2. pixel
    try:
        d = _meta_get(
            f"{META_BASE}/{META_ACCOUNT_ID}/adspixels",
            {"fields": "name,last_fired_time,id"},
        )
        pixels = []
        for p in d.get("data") or []:
            last = p.get("last_fired_time") or ""
            horas_atras = None
            label = "-"
            try:
                dt = datetime.fromisoformat(last)
                agora = datetime.now(dt.tzinfo)
                delta = agora - dt
                horas_atras = round(delta.total_seconds() / 3600, 1)
                if horas_atras < 1:
                    label = f"{int(delta.total_seconds()/60)} min atras"
                elif horas_atras < 24:
                    label = f"{horas_atras:.1f}h atras"
                else:
                    label = f"{int(horas_atras/24)} dias atras"
            except Exception:
                pass
            pixels.append({
                "id": p.get("id"),
                "nome": p.get("name") or "-",
                "last_fired": last,
                "last_fired_label": label,
                "horas_atras": horas_atras,
                "alerta": (horas_atras is not None and horas_atras > 24),
            })
        out["pixels"] = pixels
    except Exception as e:
        out["pixels"] = []
        out["erros"].append(f"pixels: {e}")

    # 3. status dos ads (ativos vs pausados)
    try:
        d = _meta_get(
            f"{META_BASE}/{META_ACCOUNT_ID}/ads",
            {"fields": "name,status,effective_status,adset{name,status}", "limit": 100},
        )
        ativos = 0
        pausados = 0
        rejeitados = 0
        detalhes = []
        for a in d.get("data") or []:
            st = (a.get("effective_status") or a.get("status") or "").upper()
            if st == "ACTIVE":
                ativos += 1
            elif st in ("PAUSED", "ADSET_PAUSED", "CAMPAIGN_PAUSED"):
                pausados += 1
            elif st in ("DISAPPROVED", "PENDING_REVIEW", "WITH_ISSUES"):
                rejeitados += 1
            detalhes.append({
                "nome": a.get("name") or "-",
                "status": st,
                "adset": (a.get("adset") or {}).get("name") or "-",
            })
        out["ads"] = {
            "ativos": ativos,
            "pausados": pausados,
            "rejeitados": rejeitados,
            "total": len(d.get("data") or []),
            "detalhes": detalhes,
        }
    except Exception as e:
        out["ads"] = {"erro": str(e)}
        out["erros"].append(f"ads: {e}")

    out["ok"] = len(out["erros"]) == 0
    return out


# ============================================================
# Coleta consolidada (o coracao da dash)
# ============================================================
def _coletar_overview(periodo: dict) -> dict:
    """Retorna payload completo pro periodo escolhido:
      - agregado atual + agregado periodo anterior (com deltas)
      - serie diaria do periodo
      - mes atual agregado (sempre presente pro card 'mes')
      - adsets do periodo
      - ads do periodo
      - health check
    """
    since = periodo["since"]
    until = periodo["until"]
    since_prev = periodo["since_prev"]
    until_prev = periodo["until_prev"]
    ini_mes, fim_mes = _mes_atual_range()

    # coletas em paralelo (thread pool simples)
    resultados: dict = {}

    def _run(key, fn):
        try:
            resultados[key] = fn()
        except Exception as e:
            resultados[key] = {"__erro__": str(e)}

    tasks = [
        ("agg", lambda: _buscar_insights_conta(since, until)),
        ("agg_prev", lambda: _buscar_insights_conta(since_prev, until_prev)),
        ("agg_mes", lambda: _buscar_insights_conta(ini_mes, fim_mes)),
        ("serie", lambda: _serie_diaria_conta(since, until)),
        ("adsets", lambda: _insights_por_adset(since, until)),
        ("ads", lambda: _insights_por_ad(since, until)),
        ("health", _health_check),
    ]
    threads = []
    for k, fn in tasks:
        t = threading.Thread(target=_run, args=(k, fn), daemon=True)
        t.start()
        threads.append(t)
    for t in threads:
        t.join(timeout=45)

    # monta payload
    agg = resultados.get("agg", {}) or {}
    agg_prev = resultados.get("agg_prev", {}) or {}
    agg_mes = resultados.get("agg_mes", {}) or {}

    dias = periodo["days"]
    sufixo = f"({dias}d)"

    # cards com delta
    def _card(label_kpi, atual_val, prev_val, fmt="brl"):
        if fmt == "brl":
            valor_fmt = _fmt_brl(atual_val)
        elif fmt == "int":
            valor_fmt = _fmt_int(atual_val)
        elif fmt == "pct":
            valor_fmt = _fmt_pct(atual_val)
        else:
            valor_fmt = str(atual_val)
        delta = _fmt_delta(float(atual_val or 0), float(prev_val or 0))
        return {
            "label": label_kpi,
            "valor": valor_fmt,
            "valor_raw": atual_val,
            "prev_raw": prev_val,
            "delta_pct": delta["pct"],
            "delta_sign": delta["sign"],
        }

    cards = [
        _card(f"Investimento {sufixo}", agg.get("gasto", 0), agg_prev.get("gasto", 0), "brl"),
        _card(f"Leads {sufixo}", agg.get("leads", 0), agg_prev.get("leads", 0), "int"),
        _card(f"CPL {sufixo}", agg.get("cpl", 0), agg_prev.get("cpl", 0), "brl"),
        _card(f"CTR {sufixo}", agg.get("ctr", 0), agg_prev.get("ctr", 0), "pct"),
        _card(f"CPM {sufixo}", agg.get("cpm", 0), agg_prev.get("cpm", 0), "brl"),
    ]

    # sinalizar adset mais caro
    adsets = resultados.get("adsets", []) or []
    if isinstance(adsets, dict) and adsets.get("__erro__"):
        adsets_erro = adsets["__erro__"]
        adsets = []
    else:
        adsets_erro = ""
    if adsets:
        cpl_max = max((a["cpl_raw"] for a in adsets if a["cpl_raw"] > 0), default=0)
        for a in adsets:
            a["mais_caro"] = (a["cpl_raw"] == cpl_max and cpl_max > 0)

    ads = resultados.get("ads", []) or []
    if isinstance(ads, dict) and ads.get("__erro__"):
        ads_erro = ads["__erro__"]
        ads = []
    else:
        ads_erro = ""

    # sinaliza ads com CTR muito abaixo da media do periodo
    if ads:
        ctrs = [a["ctr_raw"] for a in ads if a["ctr_raw"] > 0]
        media_ctr = sum(ctrs) / len(ctrs) if ctrs else 0
        for a in ads:
            a["ctr_baixo"] = (media_ctr > 0 and a["ctr_raw"] < media_ctr * 0.7)

    payload = {
        "ok": True,
        "gerado_em": datetime.now(TZ_BR).strftime("%d/%m/%Y %H:%M"),
        "periodo": {
            "since": periodo["since"],
            "until": periodo["until"],
            "since_prev": periodo["since_prev"],
            "until_prev": periodo["until_prev"],
            "preset": periodo["preset"],
            "days": periodo["days"],
            "label": periodo["label"],
            "label_short": periodo["label_short"],
            "label_prev": periodo["label_prev"],
            "label_prev_short": periodo["label_prev_short"],
            "warnings": periodo["warnings"],
            # compat com codigo antigo (algumas paginas ainda podem esperar):
            "atual": periodo["label_short"],
            "anterior": periodo["label_prev_short"],
            "mes": f"{_fmt_data_short(ini_mes)} a {_fmt_data_short(fim_mes)}",
        },
        "cards": cards,
        "agregado": {
            "gasto": _fmt_brl(agg.get("gasto", 0)),
            "leads": agg.get("leads", 0),
            "cpl": _fmt_brl(agg.get("cpl", 0)) if agg.get("leads", 0) > 0 else "-",
            "ctr": _fmt_pct(agg.get("ctr", 0)),
            "cpm": _fmt_brl(agg.get("cpm", 0)),
            "impressoes": _fmt_int(agg.get("impressoes", 0)),
            "reach": _fmt_int(agg.get("reach", 0)),
        },
        "mes_atual": {
            "gasto": _fmt_brl(agg_mes.get("gasto", 0)),
            "leads": agg_mes.get("leads", 0),
            "cpl": _fmt_brl(agg_mes.get("cpl", 0)) if agg_mes.get("leads", 0) > 0 else "-",
            "ctr": _fmt_pct(agg_mes.get("ctr", 0)),
            "impressoes": _fmt_int(agg_mes.get("impressoes", 0)),
            "reach": _fmt_int(agg_mes.get("reach", 0)),
        },
        "serie": resultados.get("serie") if isinstance(resultados.get("serie"), list) else [],
        # alias legado
        "serie_30d": resultados.get("serie") if isinstance(resultados.get("serie"), list) else [],
        "adsets": adsets,
        "adsets_erro": adsets_erro,
        "ads": ads,
        "ads_erro": ads_erro,
        "health": resultados.get("health", {}),
    }
    return payload


def _fmt_data_short(iso: str) -> str:
    try:
        return datetime.fromisoformat(iso).strftime("%d/%m")
    except Exception:
        return iso


def _coletar_overview_cached(periodo: dict) -> dict:
    """Cache indexado por (since, until). TTL 5 min."""
    key = f"{periodo['since']}::{periodo['until']}"
    now = time.time()
    entry = _meta_cache.get(key)
    if entry and now - entry["ts"] < _META_TTL:
        return entry["data"]
    data = _coletar_overview(periodo)
    _meta_cache[key] = {"ts": now, "data": data}
    # limpa cache antigo (> 30 min) pra nao crescer infinito
    _limpar_cache_antigo(now)
    return data


def _limpar_cache_antigo(now: float, ttl_max: int = 1800):
    stale = [k for k, v in _meta_cache.items() if now - v["ts"] > ttl_max]
    for k in stale:
        _meta_cache.pop(k, None)


# ============================================================
# Context processor
# ============================================================
@app.context_processor
def inject_globals():
    return {
        "current_year": datetime.now().year,
        "cliente_nome": "Chacara Sonho Verde",
    }


# ============================================================
# Routes
# ============================================================
@app.route("/")
def index():
    return render_template("index.html", active="home")


@app.route("/conjuntos")
def conjuntos():
    return render_template("conjuntos.html", active="conjuntos")


@app.route("/anuncios")
def anuncios():
    return render_template("anuncios.html", active="anuncios")


@app.route("/health")
def health_page():
    return render_template("health.html", active="health")


@app.route("/api/overview")
def api_overview():
    try:
        periodo = _resolver_periodo(request.args)
        data = _coletar_overview_cached(periodo)
    except Exception as e:
        app.logger.exception("erro api_overview")
        return jsonify({"ok": False, "erro": str(e)}), 500
    return jsonify(data)


@app.route("/api/refresh")
def api_refresh():
    """Forca busca nova (ignora cache) do periodo pedido."""
    try:
        periodo = _resolver_periodo(request.args)
        key = f"{periodo['since']}::{periodo['until']}"
        _meta_cache.pop(key, None)
        data = _coletar_overview(periodo)
        _meta_cache[key] = {"ts": time.time(), "data": data}
    except Exception as e:
        app.logger.exception("erro api_refresh")
        return jsonify({"ok": False, "erro": str(e)}), 500
    return jsonify(data)


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


# ============================================================
# Cache warmer - pre-carrega o periodo padrao (last7)
# ============================================================
def _warm_cache_loop():
    def _loop():
        # payload minimo pra chamar _resolver_periodo
        class _EmptyArgs:
            def get(self, *a, **kw): return None
        empty = _EmptyArgs()

        # primeira warmup imediata
        try:
            periodo = _resolver_periodo(empty)  # padrao last7
            _coletar_overview_cached(periodo)
        except Exception:
            traceback.print_exc()
        while True:
            time.sleep(240)  # 4 min
            try:
                periodo = _resolver_periodo(empty)
                key = f"{periodo['since']}::{periodo['until']}"
                _meta_cache.pop(key, None)
                _coletar_overview_cached(periodo)
            except Exception:
                traceback.print_exc()

    t = threading.Thread(target=_loop, daemon=True, name="chacara-warmer")
    t.start()


_warm_cache_loop()


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