// Stripe Financial Connections bank feed (replaces Plaid).
// Actions (mirrors the old `plaid` function contract):
//   create_session { companyId }            -> { client_secret, publishable_key }
//   save_account   { companyId, accountId, fc_account_id, fc_customer_id, institution_name, last4 }
//   sync           { companyId, accountId? } -> { added, skipped, errors }
//   disconnect     { accountId }            -> { success }
// Per-association Stripe keys come from public.stripe_account_mappings, same as the
// payment functions. Imported transactions land uncategorised in accounting.transactions
// (no coa yet), so they appear in the bank feed for matching/categorisation.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

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 json = (body: unknown, status = 200) =>
  new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, "Content-Type": "application/json" } });

const STRIPE = "https://api.stripe.com";

// Encode nested params the way Stripe expects (a[b]=c, arr[]=x).
function encodeForm(obj: Record<string, unknown>, prefix = ""): string {
  const parts: string[] = [];
  for (const [k, v] of Object.entries(obj)) {
    if (v === undefined || v === null) continue;
    const key = prefix ? `${prefix}[${k}]` : k;
    if (Array.isArray(v)) {
      for (const item of v) parts.push(`${encodeURIComponent(`${key}[]`)}=${encodeURIComponent(String(item))}`);
    } else if (typeof v === "object") {
      parts.push(encodeForm(v as Record<string, unknown>, key));
    } else {
      parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`);
    }
  }
  return parts.filter(Boolean).join("&");
}

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
  try {
    const authHeader = req.headers.get("Authorization");
    if (!authHeader) return json({ error: "Missing authorization" }, 401);

    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 ", "");
    const authClient = createClient(supabaseUrl, anonKey);
    const { data: claims, error: authErr } = await authClient.auth.getClaims(token);
    if (authErr || !claims?.claims?.sub) return json({ error: "Unauthorized" }, 401);

    const svc = createClient(supabaseUrl, serviceKey);
    const acct = svc.schema("accounting");

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

    // ---- resolve the company's Stripe config (per association) ----
    async function stripeConfigFor(companyId: string) {
      const { data: company } = await acct.from("companies").select("id, name, association_id").eq("id", companyId).maybeSingle();
      if (!company?.association_id) throw new Error("Company or association not found.");
      const { data: mapping } = await svc.from("stripe_account_mappings")
        .select("stripe_secret_key, stripe_public_key, stripe_account_id")
        .eq("association_id", company.association_id).eq("is_active", true).maybeSingle();
      if (!mapping?.stripe_secret_key) throw new Error("No active Stripe configuration for this association.");
      const headers: Record<string, string> = {
        Authorization: `Bearer ${mapping.stripe_secret_key}`,
        "Content-Type": "application/x-www-form-urlencoded",
      };
      if (mapping.stripe_account_id?.startsWith("acct_")) headers["Stripe-Account"] = mapping.stripe_account_id;
      return { company, mapping, headers };
    }
    async function stripeReq(path: string, method: "GET" | "POST", headers: Record<string, string>, params?: Record<string, unknown>) {
      const url = method === "GET" && params ? `${STRIPE}${path}?${encodeForm(params)}` : `${STRIPE}${path}`;
      const res = await fetch(url, { method, headers, body: method === "POST" && params ? encodeForm(params) : undefined });
      const data = await res.json();
      if (!res.ok) throw new Error(data?.error?.message || `Stripe ${path} failed`);
      return data;
    }

    if (action === "create_session") {
      const { headers, company, mapping } = await stripeConfigFor(body.companyId);
      // The HOA's own bank is linked under a Stripe customer acting as the account holder.
      const customer = await stripeReq("/v1/customers", "POST", headers, {
        name: company.name, metadata: { association_id: company.association_id, purpose: "bank_feed" },
      });
      const session = await stripeReq("/v1/financial_connections/sessions", "POST", headers, {
        account_holder: { type: "customer", customer: customer.id },
        permissions: ["transactions", "balances"],
        filters: { countries: ["US"] },
      });
      const { data: mapping2 } = await svc.from("stripe_account_mappings")
        .select("stripe_public_key").eq("association_id", company.association_id).eq("is_active", true).maybeSingle();
      return json({
        client_secret: session.client_secret, fc_customer_id: customer.id,
        publishable_key: mapping2?.stripe_public_key ?? null,
        stripe_account: mapping.stripe_account_id?.startsWith("acct_") ? mapping.stripe_account_id : null,
      });
    }

    if (action === "save_account") {
      const { headers } = await stripeConfigFor(body.companyId);
      // Subscribe to ongoing transaction refreshes (best-effort; some accounts lack support).
      try { await stripeReq(`/v1/financial_connections/accounts/${body.fc_account_id}/subscribe`, "POST", headers, { features: ["transactions"] }); } catch (_) { /* ok */ }
      const { error } = await acct.from("stripe_bank_connections").upsert({
        company_id: body.companyId, account_id: body.accountId, fc_account_id: body.fc_account_id,
        fc_customer_id: body.fc_customer_id ?? null, institution_name: body.institution_name ?? null,
        last4: body.last4 ?? null, status: "active",
      }, { onConflict: "company_id,account_id" });
      if (error) throw new Error(error.message);
      return json({ success: true });
    }

    if (action === "sync") {
      const { headers } = await stripeConfigFor(body.companyId);
      let q = acct.from("stripe_bank_connections").select("*").eq("company_id", body.companyId).eq("status", "active");
      if (body.accountId) q = q.eq("account_id", body.accountId);
      const { data: conns } = await q;
      let added = 0, skipped = 0; const errors: string[] = [];
      for (const conn of (conns ?? [])) {
        try {
          // Kick a refresh so next sync has fresh data, then pull what Stripe has now.
          try { await stripeReq(`/v1/financial_connections/accounts/${conn.fc_account_id}/refresh`, "POST", headers, { features: ["transactions"] }); } catch (_) { /* ok */ }

          // Already-imported references for this bank account (dedupe).
          const { data: existing } = await acct.from("transactions").select("reference").eq("account_id", conn.account_id).not("reference", "is", null);
          const seen = new Set((existing ?? []).map((r: any) => r.reference));

          let startingAfter: string | undefined; let more = true; const rows: any[] = [];
          while (more) {
            const page = await stripeReq("/v1/financial_connections/transactions", "GET", headers, {
              account: conn.fc_account_id, limit: 100, ...(startingAfter ? { starting_after: startingAfter } : {}),
            });
            for (const t of page.data ?? []) {
              if (t.status === "void") continue;
              if (seen.has(t.id)) continue;
              const cents = Number(t.amount ?? 0);
              const when = t.transacted_at ?? t?.status_transitions?.posted_at ?? null;
              rows.push({
                company_id: body.companyId, account_id: conn.account_id,
                date: when ? new Date(when * 1000).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10),
                description: t.description ?? "Bank transaction",
                amount: Math.abs(cents) / 100,
                type: cents >= 0 ? "credit" : "debit",
                reference: t.id, cleared: t.status === "posted",
              });
            }
            more = !!page.has_more && (page.data?.length ?? 0) > 0;
            if (more) startingAfter = page.data[page.data.length - 1].id;
          }
          if (rows.length) {
            const { error } = await acct.from("transactions").insert(rows);
            if (error) throw new Error(error.message);
            added += rows.length;
          }
          skipped += seen.size;
          await acct.from("stripe_bank_connections").update({ last_sync_at: new Date().toISOString() }).eq("id", conn.id);
        } catch (e) {
          errors.push(`${conn.institution_name ?? conn.fc_account_id}: ${(e as Error).message}`);
        }
      }
      return json({ added, skipped, errors });
    }

    if (action === "disconnect") {
      const { data: conn } = await acct.from("stripe_bank_connections").select("*").eq("account_id", body.accountId).maybeSingle();
      if (conn) {
        try {
          const { headers } = await stripeConfigFor(conn.company_id);
          await stripeReq(`/v1/financial_connections/accounts/${conn.fc_account_id}/disconnect`, "POST", headers, {});
        } catch (_) { /* ignore — still remove our link */ }
        await acct.from("stripe_bank_connections").delete().eq("id", conn.id);
      }
      return json({ success: true });
    }

    return json({ error: `Unknown action: ${action}` }, 400);
  } catch (e) {
    return json({ error: (e as Error).message ?? "Request failed" }, 400);
  }
});
