// buildium-account-categories
// Pulls each GL account's parent-category from Buildium's chart hierarchy and
// writes it onto accounting.accounts.category, which drives the Income Statement
// subgroups (Operating Income, Administration, Utilities, …). Matched by account
// code, falling back to name. Buildium's GL chart is shared across associations,
// so a single fetch maps code → category for every company.
//
// Body (all optional): { dryRun?: boolean, overwrite?: boolean }
//   dryRun   — compute and report, write nothing (default false)
//   overwrite — replace existing categories too (default true); false only fills blanks
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",
};
const BUILDIUM_BASE = "https://api.buildium.com";

function wait(ms: number) { return new Promise((r) => setTimeout(r, ms)); }
function norm(s: unknown) { return String(s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); }
function stripCode(name: unknown) { return String(name ?? "").replace(/^\s*\d+[\s.\-]*/, "").trim(); }
function json(body: unknown, status = 200) {
  return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, "Content-Type": "application/json" } });
}

async function buildiumFetch(path: string, clientId: string, clientSecret: string, params?: Record<string, string>) {
  const url = new URL(`${BUILDIUM_BASE}${path}`);
  if (params) Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  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) { await wait(600 * Math.pow(2, attempt)); continue; }
    throw new Error(`Buildium ${path} failed [${res.status}]: ${text}`);
  }
  throw new Error(`Buildium ${path} failed after retries`);
}

async function buildiumFetchAll(path: string, clientId: string, clientSecret: string) {
  const all: any[] = [];
  let offset = 0; const limit = 50;
  while (true) {
    const page = await buildiumFetch(path, clientId, clientSecret, { offset: String(offset), limit: String(limit) });
    if (!Array.isArray(page) || page.length === 0) break;
    all.push(...page);
    if (page.length < limit) break;
    offset += limit;
  }
  return all;
}

function glCode(node: any): string {
  return String(node?.AccountNumber ?? node?.Number ?? node?.GlNumber ?? node?.GLNumber ?? node?.Code ?? "").trim();
}

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
  try {
    const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
    const clientId = Deno.env.get("BUILDIUM_API_KEY") ?? "";
    const clientSecret = Deno.env.get("BUILDIUM_API_SECRET") ?? "";
    if (!clientId || !clientSecret) return json({ error: "Buildium API credentials not configured" }, 500);

    const body = await req.json().catch(() => ({} as any));
    const dryRun = body?.dryRun === true;
    const overwrite = body?.overwrite !== false; // default true

    // 1) Pull the GL chart (nested) and map each leaf → its immediate parent's name.
    const roots = await buildiumFetchAll("/v1/glaccounts", clientId, clientSecret);
    const byCode = new Map<string, string>();
    const byName = new Map<string, string>();
    const walk = (parentName: string | null, node: any) => {
      const name = node?.Name ? String(node.Name) : "";
      if (parentName) {
        const code = glCode(node);
        if (code) byCode.set(code, parentName);
        if (name) byName.set(norm(name), parentName);
      }
      const subs = node?.SubAccounts;
      if (Array.isArray(subs)) for (const s of subs) walk(name || parentName, s);
    };
    for (const r of roots) walk(null, r);

    // 2) Apply to accounting.accounts (income/expense only).
    const { data: accts, error } = await supabase.schema("accounting")
      .from("accounts").select("id,code,name,type,category").in("type", ["income", "expense"]);
    if (error) throw error;

    let matched = 0;
    const updates: { id: string; category: string }[] = [];
    for (const a of accts ?? []) {
      const cat = (a.code && byCode.get(String(a.code).trim())) || byName.get(norm(stripCode(a.name)));
      if (!cat) continue;
      matched++;
      if (!overwrite && a.category) continue;
      if (a.category === cat) continue;
      updates.push({ id: a.id, category: cat });
    }

    let updated = 0;
    if (!dryRun) {
      for (const u of updates) {
        const { error: ue } = await supabase.schema("accounting").from("accounts").update({ category: u.category }).eq("id", u.id);
        if (ue) throw ue;
        updated++;
      }
    }

    return json({
      success: true, dryRun, overwrite,
      glAccountsFetched: roots.length,
      mappedCodes: byCode.size,
      accountsScanned: (accts ?? []).length,
      matched, toUpdate: updates.length, updated,
      categories: [...new Set(updates.map((u) => u.category))].sort(),
    });
  } catch (e: any) {
    return json({ error: String(e?.message ?? e) }, 500);
  }
});
