// Cliente Supabase + helpers de I/O.
// Carrega o estado completo dos workspaces para a UI e expõe operações granulares
// (insert/update/delete) em transactions, incomes e settings de workspace.
//
// Mapeamento DB -> UI:
//   workspaces.full_name     -> ws.fullName
//   workspaces.accent_soft   -> ws.accentSoft
//   workspaces.monthly_goal  -> ws.monthlyGoal
//   workspaces.order_index   -> ws._order
//   transactions.description -> txn.desc
//   incomes.description      -> rec.desc
//
// IDs são UUIDs (string). receitas e transactions cada um com seu próprio set.

const SUPABASE_URL = "https://mmokdqeqiepqpvhuabyb.supabase.co";
const SUPABASE_KEY = "sb_publishable_PmvsBAwBL7L3Mr-3E7A19A_lnZiLx9H";

const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);

// ===== CIRCUIT BREAKER ANTI-WIPE =====
// Limita deletes pra blindar contra qualquer bug que tente apagar muita coisa de uma vez.
// Se passar dos limites, ENTRA EM MODO PÂNICO — bloqueia toda escrita até refresh.
// Why: incidente 2026-06-18 apagou milhares de lançamentos por race condition no sync.
//      Esse breaker garante que ANY bug futuro de qualquer natureza não conseguirá
//      apagar mais que ~20 lançamentos antes de travar.
// How to apply: chama _deleteGuard.recordDelete() em TODO delete individual.
//      deleteAll* requer token explícito separado, nunca passa pelo breaker.
const _deleteGuard = {
  recent: [], // timestamps dos deletes recentes
  panicMode: false,
  WINDOW_MS: 60_000,
  MAX_DELETES_PER_WINDOW: 20,
  recordDelete(label) {
    if (this.panicMode) {
      throw new Error("[GUARD] Modo pânico — recarregue a página.");
    }
    const now = Date.now();
    this.recent = this.recent.filter(t => now - t < this.WINDOW_MS);
    this.recent.push(now);
    if (this.recent.length > this.MAX_DELETES_PER_WINDOW) {
      this.panicMode = true;
      console.error(`[GUARD] CIRCUIT BREAKER (${label}): ${this.recent.length} deletes em ${this.WINDOW_MS}ms — bloqueado.`);
      if (window.toast) {
        window.toast({
          message: "🛑 Muitas exclusões detectadas. Operações bloqueadas por segurança — recarregue a página.",
          duration: 60000,
        });
      }
      throw new Error("[GUARD] Excesso de deletes — circuit breaker tripou.");
    }
  },
  reset() { this.recent = []; this.panicMode = false; },
};
window._deleteGuard = _deleteGuard;

// ---------- mapeadores ----------
const wsRowToUi = (row) => ({
  name: row.name,
  fullName: row.full_name,
  accent: row.accent,
  accentSoft: row.accent_soft,
  icon: row.icon,
  monthlyGoal: Number(row.monthly_goal) || 0,
  _order: row.order_index,
  type: row.type || "business",
  categories: Array.isArray(row.categories) ? row.categories : [],
  incomeCategories: Array.isArray(row.income_categories) ? row.income_categories : [],
  payments: Array.isArray(row.payments) ? row.payments : [],
  transactions: [],
  receitas: [],
});

const txRowToUi = (row) => ({
  id: row.id,
  date: row.date,
  desc: row.description,
  value: Number(row.value),
  status: row.status,
  payment: row.payment ?? "",
  category: row.category ?? "",
  subcategory: row.subcategory ?? "",
  enquadramento: row.enquadramento ?? "",
  who: row.who ?? "",
  receiptUrl: row.receipt_url ?? "",
});

const incomeRowToUi = (row) => ({
  id: row.id,
  date: row.date,
  desc: row.description,
  value: Number(row.value),
  status: row.status,
  payment: row.payment ?? "",
  category: row.category ?? "",
  subcategory: row.subcategory ?? "",
  enquadramento: row.enquadramento ?? "",
  who: row.who ?? "",
  receiptUrl: row.receipt_url ?? "",
});

const txUiToRow = (wsKey, t) => ({
  id: t.id,
  workspace_key: wsKey,
  date: t.date,
  description: t.desc,
  value: Number(t.value) || 0,
  status: t.status,
  payment: t.payment || null,
  category: t.category || null,
  subcategory: t.subcategory || null,
  enquadramento: t.enquadramento || null,
  who: t.who || null,
  receipt_url: t.receiptUrl || null,
});

const incomeUiToRow = (wsKey, r) => ({
  id: r.id,
  workspace_key: wsKey,
  date: r.date,
  description: r.desc,
  value: Number(r.value) || 0,
  status: r.status,
  payment: r.payment || null,
  category: r.category || null,
  subcategory: r.subcategory || null,
  enquadramento: r.enquadramento || null,
  who: r.who || null,
  receipt_url: r.receiptUrl || null,
});

// ---------- API ----------
const db = {
  // ---------- auth ----------
  auth: {
    async getSession() {
      const { data } = await supabase.auth.getSession();
      return data.session;
    },
    async getUser() {
      const { data } = await supabase.auth.getUser();
      return data.user;
    },
    onAuthStateChange(cb) {
      return supabase.auth.onAuthStateChange(cb);
    },
    async signInWithPassword(email, password) {
      return supabase.auth.signInWithPassword({ email, password });
    },
    async signUp(email, password, metadata = {}) {
      return supabase.auth.signUp({
        email, password,
        options: { data: metadata, emailRedirectTo: window.location.origin + window.location.pathname },
      });
    },
    async signInWithGoogle() {
      return supabase.auth.signInWithOAuth({
        provider: "google",
        options: { redirectTo: window.location.origin + window.location.pathname },
      });
    },
    async resetPassword(email) {
      return supabase.auth.resetPasswordForEmail(email, {
        redirectTo: window.location.origin + window.location.pathname,
      });
    },
    async updatePassword(newPassword) {
      return supabase.auth.updateUser({ password: newPassword });
    },
    async signOut() {
      return supabase.auth.signOut();
    },
  },

  /** Profile do usuário atual */
  async fetchProfile() {
    const { data, error } = await supabase.from("profiles").select("*").single();
    if (error && error.code !== "PGRST116") console.error("[db] fetchProfile", error);
    return data;
  },
  async updateProfile(patch) {
    const { error } = await supabase.from("profiles").update(patch).eq("id", (await supabase.auth.getUser()).data.user.id);
    if (error) console.error("[db] updateProfile", error);
  },

  /** Adota dados sem user_id (chamado 1x no primeiro login). Idempotente. */
  async claimOrphanData() {
    const { error } = await supabase.rpc("claim_orphan_data");
    if (error) console.error("[db] claim_orphan_data", error);
  },

  // ---------- IA (Anthropic / OpenAI / Google Gemini) ----------
  /**
   * Lê provider + chave do Supabase (profiles). Sincroniza entre dispositivos.
   * Fallback: se DB vazio mas localStorage tem chave antiga, migra automático.
   * Sempre usa o modelo mais forte do provedor.
   */
  async getAiConfig() {
    const user = (await supabase.auth.getUser()).data.user;
    if (!user) return { provider: null, key: null };

    // 1) Tenta ler do Supabase primeiro
    try {
      const { data, error } = await supabase
        .from("profiles")
        .select("ai_provider, ai_key")
        .eq("id", user.id)
        .maybeSingle();
      if (!error && data && data.ai_key) {
        return { provider: data.ai_provider || "anthropic", key: data.ai_key };
      }
    } catch (e) {
      console.warn("[db] getAiConfig — fallback pro localStorage:", e?.message || e);
    }

    // 2) Fallback: localStorage legado (devices antigos pré-migração)
    const lsProvider = localStorage.getItem(`fin_ai_provider_${user.id}`);
    const lsKey = localStorage.getItem(`fin_ai_key_${user.id}`);
    if (lsKey) {
      // Migra automaticamente pro DB pra esse user nunca mais precisar reconfigurar
      this.setAiConfig(lsProvider || "anthropic", lsKey).catch(() => {});
      return { provider: lsProvider || "anthropic", key: lsKey };
    }

    return { provider: "anthropic", key: null };
  },

  async setAiConfig(provider, key) {
    const user = (await supabase.auth.getUser()).data.user;
    if (!user) return;

    // Atualiza Supabase (fonte de verdade)
    const { error } = await supabase
      .from("profiles")
      .update({
        ai_provider: provider || null,
        ai_key: key || null,
      })
      .eq("id", user.id);
    if (error) {
      console.error("[db] setAiConfig — falha ao salvar no Supabase:", error);
      // Sem fallback aqui — se falhou no DB, é melhor o user saber e tentar de novo
      throw error;
    }

    // Mantém localStorage espelhado pra leitura imediata sem round-trip
    if (provider) localStorage.setItem(`fin_ai_provider_${user.id}`, provider);
    if (key) localStorage.setItem(`fin_ai_key_${user.id}`, key);
    else localStorage.removeItem(`fin_ai_key_${user.id}`);
  },

  async clearAiConfig() {
    const user = (await supabase.auth.getUser()).data.user;
    if (!user) return;

    // Limpa no Supabase
    try {
      await supabase
        .from("profiles")
        .update({ ai_provider: null, ai_key: null })
        .eq("id", user.id);
    } catch (e) {
      console.error("[db] clearAiConfig — falha ao limpar no Supabase:", e);
    }

    // Limpa localStorage também
    localStorage.removeItem(`fin_ai_key_${user.id}`);
    localStorage.removeItem(`fin_ai_provider_${user.id}`);
    localStorage.removeItem(`fin_ai_level_${user.id}`);
  },

  /** Modelo mais forte por provedor. Sem escolha de nível — sempre o melhor. */
  _aiModel(provider) {
    if (provider === "openai") return "gpt-4o";
    if (provider === "gemini") return "gemini-1.5-pro";
    return "claude-sonnet-4-6"; // anthropic default
  },

  /**
   * Chama a IA (provider salvo). Retorna o texto de resposta.
   * Roteia entre Anthropic, OpenAI e Google Gemini sempre com o modelo mais forte.
   */
  async callAi({ system, user, maxTokens = 16000 }) {
    const { provider, key } = await this.getAiConfig();
    if (!key) throw new Error("Chave de IA não configurada. Vá em Configurações → Inteligência Artificial.");
    const model = this._aiModel(provider);

    if (provider === "openai") {
      const res = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
        body: JSON.stringify({
          model,
          messages: [{ role: "system", content: system }, { role: "user", content: user }],
          response_format: { type: "json_object" },
          max_tokens: maxTokens,
        }),
      });
      if (!res.ok) {
        const e = await res.json().catch(() => ({}));
        throw new Error(`OpenAI: ${e?.error?.message || res.statusText}`);
      }
      const data = await res.json();
      return data.choices?.[0]?.message?.content || "";
    }

    if (provider === "gemini") {
      const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`;
      const res = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          systemInstruction: { parts: [{ text: system }] },
          contents: [{ parts: [{ text: user }] }],
          generationConfig: { responseMimeType: "application/json", maxOutputTokens: maxTokens },
        }),
      });
      if (!res.ok) {
        const e = await res.json().catch(() => ({}));
        throw new Error(`Gemini: ${e?.error?.message || res.statusText}`);
      }
      const data = await res.json();
      return data.candidates?.[0]?.content?.parts?.[0]?.text || "";
    }

    // Default: Anthropic
    const res = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-api-key": key,
        "anthropic-version": "2023-06-01",
        "anthropic-dangerous-direct-browser-access": "true",
      },
      body: JSON.stringify({
        model,
        max_tokens: maxTokens,
        system,
        messages: [{ role: "user", content: user }],
      }),
    });
    if (!res.ok) {
      const e = await res.json().catch(() => ({}));
      throw new Error(`Anthropic: ${e?.error?.message || res.statusText}`);
    }
    const data = await res.json();
    return data.content?.[0]?.text || "";
  },

  /**
   * IA com vision — analisa imagem + texto. Mesmo roteamento de provider que callAi,
   * mas cada um tem schema próprio pra content multimodal.
   * Todos os 3 modelos padrão (claude-sonnet-4-6, gpt-4o, gemini-1.5-pro) suportam vision.
   */
  async callAiWithImage({ system, user, imageBase64, mimeType = "image/jpeg", maxTokens = 2000 }) {
    const { provider, key } = await this.getAiConfig();
    if (!key) throw new Error("Chave de IA não configurada. Vá em Configurações → Inteligência Artificial.");
    const model = this._aiModel(provider);

    if (provider === "openai") {
      const res = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
        body: JSON.stringify({
          model,
          messages: [
            { role: "system", content: system },
            {
              role: "user",
              content: [
                { type: "image_url", image_url: { url: `data:${mimeType};base64,${imageBase64}` } },
                { type: "text", text: user },
              ],
            },
          ],
          response_format: { type: "json_object" },
          max_tokens: maxTokens,
        }),
      });
      if (!res.ok) {
        const e = await res.json().catch(() => ({}));
        throw new Error(`OpenAI: ${e?.error?.message || res.statusText}`);
      }
      const data = await res.json();
      return data.choices?.[0]?.message?.content || "";
    }

    if (provider === "gemini") {
      const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`;
      const res = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          systemInstruction: { parts: [{ text: system }] },
          contents: [{
            parts: [
              { inline_data: { mime_type: mimeType, data: imageBase64 } },
              { text: user },
            ],
          }],
          generationConfig: { responseMimeType: "application/json", maxOutputTokens: maxTokens },
        }),
      });
      if (!res.ok) {
        const e = await res.json().catch(() => ({}));
        throw new Error(`Gemini: ${e?.error?.message || res.statusText}`);
      }
      const data = await res.json();
      return data.candidates?.[0]?.content?.parts?.[0]?.text || "";
    }

    // Default: Anthropic (Claude)
    const res = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-api-key": key,
        "anthropic-version": "2023-06-01",
        "anthropic-dangerous-direct-browser-access": "true",
      },
      body: JSON.stringify({
        model,
        max_tokens: maxTokens,
        system,
        messages: [{
          role: "user",
          content: [
            { type: "image", source: { type: "base64", media_type: mimeType, data: imageBase64 } },
            { type: "text", text: user },
          ],
        }],
      }),
    });
    if (!res.ok) {
      const e = await res.json().catch(() => ({}));
      throw new Error(`Anthropic: ${e?.error?.message || res.statusText}`);
    }
    const data = await res.json();
    return data.content?.[0]?.text || "";
  },

  // Aliases legados (caso algum código antigo use)
  async getAnthropicKey() { return (await this.getAiConfig()).key; },
  async setAnthropicKey(key) { return this.setAiConfig("anthropic", key); },
  async callClaude(opts) { return this.callAi(opts); },

  // ---------- comprovantes (storage) ----------
  /** Upload de comprovante. Retorna o path interno (ex: "{user_id}/{txId}-{ts}.pdf"). */
  async uploadReceipt(file, txId) {
    const user = (await supabase.auth.getUser()).data.user;
    if (!user) throw new Error("Não autenticado");
    const ext = (file.name.split(".").pop() || "bin").toLowerCase().slice(0, 8);
    const path = `${user.id}/${txId}-${Date.now()}.${ext}`;
    const { error } = await supabase.storage.from("receipts").upload(path, file, {
      cacheControl: "3600",
      upsert: false,
      contentType: file.type || undefined,
    });
    if (error) { console.error("[db] uploadReceipt", error); throw error; }
    return path;
  },
  /** Gera URL temporária (1h) pra ver/baixar o comprovante. */
  async getReceiptUrl(path) {
    if (!path) return null;
    const { data, error } = await supabase.storage.from("receipts").createSignedUrl(path, 3600);
    if (error) { console.error("[db] getReceiptUrl", error); return null; }
    return data.signedUrl;
  },
  /** Apaga comprovante do bucket. */
  async deleteReceipt(path) {
    if (!path) return;
    const { error } = await supabase.storage.from("receipts").remove([path]);
    if (error) console.error("[db] deleteReceipt", error);
  },

  /** Exclui uma workspace e cascata todos os lançamentos/receitas (via FK). */
  async deleteWorkspace(wsKey) {
    const { error } = await supabase.from("workspaces").delete().eq("key", wsKey);
    if (error) console.error("[db] deleteWorkspace", error);
    return error;
  },

  /** Cria uma workspace nova (auto-fill user_id via default da DB). */
  async createWorkspace(ws) {
    const { error } = await supabase.from("workspaces").insert({
      key: ws.key,
      name: ws.name,
      full_name: ws.fullName,
      accent: ws.accent,
      accent_soft: ws.accentSoft || null,
      icon: ws.icon || "briefcase",
      monthly_goal: ws.monthlyGoal || 0,
      order_index: ws._order || 0,
      type: ws.type || "business",
      categories: ws.categories || [],
      income_categories: ws.incomeCategories || [],
      payments: ws.payments || [],
    });
    if (error) console.error("[db] createWorkspace", error);
  },

  /** Carrega tudo do banco e devolve no formato `{ [wsKey]: workspace }` da UI. */
  async fetchAll() {
    // range explícito (0..199_999) pra blindar contra MAX_ROWS do PostgREST.
    // Sem range, alguns servidores aplicam limit silencioso e prevRef do sync
    // ficaria com menos itens que o DB — sync subsequente apaga o que "sumiu".
    // Filter is("deleted_at", null) — ignora itens na lixeira (soft-delete).
    const [wsRes, txRes, inRes] = await Promise.all([
      supabase.from("workspaces").select("*").order("order_index", { ascending: true }),
      supabase.from("transactions").select("*").is("deleted_at", null).range(0, 199_999),
      supabase.from("incomes").select("*").is("deleted_at", null).range(0, 199_999),
    ]);
    if (wsRes.error) throw wsRes.error;
    if (txRes.error) throw txRes.error;
    if (inRes.error) throw inRes.error;

    const out = {};
    for (const row of wsRes.data) out[row.key] = wsRowToUi(row);
    for (const row of txRes.data) {
      if (out[row.workspace_key]) out[row.workspace_key].transactions.push(txRowToUi(row));
    }
    for (const row of inRes.data) {
      if (out[row.workspace_key]) out[row.workspace_key].receitas.push(incomeRowToUi(row));
    }
    return out;
  },

  // ---------- workspaces ----------
  async updateWorkspaceSettings(wsKey, ws) {
    const { error } = await supabase.from("workspaces").update({
      name: ws.name,
      full_name: ws.fullName,
      accent: ws.accent,
      accent_soft: ws.accentSoft,
      icon: ws.icon,
      monthly_goal: ws.monthlyGoal,
      type: ws.type,
      categories: ws.categories,
      income_categories: ws.incomeCategories || [],
      payments: ws.payments,
    }).eq("key", wsKey);
    if (error) console.error("[db] updateWorkspaceSettings", wsKey, error);
  },

  // ---------- transactions ----------
  // Mesma proteção: chunks 100 + retry x3 + upsert idempotente + progresso.
  async insertTransactions(wsKey, txns, onProgress) {
    if (!txns.length) return { saved: 0, total: 0 };
    const rows = txns.map(t => txUiToRow(wsKey, t));
    const CHUNK = 100;
    let totalInserted = 0;
    for (let i = 0; i < rows.length; i += CHUNK) {
      const slice = rows.slice(i, i + CHUNK);
      let attempt = 0;
      let lastError = null;
      while (attempt < 3) {
        const { error } = await supabase.from("transactions").upsert(slice, { onConflict: "id" });
        if (!error) { totalInserted += slice.length; lastError = null; break; }
        lastError = error;
        attempt++;
        await new Promise(r => setTimeout(r, 400 * attempt));
      }
      if (lastError) {
        console.error(`[db] insertTransactions — falhou no chunk ${i}-${i + slice.length}:`, lastError);
        if (window.toast) {
          window.toast({
            message: `⚠ Falha ao salvar ${slice.length} transações. Tente importar de novo.`,
            duration: 10000,
          });
        }
      }
      if (typeof onProgress === "function") {
        try { onProgress({ saved: totalInserted, total: rows.length }); } catch (e) {}
      }
    }
    console.log(`[db] insertTransactions: ${totalInserted}/${rows.length} salvas no Supabase`);
    return { saved: totalInserted, total: rows.length };
  },
  async updateTransaction(id, t) {
    const { workspace_key, id: _id, ...patch } = txUiToRow("", t);
    const { error } = await supabase.from("transactions").update(patch).eq("id", id);
    if (error) console.error("[db] updateTransaction", id, error);
  },
  async deleteTransaction(id) {
    _deleteGuard.recordDelete("deleteTransaction");
    // SOFT DELETE: marca deleted_at, não apaga de verdade. Recuperável via Lixeira.
    const { error } = await supabase.from("transactions").update({ deleted_at: new Date().toISOString() }).eq("id", id);
    if (error) console.error("[db] deleteTransaction (soft)", id, error);
  },
  // Soft-delete em massa — marca deleted_at em todas as transações da workspace.
  // Continua exigindo token (defesa contra refactor) mas agora é reversível.
  async deleteAllTransactions(wsKey, confirmToken) {
    if (confirmToken !== "I_REALLY_WANT_TO_DELETE_ALL_TRANSACTIONS") {
      console.error("[GUARD] deleteAllTransactions BLOQUEADO — chamado sem token. wsKey:", wsKey);
      console.trace();
      throw new Error("[GUARD] deleteAllTransactions requer token explícito.");
    }
    const { error } = await supabase.from("transactions")
      .update({ deleted_at: new Date().toISOString() })
      .eq("workspace_key", wsKey)
      .is("deleted_at", null);
    if (error) console.error("[db] deleteAllTransactions (soft)", error);
  },

  // ---------- incomes ----------
  // Insere em chunks de 100 com retry + upsert (idempotente — seguro chamar
  // 2x sem duplicar). onProgress(savedCount, total) opcional pra reportar
  // progresso em bulk imports.
  async insertIncomes(wsKey, recs, onProgress) {
    if (!recs.length) return { saved: 0, total: 0 };
    const rows = recs.map(r => incomeUiToRow(wsKey, r));
    const CHUNK = 100;
    let totalInserted = 0;
    for (let i = 0; i < rows.length; i += CHUNK) {
      const slice = rows.slice(i, i + CHUNK);
      let attempt = 0;
      let lastError = null;
      while (attempt < 3) {
        // upsert + onConflict=id → idempotente. Se a linha já existe, no-op.
        const { error } = await supabase.from("incomes").upsert(slice, { onConflict: "id" });
        if (!error) { totalInserted += slice.length; lastError = null; break; }
        lastError = error;
        attempt++;
        await new Promise(r => setTimeout(r, 400 * attempt));
      }
      if (lastError) {
        console.error(`[db] insertIncomes — falhou no chunk ${i}-${i + slice.length} após 3 tentativas:`, lastError);
        if (window.toast) {
          window.toast({
            message: `⚠ Falha ao salvar ${slice.length} receitas (chunk ${i}). Tente importar de novo.`,
            duration: 10000,
          });
        }
      }
      // Reporta progresso após cada chunk
      if (typeof onProgress === "function") {
        try { onProgress({ saved: totalInserted, total: rows.length }); } catch (e) {}
      }
    }
    console.log(`[db] insertIncomes: ${totalInserted}/${rows.length} salvas no Supabase`);
    return { saved: totalInserted, total: rows.length };
  },
  async updateIncome(id, r) {
    const { workspace_key, id: _id, ...patch } = incomeUiToRow("", r);
    const { error } = await supabase.from("incomes").update(patch).eq("id", id);
    if (error) console.error("[db] updateIncome", id, error);
  },
  async deleteIncome(id) {
    _deleteGuard.recordDelete("deleteIncome");
    // SOFT DELETE: marca deleted_at, não apaga de verdade. Recuperável via Lixeira.
    const { error } = await supabase.from("incomes").update({ deleted_at: new Date().toISOString() }).eq("id", id);
    if (error) console.error("[db] deleteIncome (soft)", id, error);
  },
  // Soft-delete em massa — marca deleted_at em todas as receitas da workspace.
  async deleteAllIncomes(wsKey, confirmToken) {
    if (confirmToken !== "I_REALLY_WANT_TO_DELETE_ALL_INCOMES") {
      console.error("[GUARD] deleteAllIncomes BLOQUEADO — chamado sem token. wsKey:", wsKey);
      console.trace();
      throw new Error("[GUARD] deleteAllIncomes requer token explícito.");
    }
    const { error } = await supabase.from("incomes")
      .update({ deleted_at: new Date().toISOString() })
      .eq("workspace_key", wsKey)
      .is("deleted_at", null);
    if (error) console.error("[db] deleteAllIncomes (soft)", error);
  },

  // ===== LIXEIRA =====
  // fetchTrash: lista todos os itens deletados nos últimos 90 dias.
  // Retorna { transactions: [...], incomes: [...] } no formato UI.
  async fetchTrash(wsKey) {
    const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
    const [txRes, inRes] = await Promise.all([
      supabase.from("transactions").select("*")
        .eq("workspace_key", wsKey)
        .not("deleted_at", "is", null)
        .gte("deleted_at", ninetyDaysAgo)
        .order("deleted_at", { ascending: false }),
      supabase.from("incomes").select("*")
        .eq("workspace_key", wsKey)
        .not("deleted_at", "is", null)
        .gte("deleted_at", ninetyDaysAgo)
        .order("deleted_at", { ascending: false }),
    ]);
    if (txRes.error) console.error("[db] fetchTrash tx", txRes.error);
    if (inRes.error) console.error("[db] fetchTrash in", inRes.error);
    return {
      transactions: (txRes.data || []).map(r => ({ ...txRowToUi(r), deletedAt: r.deleted_at })),
      incomes:      (inRes.data || []).map(r => ({ ...incomeRowToUi(r), deletedAt: r.deleted_at })),
    };
  },

  async restoreTransaction(id) {
    const { error } = await supabase.from("transactions").update({ deleted_at: null }).eq("id", id);
    if (error) console.error("[db] restoreTransaction", id, error);
    return !error;
  },

  async restoreIncome(id) {
    const { error } = await supabase.from("incomes").update({ deleted_at: null }).eq("id", id);
    if (error) console.error("[db] restoreIncome", id, error);
    return !error;
  },

  // Hard delete da lixeira (apaga DE VERDADE 1 item por vez).
  // Limitado pelo trigger no Postgres (>50 = block).
  async hardDeleteTransaction(id) {
    _deleteGuard.recordDelete("hardDeleteTransaction");
    const { error } = await supabase.from("transactions").delete().eq("id", id);
    if (error) console.error("[db] hardDeleteTransaction", id, error);
    return !error;
  },
  async hardDeleteIncome(id) {
    _deleteGuard.recordDelete("hardDeleteIncome");
    const { error } = await supabase.from("incomes").delete().eq("id", id);
    if (error) console.error("[db] hardDeleteIncome", id, error);
    return !error;
  },

  // ===== PLANO (Free / Pro) =====
  // Lê o plano efetivo do usuário (considera expiração).
  // Retorna { plan: "free"|"pro", expiresAt, source, raw }.
  // Filtra explicitamente por user.id porque a view user_plan não aplica
  // RLS automaticamente (views Postgres rodam como owner por default).
  async getPlan() {
    try {
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) return { plan: "free", expiresAt: null, source: null, raw: "free" };

      const { data, error } = await supabase
        .from("user_plan")
        .select("effective_plan, raw_plan, plan_expires_at, plan_source")
        .eq("id", user.id)
        .maybeSingle();
      if (error) {
        if (error.code === "PGRST116" || error.code === "42P01") {
          return { plan: "free", expiresAt: null, source: null, raw: "free" };
        }
        console.error("[db] getPlan", error);
        return { plan: "free", expiresAt: null, source: null, raw: "free" };
      }
      if (!data) return { plan: "free", expiresAt: null, source: null, raw: "free" };
      return {
        plan: data.effective_plan,
        raw: data.raw_plan,
        expiresAt: data.plan_expires_at,
        source: data.plan_source,
      };
    } catch (e) {
      console.error("[db] getPlan exception", e);
      return { plan: "free", expiresAt: null, source: null, raw: "free" };
    }
  },

  // [DEV] Ativa/desativa Pro manualmente. Em produção (com Stripe), essa RPC vai
  // ser dropada e plano só muda via webhook.
  async devSetPlan(targetPlan) {
    if (targetPlan !== "free" && targetPlan !== "pro") {
      throw new Error("Plan inválido");
    }
    const { data, error } = await supabase.rpc("dev_set_plan", { target_plan: targetPlan });
    if (error) {
      console.error("[db] devSetPlan", error);
      throw error;
    }
    return data;
  },

  // Chama Edge Function create-checkout-session e retorna a URL do Stripe Checkout.
  // plan: "monthly" | "yearly"
  async createCheckoutSession(plan) {
    if (plan !== "monthly" && plan !== "yearly") {
      throw new Error("Plano inválido");
    }
    const successUrl = `${window.location.origin}${window.location.pathname}?stripe_success=1`;
    const cancelUrl = `${window.location.origin}${window.location.pathname}?stripe_cancel=1`;
    const { data, error } = await supabase.functions.invoke("create-checkout-session", {
      body: { plan, successUrl, cancelUrl },
    });
    if (error) {
      console.error("[db] createCheckoutSession", error);
      throw error;
    }
    if (!data?.url) {
      throw new Error(data?.error || "Edge Function não retornou URL do Stripe.");
    }
    return data.url;
  },
};

window.db = db;
window.SUPABASE_CFG = { url: SUPABASE_URL, key: SUPABASE_KEY };
