// data.jsx — Portal do Cliente (carregado dinamicamente do backend)
//
// Inicialmente expoe globais vazios + um helper `portalApi` pra chamadas
// ao backend. Apos login, `loadPortalData()` popula PROJECT, DIARIES, etc.
// e dispara um evento 'portal-data-ready' pra App re-renderizar.

// ---------- Valores iniciais (vazios) ----------
let PROJECT = {
  name: "", kind: "", code: "", address: "", client: "", sindico: "",
  startDate: null, endDate: null, budget: 0, paid: 0, progress: 0,
};
let WEATHER_TODAY = { cond: "—", temp: 0, min: 0, max: 0, rain: 0, wind: 0, hum: 0 };
let DIARIES = [];
let PHASES = [];
const MONTHS = ["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"];
let FIN = { total: 0, paid: 0, entry: null, installments: [] };
let CAMERAS = [];
let DOCS_OBRA = [];
let DOCS_EQUIPE = [];
let DOCS_EMPRESA = []; // pastas com arquivos compartilhados (Certidoes, Alvara, Seguros, ...)
let NF_EMITIDAS = []; // NFS-e emitidas ao condominio (servicos)
let NF_MATERIAIS = []; // NF-e de materiais comprados (fornecedores)
let EQUIPE_SUMMARY = null;
let PORTAL_PERMISSOES = {}; // { dashboard:true, financeiro:false, ... } — vazio = tudo liberado
let MILESTONES = [];
let EXECUTION = [];
let CURVA_S = [];
let EXEC_VS_TEAM = [];
let FISICO_FINANCEIRO = []; // { mes, progressoFisico, progressoFinanceiro }
let PORTAL_USER = null;

// ---------- Helpers de formatacao ----------
const fmtBRL = (v) => "R$ " + Number(v || 0).toLocaleString("pt-BR", { minimumFractionDigits: 0, maximumFractionDigits: 0 });
const fmtBRL2 = (v) => Number(v || 0).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const fmtDate = (iso) => {
  if (!iso) return "—";
  try {
    const d = new Date(String(iso).slice(0, 10) + "T12:00:00");
    return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "short", year: "numeric" }).replace(".", "");
  } catch { return String(iso); }
};
const fmtDateShort = (iso) => {
  if (!iso) return "—";
  try {
    const d = new Date(String(iso).slice(0, 10) + "T12:00:00");
    return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
  } catch { return String(iso); }
};

// ---------- Slug da obra na URL (/portal-cliente/:slug/) ----------
// Ex: /portal-cliente/condominio-flamboville/ -> "condominio-flamboville"
function readObraSlugFromUrl() {
  const m = window.location.pathname.match(/^\/portal-cliente\/([A-Za-z0-9\-_]+)\/?$/);
  return m ? m[1].toLowerCase() : null;
}
let OBRA_SLUG = readObraSlugFromUrl();

// ---------- API helper ----------
const portalApi = {
  async request(path, options = {}) {
    const res = await fetch(path, {
      credentials: "include",
      headers: { "Content-Type": "application/json", ...(options.headers || {}) },
      ...options,
      body: options.body ? (typeof options.body === "string" ? options.body : JSON.stringify(options.body)) : undefined,
    });
    if (!res.ok) {
      let msg = "";
      try { const j = await res.json(); msg = j.error || JSON.stringify(j); } catch { msg = await res.text().catch(() => res.statusText); }
      const err = new Error(msg || "HTTP " + res.status);
      err.status = res.status;
      throw err;
    }
    const ct = res.headers.get("content-type") || "";
    return ct.includes("application/json") ? res.json() : res.text();
  },
  login(email, password) {
    return this.request("/portal-cliente/auth/login", {
      method: "POST",
      body: { email, password, obraSlug: OBRA_SLUG || undefined },
    });
  },
  logout() {
    return this.request("/portal-cliente/auth/logout", { method: "POST" });
  },
  me() {
    return this.request("/portal-cliente/auth/me");
  },
  publicObraInfo(slug) {
    return this.request("/portal-cliente/api/public/obra-info/" + encodeURIComponent(slug));
  },
  obra()             { return this.request("/portal-cliente/api/obra"); },
  diarios()          { return this.request("/portal-cliente/api/diarios"); },
  financeiro()       { return this.request("/portal-cliente/api/financeiro"); },
  progresso()        { return this.request("/portal-cliente/api/progresso"); },
  documentos()       { return this.request("/portal-cliente/api/documentos"); },
  documentosEmpresa(){ return this.request("/portal-cliente/api/documentos-empresa"); },
  notasFiscais()     { return this.request("/portal-cliente/api/notas-fiscais"); },
  cameras()          { return this.request("/portal-cliente/api/cameras"); },
  cronograma()       { return this.request("/portal-cliente/api/cronograma"); },
};

// ---------- Transformadores (dados reais -> shape do portal) ----------
function mapStatusLabel(status) {
  const norm = String(status || "").toLowerCase();
  if (norm.includes("pausada") || norm.includes("chuva") || norm.includes("parado")) return { status: "stopped", label: "Parado" };
  if (norm.includes("meio") || norm.includes("parcial")) return { status: "partial", label: "Meio-periodo" };
  return { status: "normal", label: "Operacao normal" };
}
function pickIcon(cond) {
  const s = String(cond || "").toLowerCase();
  if (s.includes("chuv") || s.includes("temporal")) return "Rain";
  if (s.includes("sol") || s.includes("ensolarado")) return "Sun";
  return "Cloud";
}
function weekdayShort(iso) {
  try {
    const d = new Date(String(iso).slice(0, 10) + "T12:00:00");
    return ["dom","seg","ter","qua","qui","sex","sab"][d.getDay()] || "—";
  } catch { return "—"; }
}
function summarizeText(txt) {
  if (!txt) return [];
  return String(txt)
    .split(/[\r\n]+/)
    .map((l) => l.trim())
    .filter(Boolean)
    .slice(0, 6);
}
function extractClimaSummary(climaJson) {
  if (!climaJson) return { cond: "—", temp: 0, rain: 0, vento: 0, tempMin: 0, tempMax: 0, umidade: 0 };
  // Formato velho (flat): { descricao, tempC, chuvaMm, vento, umidade }
  if (climaJson.descricao || climaJson.tempC != null || climaJson.cond) {
    return {
      cond: climaJson.descricao || climaJson.cond || "—",
      temp: Math.round(climaJson.tempC || climaJson.temp || 0),
      rain: Math.round((climaJson.chuvaMm || climaJson.rain || 0) * 10) / 10,
      vento: Math.round(climaJson.vento || climaJson.wind || 0),
      tempMin: Math.round(climaJson.tempMin || 0),
      tempMax: Math.round(climaJson.tempMax || 0),
      umidade: Math.round(climaJson.umidade || climaJson.hum || 0),
    };
  }
  // Formato atual: { periodos: { manha: { temperatura, descricao, ... }, tarde: ..., noite: ... } }
  const periodos = climaJson.periodos || {};
  const tarde = periodos.tarde || periodos.manha || periodos.noite || periodos.madrugada || {};
  const temps = Object.values(periodos).map((p) => p?.temperatura).filter((x) => typeof x === "number");
  const rain = Object.values(periodos).reduce((a, p) => a + (Number(p?.precipitacaoMm) || 0), 0);
  return {
    cond: tarde.descricao || "—",
    temp: Math.round(tarde.temperatura || 0),
    rain: Math.round(rain * 10) / 10,
    vento: Math.round(tarde.vento || 0),
    tempMin: temps.length ? Math.round(Math.min(...temps)) : 0,
    tempMax: temps.length ? Math.round(Math.max(...temps)) : 0,
    umidade: 0,
  };
}

function transformDiario(d) {
  const statusFrentes = d.StatusFrentes || [];
  const paused = statusFrentes.some((s) => s.status === "pausada");
  const rainNote = (d.ocorrencias || "").toLowerCase().includes("chuva");
  let status;
  if (paused && rainNote) status = mapStatusLabel("parado");
  else if (statusFrentes.length === 0) status = mapStatusLabel("parado");
  else status = mapStatusLabel("normal");

  const clima = extractClimaSummary(d.climaJson);

  // Agrupa equipe por cargo
  const team = [];
  const byCargo = new Map();
  const equipeList = d.EquipeRegistros || d.DiarioEquipes || [];
  for (const e of equipeList) {
    const nome = e.Funcionario?.cargo || "Equipe";
    const prev = byCargo.get(nome) || { role: nome, count: 0, company: "—" };
    byCargo.set(nome, { ...prev, count: prev.count + 1 });
  }
  byCargo.forEach((v) => team.push(v));

  // Atividades: vem dos status de frentes com comentario + opcionalmente do texto livre
  const activities = [];
  for (const s of statusFrentes) {
    const frente = s.FrenteTrabalho?.nome || "Frente";
    const coment = (s.comentario || "").trim();
    if (coment) activities.push(frente + ": " + coment);
    else if (s.status === "em_execucao") activities.push(frente + " em execucao (" + (s.progresso || 0) + "%)");
    else if (s.status === "concluida") activities.push(frente + " concluida");
    else if (s.status === "pausada") activities.push(frente + " pausada");
  }
  // Fallback para texto livre
  if (activities.length === 0 && d.texto) {
    activities.push(...summarizeText(d.texto));
  }

  return {
    id: "D-" + String(d.id).padStart(3, "0"),
    date: String(d.data).slice(0, 10),
    weekday: weekdayShort(d.data),
    status: status.status,
    statusLabel: status.label,
    weather: {
      cond: clima.cond,
      icon: pickIcon(clima.cond),
      temp: clima.temp,
      rain: clima.rain,
    },
    team,
    activities,
    photos: (d.DiarioFotos || []).length,
    author: d.vistoriaEngenheiro ? "Vistoriado pelo engenheiro" : "Registrado em campo",
    note: d.ocorrencias || null,
    _raw: d,
  };
}

function transformObraToProject(obra) {
  const end = obra.Endereco || {};
  const parts = [];
  if (end.logradouro) parts.push(end.logradouro);
  if (end.numero) parts.push(", " + end.numero);
  if (end.bairro) parts.push(" — " + end.bairro);
  if (end.cidade) parts.push(", " + end.cidade);
  if (end.uf) parts.push("/" + end.uf);
  const endereco = parts.join("");
  return {
    name: obra.nome || "",
    kind: obra.descricao || "",
    code: "OB-" + String(obra.id).padStart(5, "0"),
    address: endereco || "—",
    client: obra.sindicoNome || "—",
    sindico: obra.sindicoNome || "—",
    startDate: obra.cronogramaInicio || obra.createdAt,
    endDate: obra.cronogramaFimPrevisto || null,
    budget: Number(obra.contratoValorTotal) || 0,
    paid: 0,
    progress: (Number(obra.progressoGeral) || 0) / 100,
  };
}

function buildMilestones(obra, financeiro) {
  const out = [];
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const daysBetween = (iso) => {
    const d = new Date(String(iso).slice(0, 10) + "T12:00:00");
    return Math.max(0, Math.round((d - today) / 86400000));
  };
  const nextParcela = (financeiro?.parcelas || []).find((p) => {
    if (p.pago === true) return false;
    const s = String(p.status || "").toLowerCase();
    if (s === "paid" || s === "pago" || s === "quitada") return false;
    const iso = p.data || p.due || "";
    if (!iso) return false;
    return new Date(String(iso).slice(0, 10) + "T12:00:00") >= today;
  });
  if (nextParcela) {
    const iso = nextParcela.data || nextParcela.due;
    out.push({ date: iso, label: "Parcela " + (nextParcela.numero || nextParcela.n || ""), days: daysBetween(iso), fin: true });
  }
  if (obra.cronogramaFimPrevisto) {
    const iso = obra.cronogramaFimPrevisto;
    out.push({ date: iso, label: "Entrega prevista", days: daysBetween(iso) });
  }
  for (const f of (obra.FrenteTrabalhos || [])) {
    if ((Number(f.progressoManual) || 0) >= 100) continue;
    if (out.length >= 4) break;
    const d = new Date(today.getTime() + 30 * 86400000);
    out.push({ date: d.toISOString().slice(0, 10), label: "Concluir " + f.nome, days: 30 });
  }
  return out.slice(0, 4);
}

function transformFinanceiro(fin) {
  const total = Number(fin.contratoValorTotal) || 0;
  const today = new Date(); today.setHours(0,0,0,0);
  const nowISO = today.toISOString().slice(0,10);
  const parcelas = (fin.parcelas || []).map((p, idx) => {
    // Diferentes shapes: { pago: true/false } OU { status: "paid"/"pending" }
    let st = "pending";
    if (p.pago === true || String(p.status || "").toLowerCase() === "paid" || String(p.status || "").toLowerCase() === "pago") {
      st = "paid";
    } else if (String(p.status || "").toLowerCase() === "upcoming") {
      st = "upcoming";
    }
    return {
      n: p.numero || p.n || (idx + 1),
      due: String(p.data || p.due || "").slice(0, 10),
      amount: Number(p.valor || p.amount) || 0,
      status: st,
      paidOn: p.pagoEm || p.paidOn || null,
      label: p.label || null,
      // Flags do backend: 2ª via de boleto disponivel + NF emitida
      boletoDisponivel: p.boletoDisponivel === true,
      nfEmitida: p.nfEmitida === true,
    };
  });
  // marca a proxima nao paga como "upcoming"
  const nextIdx = parcelas.findIndex((p) => p.status !== "paid" && p.due >= nowISO);
  if (nextIdx >= 0) parcelas[nextIdx].status = "upcoming";

  const paid = parcelas.filter((p) => p.status === "paid").reduce((a, p) => a + p.amount, 0)
    + (fin.entradaValor && (String(fin.entradaStatus || "").toLowerCase() === "pago" || fin.entradaStatus === true) ? Number(fin.entradaValor) : 0);

  const entradaPaid = String(fin.entradaStatus || "").toLowerCase() === "pago" || fin.entradaStatus === true;
  const entry = fin.entradaValor ? {
    label: "Entrada",
    amount: Number(fin.entradaValor) || 0,
    date: fin.entradaData || null,
    status: entradaPaid ? "paid" : "pending",
    paidOn: fin.entradaPagoEm || null,
    boletoDisponivel: fin.entradaBoletoDisponivel === true,
  } : null;
  return { total, paid, entry, installments: parcelas };
}

function transformProgresso(prog) {
  const execution = (prog.frentes || []).map((f) => ({
    phase: f.nome,
    planned: 1.0,
    actual: (Number(f.progressoManual) || 0) / 100,
  }));
  const curva = (prog.curva || []).map((c) => ({
    m: c.mes,
    planned: null,
    realized: c.realizado,
  }));
  curva.forEach((c, i) => {
    c.planned = Math.round(((i + 1) / Math.max(curva.length, 1)) * 100);
  });
  return { execution, curva };
}

function transformDocumentos(docs) {
  const mapDoc = (kind) => (d) => ({
    id: d.id,
    kind, // "obra" ou "entrega"
    name: d.nomeOriginal || d.descricao || "Documento",
    type: ((d.mimetype || "").split("/").pop() || "pdf").toUpperCase().slice(0, 4),
    size: d.tamanho ? ((d.tamanho / 1024).toFixed(0) + " KB") : "—",
    date: String(d.createdAt || "").slice(0, 10),
    category: d.categoria || d.tipo || "Geral",
    mimetype: d.mimetype || null,
  });
  return {
    obra: (docs.obra || []).map(mapDoc("obra")),
    equipe: (docs.entrega || []).map(mapDoc("entrega")),
    // Resumo da equipe alocada (sem expor PII individual)
    equipeSummary: docs.equipe || null,
  };
}

function transformCronograma(cron) {
  const startDate = cron.inicio ? new Date(String(cron.inicio).slice(0, 10) + "T12:00:00") : new Date();
  const endDate = cron.fimPrevisto ? new Date(String(cron.fimPrevisto).slice(0, 10) + "T12:00:00") : new Date(startDate.getTime() + 365 * 86400000);
  const totalMonths = Math.max(1, Math.round((endDate - startDate) / (30 * 86400000)));
  const list = cron.frentes || [];
  const phases = list.map((f, i) => {
    const start = (i / Math.max(list.length, 1)) * totalMonths;
    const end = start + Math.max(2, totalMonths / Math.max(list.length, 1) * 1.5);
    const progress = (Number(f.progresso) || 0) / 100;
    let stage = "future";
    if (progress >= 1) stage = "done";
    else if (progress > 0) stage = "active";
    return { name: f.nome, start, end, progress, stage };
  });
  const months = [];
  for (let i = 0; i < totalMonths; i++) {
    const d = new Date(startDate.getTime() + i * 30 * 86400000);
    months.push(d.toLocaleDateString("pt-BR", { month: "short" }).replace(".", "").trim());
  }
  return { phases, months };
}

/**
 * Fisico x Financeiro — espelha buildFisicoFinanceiroSeries do sistema admin.
 * Usa a curva de progresso por mes (progressoMedio) + pagamentos (entrada + parcelas pagas)
 * para montar duas series: progresso fisico (0-100%) e progresso financeiro (valor pago / valor total * 100).
 */
function buildFisicoFinanceiroSeries(curva, financeiro) {
  const total = Number(financeiro?.contratoValorTotal) || 0;
  if (!curva?.length || total <= 0) return [];
  // Lista de eventos de pagamento: entrada + parcelas pagas, ordenados por data
  const events = [];
  if (financeiro.entradaValor && (String(financeiro.entradaStatus || "").toLowerCase() === "pago" || financeiro.entradaStatus === true)) {
    events.push({
      date: financeiro.entradaData || (financeiro.parcelas?.[0]?.data || null),
      value: Number(financeiro.entradaValor) || 0,
    });
  }
  for (const p of (financeiro.parcelas || [])) {
    const pago = p.pago === true
      || String(p.status || "").toLowerCase() === "paid"
      || String(p.status || "").toLowerCase() === "pago";
    if (!pago) continue;
    const val = Number(p.valor) || 0;
    if (val <= 0) continue;
    events.push({ date: p.pagoEm || p.data || null, value: val });
  }
  events.sort((a, b) => {
    const ta = a.date ? new Date(a.date).getTime() : 0;
    const tb = b.date ? new Date(b.date).getTime() : 0;
    return ta - tb;
  });

  let acc = 0;
  let eventIdx = 0;
  return curva.map((c) => {
    // c.mes no formato YYYY-MM; considera fim do mes para comparar com pagamentos
    const monthEnd = new Date(c.mes + "-31T23:59:59");
    while (eventIdx < events.length) {
      const ev = events[eventIdx];
      const evDate = ev.date ? new Date(ev.date) : null;
      if (!evDate || isNaN(evDate) || evDate <= monthEnd) {
        acc += ev.value;
        eventIdx++;
      } else {
        break;
      }
    }
    const progressoFinanceiro = Math.min(100, Math.round((acc / total) * 100 * 10) / 10);
    return {
      mes: c.mes,
      progressoFisico: Math.round(Number(c.realizado) || 0),
      progressoFinanceiro,
    };
  });
}

/**
 * Progresso vs mao de obra (espelha o grafico do admin "Progresso vs mao de obra").
 * - trabalhadores : media diaria de pessoas no canteiro no mes (barras, eixo esq)
 * - progresso     : % realizado do mes (linha, eixo dir 0-100)
 * Retorna em ordem cronologica (mais antigo -> mais recente).
 *
 * Expomos tambem nos aliases antigos { m, exec, team } para compat.
 */
function buildExecVsTeam(diariesRaw, curva) {
  const byMonth = new Map();
  for (const d of diariesRaw) {
    const ym = String(d.data).slice(0, 7);
    const team = ((d.EquipeRegistros || d.DiarioEquipes) || []).length;
    const prev = byMonth.get(ym) || { days: 0, team: 0 };
    byMonth.set(ym, { days: prev.days + 1, team: prev.team + team });
  }
  const progByMonth = new Map();
  (curva || []).forEach((c) => progByMonth.set(c.mes, Number(c.realizado) || 0));

  return Array.from(byMonth.entries())
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([m, v]) => {
      const trabalhadores = Math.round(v.team / Math.max(v.days, 1));
      const progresso = Math.round(progByMonth.get(m) ?? 0);
      return {
        m,
        mes: m,
        trabalhadores,
        progresso,
        // Aliases legados:
        exec: progresso,
        team: trabalhadores,
      };
    });
}

// ---------- Loader principal ----------
async function loadPortalData() {
  try {
    // Módulos desligados pelo admin respondem 403 — capturamos cada um pra que
    // um módulo oculto não derrube o carregamento do portal inteiro.
    const [meRes, obraRes, diariosRes, financeiroRes, progressoRes, documentosRes, documentosEmpresaRes, notasFiscaisRes, camerasRes, cronogramaRes] = await Promise.all([
      portalApi.me(),
      portalApi.obra(),
      portalApi.diarios().catch(() => ({ diarios: [] })),
      portalApi.financeiro().catch(() => ({})),
      portalApi.progresso().catch(() => ({ frentes: [], curva: [] })),
      portalApi.documentos().catch(() => ({ obra: [], entrega: [], equipe: null })),
      portalApi.documentosEmpresa().catch(() => ({ pastas: [] })),
      portalApi.notasFiscais().catch(() => ({ emitidas: [], materiais: [] })),
      portalApi.cameras().catch(() => ({ cameras: [] })),
      portalApi.cronograma().catch(() => ({ frentes: [] })),
    ]);

    PORTAL_USER = meRes.user;
    // Atualiza OBRA_SLUG com o valor canonico vindo do backend
    if (meRes.obra && meRes.obra.slug) {
      OBRA_SLUG = meRes.obra.slug;
      window.OBRA_SLUG = OBRA_SLUG;
      // Se a URL nao tinha slug, atualiza (sem recarregar pagina)
      const currentPath = window.location.pathname;
      const desiredPath = "/portal-cliente/" + OBRA_SLUG + "/";
      if (currentPath === "/portal-cliente" || currentPath === "/portal-cliente/") {
        window.history.replaceState(null, "", desiredPath);
      }
    }
    PROJECT = transformObraToProject(obraRes);
    // Herda sindicoNome real do /auth/me
    if (meRes.obra?.sindicoNome) PROJECT.sindico = meRes.obra.sindicoNome;
    // Permissões de módulo (o que o cliente vê) — usado pra filtrar a navegação
    PORTAL_PERMISSOES = obraRes.permissoes || {};
    FIN = transformFinanceiro(financeiroRes);
    PROJECT.paid = FIN.paid;

    const diariosRaw = diariosRes.diarios || [];
    DIARIES = diariosRaw.map(transformDiario);

    if (diariosRaw[0] && diariosRaw[0].climaJson) {
      const summary = extractClimaSummary(diariosRaw[0].climaJson);
      WEATHER_TODAY = {
        cond: summary.cond,
        temp: summary.temp,
        min: summary.tempMin,
        max: summary.tempMax,
        rain: summary.rain,
        wind: summary.vento,
        hum: summary.umidade,
      };
    }

    const prog = transformProgresso(progressoRes);
    EXECUTION = prog.execution;
    CURVA_S = prog.curva;
    PHASES = (progressoRes.frentes || []).map((f, i) => ({
      name: f.nome,
      start: i,
      end: i + 2,
      progress: (Number(f.progressoManual) || 0) / 100,
      stage: (Number(f.progressoManual) || 0) >= 100 ? "done" : (Number(f.progressoManual) || 0) > 0 ? "active" : "future",
    }));
    EXEC_VS_TEAM = buildExecVsTeam(diariosRaw, progressoRes?.curva || []);

    FISICO_FINANCEIRO = buildFisicoFinanceiroSeries(progressoRes?.curva || [], financeiroRes || {});

    const cronog = transformCronograma(cronogramaRes);
    if (cronog.phases.length) PHASES = cronog.phases;

    const docs = transformDocumentos(documentosRes);
    DOCS_OBRA = docs.obra;
    DOCS_EQUIPE = docs.equipe;
    EQUIPE_SUMMARY = docs.equipeSummary;
    DOCS_EMPRESA = (documentosEmpresaRes?.pastas || []).map((p) => ({
      id: p.id,
      nome: p.nome,
      slug: p.slug,
      categoriaId: p.categoriaId,
      totalItens: p.totalItens,
      arquivos: (p.arquivos || []).map((a) => ({
        nome: a.nome,
        slug: p.slug,
        tamanho: a.tamanho,
        atualizadoEm: a.atualizadoEm,
        type: ((a.nome.split(".").pop()) || "pdf").toUpperCase().slice(0, 4),
      })),
    }));

    // Notas fiscais (emitidas ao condominio + materiais de fornecedores)
    NF_EMITIDAS = (notasFiscaisRes?.emitidas || []).map((n) => ({
      parcela: n.parcela,
      num: n.num,
      emissao: n.emissao,
      competencia: n.competencia,
      desc: n.desc,
      valor: Number(n.valor) || 0,
      iss: Number(n.iss) || 0,
      pago: !!n.pago,
      hasFile: !!n.hasFile,
      hasXml: !!n.hasXml,
      status: "emitida",
    }));
    NF_MATERIAIS = (notasFiscaisRes?.materiais || []).map((n) => ({
      id: n.id,
      num: n.num,
      emissao: n.emissao,
      fornecedor: n.fornecedor,
      cnpj: n.cnpj,
      desc: n.desc,
      cat: n.cat,
      valor: Number(n.valor) || 0,
      hasFile: !!n.hasFile,
      hasXml: !!n.hasXml,
    }));

    CAMERAS = (camerasRes.cameras || []).map((c) => ({
      id: "CAM-" + String(c.id).padStart(2, "0"),
      camId: c.id,                          // id numérico real — usado na URL do HLS
      name: c.nome,
      zone: c.zona || c.tipo || "—",
      online: !!c.rtmpActive,               // câmera empurrando agora?
      tipo: c.tipo || "local",
      // URL do stream HLS servida pelo backend (proxy de portal, autenticado).
      hlsUrl: "/portal-cliente/api/cameras/" + c.id + "/hls/index.m3u8",
    }));

    MILESTONES = buildMilestones(obraRes, financeiroRes);

    Object.assign(window, {
      PROJECT, WEATHER_TODAY, DIARIES, PHASES, MONTHS, FIN, CAMERAS,
      DOCS_OBRA, DOCS_EQUIPE, DOCS_EMPRESA, EQUIPE_SUMMARY, MILESTONES, EXECUTION, CURVA_S, EXEC_VS_TEAM,
      FISICO_FINANCEIRO, NF_EMITIDAS, NF_MATERIAIS, PORTAL_PERMISSOES,
      PORTAL_USER,
    });

    window.dispatchEvent(new CustomEvent("portal-data-ready"));
    return true;
  } catch (err) {
    console.error("[portal] loadPortalData error:", err);
    window.dispatchEvent(new CustomEvent("portal-data-error", { detail: err }));
    return false;
  }
}

Object.assign(window, {
  PROJECT, WEATHER_TODAY, DIARIES, PHASES, MONTHS, FIN, CAMERAS,
  DOCS_OBRA, DOCS_EQUIPE, DOCS_EMPRESA, EQUIPE_SUMMARY, MILESTONES, EXECUTION, CURVA_S, EXEC_VS_TEAM,
  FISICO_FINANCEIRO, NF_EMITIDAS, NF_MATERIAIS, PORTAL_PERMISSOES,
  fmtBRL, fmtBRL2, fmtDate, fmtDateShort,
  portalApi, loadPortalData, PORTAL_USER, OBRA_SLUG,
});
