// Buildium GL Sync (pull-only) — incrementally pulls new Buildium general
// ledger transactions into accounting.journal_entries / journal_entry_lines.
//
// Scope: companies whose books were imported from Buildium (i.e. that already
// have journal entries with external_source = 'buildium_gl'), or companies
// explicitly listed in the request body. Dedupe rides on the existing unique
// index journal_entries_external_uq (company_id, external_source, external_id)
// where external_id is the Buildium transaction id — the same keying the GL
// CSV imports used, so re-pulling an overlapping window never duplicates.
//
// Pull-only by design: nothing is written back to Buildium, and transactions
// edited or deleted in Buildium after they were pulled are NOT reconciled.
//
// Account resolution is STRICT: Buildium GL accounts must be explicitly linked
// to local accounts via public.buildium_gl_account_links (Buildium settings →
// GL Account Map). Unmapped accounts hold their transactions (watermark is not
// advanced) and are flagged in public.buildium_unmapped_gl_accounts.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.1";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version",
};

const BUILDIUM_BASE = "https://api.buildium.com";
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));

// How far behind the watermark each run re-reads. Buildium allows backdating,
// so a pure "since last run" window would miss entries posted into the past.
const OVERLAP_DAYS = 14;

async function buildiumFetch(path: string, clientId: string, clientSecret: string, params?: URLSearchParams) {
  const url = new URL(`${BUILDIUM_BASE}${path}`);
  if (params) url.search = params.toString();
  for (let attempt = 0; attempt < 4; attempt++) {
    const res = await fetch(url.toString(), {
      headers: { "x-buildium-client-id": clientId, "x-buildium-client-secret": clientSecret, Accept: "application/json" },
    });
    if (res.ok) return res.json();
    const text = await res.text();
    if ((res.status === 429 || res.status >= 500) && attempt < 3) {
      const ra = Number(res.headers.get("Retry-After") ?? "");
      await wait(Number.isFinite(ra) && ra > 0 ? ra * 1000 : 600 * Math.pow(2, attempt));
      continue;
    }
    throw new Error(`Buildium ${path} [${res.status}]: ${text}`);
  }
  throw new Error(`Buildium ${path} failed after retries`);
}

async function buildiumFetchAll(path: string, clientId: string, clientSecret: string, base?: URLSearchParams) {
  const all: any[] = [];
  let offset = 0;
  const limit = 1000;
  while (true) {
    const params = new URLSearchParams(base);
    params.set("offset", String(offset));
    params.set("limit", String(limit));
    const page = await buildiumFetch(path, clientId, clientSecret, params);
    if (!Array.isArray(page) || page.length === 0) break;
    all.push(...page);
    // Buildium silently clamps `limit` on some endpoints, so a short page
    // doesn't prove we're done — advance by what we actually got and stop
    // only on an empty page.
    offset += page.length;
  }
  return all;
}

const norm = (v: unknown) => String(v ?? "").toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();

function mapGLAccountType(type: string | null | undefined): string {
  const t = String(type || "").toLowerCase();
  if (t.includes("asset")) return "asset";
  if (t.includes("liabilit")) return "liability";
  if (t.includes("equity")) return "equity";
  if (t.includes("income") || t.includes("revenue")) return "income";
  if (t.includes("expense")) return "expense";
  return "expense";
}

const isoDate = (d: Date) => d.toISOString().slice(0, 10);
const addDays = (iso: string, days: number) => {
  const d = new Date(`${iso}T00:00:00Z`);
  d.setUTCDate(d.getUTCDate() + days);
  return isoDate(d);
};

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });

  try {
    const authHeader = req.headers.get("Authorization");
    if (!authHeader?.startsWith("Bearer ")) {
      return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } });
    }
    const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
    const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
    const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
    const token = authHeader.replace("Bearer ", "");

    // Allow either the service role (pg_cron) or a staff user JWT.
    let claims: any = null;
    try {
      const payload = token.split(".")[1];
      const padded = payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "=");
      claims = JSON.parse(atob(padded));
    } catch {
      return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } });
    }
    const isServiceRole = claims?.role === "service_role";
    if (!isServiceRole) {
      const auth = createClient(supabaseUrl, anonKey, { global: { headers: { Authorization: authHeader } } });
      const { data: roles } = await auth.from("user_roles").select("role").eq("user_id", claims?.sub ?? "");
      const isStaff = (roles || []).some((r: any) => r.role === "admin" || r.role === "manager");
      if (!isStaff) {
        return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } });
      }
    }

    const clientId = Deno.env.get("BUILDIUM_API_KEY") ?? "";
    const clientSecret = Deno.env.get("BUILDIUM_API_SECRET") ?? "";
    if (!clientId || !clientSecret) {
      return new Response(JSON.stringify({ error: "Buildium API credentials not configured" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
    }

    const supabase = createClient(supabaseUrl, serviceKey, { db: { schema: "accounting" } });
    const pub = createClient(supabaseUrl, serviceKey);

    const body = await req.json().catch(() => ({}));

    // Debug: dump a raw transaction (bypasses the sync).
    if (body.debugTransactionId) {
      const tx = await buildiumFetch(`/v1/generalledger/transactions/${body.debugTransactionId}`, clientId, clientSecret);
      return new Response(JSON.stringify(tx), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } });
    }

    // Debug: inspect specific GL accounts directly (bypasses the sync).
    if (Array.isArray(body.debugGlAccountIds) && body.debugGlAccountIds.length > 0) {
      const listed = await buildiumFetchAll("/v1/glaccounts", clientId, clientSecret);
      const out: Record<string, any> = {
        listed_count: listed.length,
        with_parent: listed.filter((g: any) => g.ParentGLAccountId).length,
        with_subaccounts: listed.filter((g: any) => Array.isArray(g.SubAccounts) && g.SubAccounts.length > 0).length,
        sample_keys: listed[0] ? Object.keys(listed[0]) : [],
        sample_subaccount: listed.find((g: any) => Array.isArray(g.SubAccounts) && g.SubAccounts.length > 0)?.SubAccounts?.[0] ?? null,
      };
      for (const id of body.debugGlAccountIds.slice(0, 10)) {
        try {
          const acct = await buildiumFetch(`/v1/glaccounts/${id}`, clientId, clientSecret);
          out[String(id)] = { AccountNumber: acct?.AccountNumber ?? null, Name: acct?.Name ?? null, Type: acct?.Type ?? null, SubType: acct?.SubType ?? null, IsActive: acct?.IsActive ?? null, ParentGLAccountId: acct?.ParentGLAccountId ?? null, inList: listed.some((g: any) => String(g.Id) === String(id)) };
        } catch (e: any) {
          out[String(id)] = { error: e?.message || String(e) };
        }
      }
      return new Response(JSON.stringify(out), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } });
    }
    const companyIdsFilter: string[] = Array.isArray(body.companyIds) ? body.companyIds.filter((s: any) => typeof s === "string" && s) : [];
    const dateFromOverride = typeof body.dateFrom === "string" ? body.dateFrom : null;
    const dateToOverride = typeof body.dateTo === "string" ? body.dateTo : null;
    const dryRun = body.dryRun === true;

    // ---- Companies in scope: Buildium-managed = has buildium_gl entries ----
    const { data: companies, error: cErr } = await supabase
      .from("companies")
      .select("id, name, association_id, acmacc_sync_config, gl_auto_post")
      .not("association_id", "is", null);
    if (cErr) throw cErr;

    const scoped = (companies || []).filter((c: any) => companyIdsFilter.length === 0 || companyIdsFilter.includes(c.id));

    // ---- Buildium association mapping by normalized name (same as import) ----
    const { data: assocRows } = await pub.from("associations").select("id, name");
    const assocNameById = new Map<string, string>();
    for (const a of assocRows || []) assocNameById.set(a.id, a.name);
    const buildiumAssocs = await buildiumFetchAll("/v1/associations", clientId, clientSecret);
    const bAssocIdByName = new Map<string, string>();
    for (const ba of buildiumAssocs) bAssocIdByName.set(norm(ba.Name), String(ba.Id));

    // ---- Buildium chart of accounts (account metadata for line signing) ----
    // /v1/glaccounts returns only top-level accounts as list items; children
    // are nested in each item's SubAccounts array. Flatten recursively.
    const glAccounts = await buildiumFetchAll("/v1/glaccounts", clientId, clientSecret);
    const bGlById = new Map<string, any>();
    const addGl = (g: any) => {
      if (!g?.Id) return;
      bGlById.set(String(g.Id), g);
      if (Array.isArray(g.SubAccounts)) for (const s of g.SubAccounts) addGl(s);
    };
    for (const g of glAccounts) addGl(g);

    // /v1/glaccounts omits some inactive accounts (e.g. prior-management bank
    // accounts) whose ledgers still hold one side of historical transactions —
    // without them, checks/deposits come back unbalanced. /v1/bankaccounts
    // returns inactive banks too, so merge their GL ids in.
    const bankAccts = await buildiumFetchAll("/v1/bankaccounts", clientId, clientSecret);
    for (const b of bankAccts) {
      const gid = String(b.GLAccount?.Id ?? b.Id ?? "");
      if (gid && !bGlById.has(gid)) {
        bGlById.set(gid, {
          Id: gid,
          Name: b.GLAccount?.Name ?? b.Name,
          AccountNumber: b.GLAccount?.AccountNumber ?? null,
          Type: b.GLAccount?.Type ?? "Asset",
          IsActive: b.IsActive,
        });
      }
    }
    const allGlIds = [...bGlById.keys()];

    const today = isoDate(new Date());
    const results: Record<string, any> = {};

    for (const company of scoped) {
      const companyResult: any = { pulled: 0, inserted: 0, skipped_existing: 0, errors: [] as string[] };
      results[company.name] = companyResult;
      try {
        // Watermark: explicit override > stored config > newest imported entry.
        const cfg = (company.acmacc_sync_config ?? {}) as Record<string, any>;
        let watermark: string | null = dateFromOverride || cfg?.buildium_gl?.last_synced_date || null;
        if (!watermark) {
          const { data: maxRow } = await supabase
            .from("journal_entries")
            .select("date")
            .eq("company_id", company.id)
            .eq("external_source", "buildium_gl")
            .order("date", { ascending: false })
            .limit(1)
            .maybeSingle();
          watermark = maxRow?.date ?? null;
        }
        if (!watermark) {
          // No baseline Buildium import and no explicit dateFrom — not a
          // Buildium-managed company; leave it alone.
          companyResult.skipped = "no buildium_gl baseline (pass dateFrom to backfill)";
          continue;
        }

        const bAssocId = bAssocIdByName.get(norm(assocNameById.get(company.association_id) ?? ""));
        if (!bAssocId) {
          companyResult.errors.push("No Buildium association matches the local association name");
          continue;
        }

        const since = dateFromOverride ?? addDays(watermark, -OVERLAP_DAYS);
        const until = dateToOverride ?? today;

        // ---- Pull general ledger entries for the window ----
        // /v1/generalledger returns, per GL account, the actual ledger entries
        // (the same data as Buildium's GL report). Each entry carries the id
        // of the transaction that produced it and a signed amount (debit
        // positive / credit negative), so grouping entries across accounts by
        // transaction id reconstructs complete double-entry journal entries.
        // (The /generalledger/transactions Journal omits implicit AR/cash
        // sides, so it can't be used for this.)
        //
        // glaccountids is required, but sending the whole chart at once blows
        // the URL length limit — chunk it.
        type GlLine = { bGlId: string; amount: number };
        const txById = new Map<string, { date: string; description: string; transactionType: string; lines: GlLine[] }>();
        const CHUNK = 50;
        for (let i = 0; i < allGlIds.length; i += CHUNK) {
          const params = new URLSearchParams();
          params.set("accountingbasis", "Accrual");
          params.set("startdate", since);
          params.set("enddate", until);
          params.set("entitytype", "Association");
          params.set("entityid", bAssocId);
          for (const id of allGlIds.slice(i, i + CHUNK)) params.append("glaccountids", id);
          const ledgers = await buildiumFetchAll("/v1/generalledger", clientId, clientSecret, params);
          for (const ledger of ledgers) {
            const bGlId = String(ledger.GLAccountId ?? ledger.GLAccount?.Id ?? "");
            if (!bGlId) continue;
            for (const e of ledger.Entries ?? []) {
              const txId = String(e.Id ?? "");
              if (!txId) continue;
              let tx = txById.get(txId);
              if (!tx) {
                tx = {
                  date: String(e.Date || "").split("T")[0],
                  description: String(e.Description || e.TransactionType || "Buildium entry").slice(0, 500),
                  transactionType: String(e.TransactionType || ""),
                  lines: [],
                };
                txById.set(txId, tx);
              }
              tx.lines.push({ bGlId, amount: Number(e.Amount) || 0 });
            }
          }
        }
        companyResult.pulled = txById.size;

        // Direct A/P import (buildium-sync "bills") posts Bill / Bill Payment /
        // Check journal entries itself for this company — skip those
        // transaction types here so they aren't double counted.
        if (cfg?.buildium_gl?.exclude_ap) {
          // NOTE: owner "Refund" transactions stay in the GL pull — they are
          // not returned by the /checks endpoint the direct import reads.
          const AP_TYPES = new Set(["bill", "bill payment", "billpayment", "check", "bill credit", "vendor credit", "applied vendor credit"]);
          let excludedAp = 0;
          for (const [txId, tx] of [...txById.entries()]) {
            if (AP_TYPES.has(String(tx.transactionType || "").toLowerCase())) {
              txById.delete(txId);
              excludedAp++;
            }
          }
          companyResult.excluded_ap = excludedAp;
        }

        // ---- Already-imported transaction ids for this company ----
        const existingIds = new Set<string>();
        for (let offset = 0; ; offset += 1000) {
          const { data: rows, error } = await supabase
            .from("journal_entries")
            .select("external_id")
            .eq("company_id", company.id)
            .eq("external_source", "buildium_gl")
            .order("id", { ascending: true })
            .range(offset, offset + 999);
          if (error) throw error;
          for (const r of rows || []) if (r.external_id) existingIds.add(String(r.external_id));
          if ((rows || []).length < 1000) break;
        }

        // ---- Local account resolution: explicit links only ----
        // STRICT mapping: Buildium GL accounts resolve exclusively through
        // public.buildium_gl_account_links (seeded from the historical
        // accounts.external_id backfills). No code/name matching and no
        // auto-create — transactions touching an unmapped account are held,
        // flagged for the GL Account Map UI, and re-pulled once mapped.
        const { data: localAccounts, error: aErr } = await supabase
          .from("accounts")
          .select("id, type, is_bank, name")
          .eq("company_id", company.id);
        if (aErr) throw aErr;
        const localById = new Map<string, any>();
        const bankIds = new Set<string>();
        const acctNameById = new Map<string, string>();
        for (const a of localAccounts || []) {
          localById.set(a.id, a);
          acctNameById.set(a.id, a.name);
          if (a.is_bank) bankIds.add(a.id);
        }
        // Import-mode companies (gl_auto_post=false) don't post Banking entries
        // to the GL; their bank registers are fed only from the GL pull. Mirror
        // each inserted bank line into accounting.transactions so the registers
        // and reconciliation stay current. gl_managed companies skip this — the
        // register is their source of truth and post_transaction_gl drives the GL.
        const materializeRegister = company.gl_auto_post === false;

        const { data: linkRows, error: lkErr } = await pub
          .from("buildium_gl_account_links")
          .select("buildium_gl_id, account_id")
          .eq("association_id", company.association_id);
        if (lkErr) throw lkErr;
        const linkByGlId = new Map<string, string>();
        for (const r of linkRows || []) linkByGlId.set(String(r.buildium_gl_id), r.account_id);

        const unmappedGl = new Map<string, any>(); // buildium_gl_id -> Buildium meta
        function resolveAccount(bGlId: string): { id: string; type?: string } | null {
          const accountId = linkByGlId.get(bGlId);
          if (!accountId) return null;
          return localById.get(accountId) || { id: accountId };
        }

        // ---- Insert new transactions as journal entries ----
        const newTxns = [...txById.entries()]
          .filter(([txId]) => !existingIds.has(txId))
          .sort((a, b) => a[1].date.localeCompare(b[1].date));
        companyResult.skipped_existing = txById.size - newTxns.length;

        for (const [txId, tx] of newTxns) {
          try {
            const lineRows: { account_id: string; debit: number; credit: number; description: string | null }[] = [];
            let resolved = true;
            for (const l of tx.lines) {
              const acct = resolveAccount(l.bGlId);
              if (!acct) {
                const meta = bGlById.get(l.bGlId);
                unmappedGl.set(String(l.bGlId), meta || null);
                if (companyResult.errors.length < 50) {
                  companyResult.errors.push(
                    `tx ${txId}: unmapped GL account ${l.bGlId || "?"} (${meta ? `#${meta.AccountNumber ?? "—"} ${meta.Name ?? "?"}${meta.IsActive === false ? ", inactive" : ""}` : "not returned by /v1/glaccounts"}) — map it in Buildium settings → GL Account Map`,
                  );
                }
                resolved = false;
                break;
              }
              // Amount is signed relative to the account's NATURAL balance:
              // positive means the account's balance increased. An increase
              // is a debit for asset/expense accounts and a credit for
              // liability/equity/income accounts. Buildium's own account type
              // is authoritative for how it signed the amount.
              if (l.amount === 0) continue;
              const meta = bGlById.get(l.bGlId);
              const bType = mapGLAccountType(meta?.Type || (acct as any).type);
              const creditNatural = bType === "liability" || bType === "equity" || bType === "income";
              const increase = l.amount >= 0;
              const isDebit = creditNatural ? !increase : increase;
              lineRows.push({
                account_id: acct.id,
                debit: isDebit ? Math.abs(l.amount) : 0,
                credit: isDebit ? 0 : Math.abs(l.amount),
                description: null,
              });
            }
            if (!resolved) continue;

            const debits = lineRows.reduce((s, l) => s + l.debit, 0);
            const credits = lineRows.reduce((s, l) => s + l.credit, 0);
            if (Math.abs(debits - credits) > 0.005) {
              companyResult.errors.push(`tx ${txId}: unbalanced (${debits.toFixed(2)} vs ${credits.toFixed(2)})`);
              continue;
            }

            if (dryRun) {
              companyResult.inserted += 1;
              continue;
            }

            const { data: je, error: jeErr } = await supabase
              .from("journal_entries")
              .insert({
                company_id: company.id,
                date: tx.date || today,
                description: tx.description,
                reference: null,
                external_source: "buildium_gl",
                external_id: txId,
              })
              .select("id")
              .single();
            if (jeErr) {
              // Unique violation = concurrently imported; anything else is real.
              if ((jeErr as any).code === "23505") {
                companyResult.skipped_existing += 1;
                continue;
              }
              throw jeErr;
            }
            const { data: insertedLines, error: lErr } = await supabase
              .from("journal_entry_lines")
              .insert(lineRows.map((l) => ({ ...l, journal_entry_id: je.id })))
              .select("id, account_id, debit, credit");
            if (lErr) {
              // Don't leave a headerless entry behind.
              await supabase.from("journal_entries").delete().eq("id", je.id);
              throw lErr;
            }
            companyResult.inserted += 1;

            // ---- Materialize bank register rows (import-mode companies) ----
            if (materializeRegister) {
              const lines = insertedLines || [];
              const offsetIds = [...new Set(lines.filter((l: any) => !bankIds.has(l.account_id)).map((l: any) => l.account_id))];
              const offsetId = offsetIds.length === 1 ? offsetIds[0] : null;
              const offsetName = offsetId ? (acctNameById.get(offsetId) ?? null) : null;
              const regRows = lines
                .filter((l: any) => bankIds.has(l.account_id))
                .map((l: any) => {
                  const moneyIn = Number(l.debit) > 0; // bank debit = deposit (money in)
                  return {
                    company_id: company.id,
                    account_id: l.account_id,
                    date: tx.date || today,
                    description: tx.description || "Buildium GL",
                    amount: moneyIn ? Number(l.debit) : Number(l.credit),
                    type: moneyIn ? "credit" : "debit", // register: credit = deposit, debit = withdrawal
                    category: offsetName,
                    coa_account_id: offsetId,
                    cleared: false,
                    journal_entry_line_id: l.id,
                  };
                });
              if (regRows.length > 0) {
                const { error: regErr } = await supabase
                  .from("transactions")
                  .upsert(regRows, { onConflict: "journal_entry_line_id", ignoreDuplicates: true });
                if (regErr) companyResult.errors.push(`register materialize (tx ${txId}): ${regErr.message}`);
                else companyResult.register_rows = (companyResult.register_rows ?? 0) + regRows.length;
              }
            }
          } catch (e: any) {
            companyResult.errors.push(`tx ${txId}: ${e?.message || String(e)}`);
          }
        }

        // ---- Flag unmapped Buildium accounts for the GL Account Map UI ----
        if (unmappedGl.size > 0) {
          const flagRows = [...unmappedGl.entries()].map(([glId, meta]) => ({
            association_id: company.association_id,
            buildium_gl_id: glId,
            buildium_name: meta?.Name ?? null,
            buildium_number: meta?.AccountNumber != null ? String(meta.AccountNumber) : null,
            buildium_type: meta?.Type || meta?.AccountType || null,
            context: "gl_sync",
            last_seen_at: new Date().toISOString(),
          }));
          const { error: flagErr } = await pub
            .from("buildium_unmapped_gl_accounts")
            .upsert(flagRows, { onConflict: "association_id,buildium_gl_id" });
          if (flagErr) companyResult.errors.push(`flagging unmapped accounts failed: ${flagErr.message}`);
          companyResult.unmapped = flagRows.map((r) => ({ buildium_gl_id: r.buildium_gl_id, name: r.buildium_name, number: r.buildium_number }));
        }

        // ---- Advance the watermark ----
        // STRICT mode: unmapped accounts hold their transactions, so keep the
        // old watermark until they're mapped — the next run re-pulls the same
        // window and the external_id dedupe skips whatever already landed.
        if (!dryRun && unmappedGl.size === 0) {
          const nextCfg = {
            ...cfg,
            buildium_gl: {
              ...(cfg.buildium_gl ?? {}),
              last_synced_date: until,
              last_run_at: new Date().toISOString(),
              last_result: {
                pulled: companyResult.pulled,
                inserted: companyResult.inserted,
                errors: companyResult.errors.length,
              },
            },
          };
          await supabase.from("companies").update({ acmacc_sync_config: nextCfg }).eq("id", company.id);
        } else if (unmappedGl.size > 0) {
          companyResult.watermark_held = true;
        }

        // Import-mode: surface no-bill vendor payments (Buildium direct checks that
        // hit an expense account with no A/P leg) as record-only bills. post_bill_gl
        // no-ops for gl_auto_post=false companies, so this never touches the GL.
        if (!dryRun && company.gl_auto_post === false) {
          const { data: madeBills, error: billErr } =
            await supabase.schema("accounting").rpc("autocreate_nobill_vendor_bills", { _company_id: company.id });
          if (billErr) companyResult.errors.push(`autocreate bills: ${billErr.message}`);
          else if (madeBills) companyResult.autobills_created = madeBills;
        }

        companyResult.window = { since, until };
      } catch (e: any) {
        companyResult.errors.push(e?.message || String(e));
      }
    }

    return new Response(JSON.stringify({ success: true, dryRun, results }), {
      status: 200,
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  } catch (e: any) {
    console.error("buildium-gl-sync error", e);
    return new Response(JSON.stringify({ error: e?.message || String(e) }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
  }
});
