import { createClient } from 'npm:@supabase/supabase-js@2'

// One-time (idempotent) opening-balance migration from Buildium.
// For a given association: fetches its general-ledger trial balance as of a
// cutoff date from Buildium, maps each GL account to an accounting account
// (creating/flagging bank accounts), rolls prior-year P&L into Retained
// Earnings, writes accounting.opening_balances, and posts the opening GL entry.
// Service-role gated. Activity (owner ledger + bills) on/after the cutoff is
// replayed separately so it does not double-count these opening balances.

const BUILDIUM_BASE = 'https://api.buildium.com'
const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, content-type' }
const J = (o: unknown, s = 200) => new Response(JSON.stringify(o, null, 2), { status: s, headers: { ...cors, 'Content-Type': 'application/json' } })

function claimsOf(token: string): Record<string, unknown> | null {
  const p = token.split('.'); if (p.length < 2) return null
  try { return JSON.parse(atob(p[1].replaceAll('-', '+').replaceAll('_', '/').padEnd(Math.ceil(p[1].length / 4) * 4, '='))) } catch { return null }
}
async function bfetchAll(path: string, cid: string, cs: string, params: Record<string,string>) {
  const out: any[] = []; let offset = 0
  for (;;) {
    const url = new URL(`${BUILDIUM_BASE}${path}`)
    for (const [k,v] of Object.entries({ ...params, limit: '1000', offset: String(offset) })) url.searchParams.set(k, v)
    const res = await fetch(url.toString(), { headers: { 'x-buildium-client-id': cid, 'x-buildium-client-secret': cs, Accept: 'application/json' } })
    if (!res.ok) throw new Error(`Buildium ${path} ${res.status}: ${(await res.text()).slice(0,300)}`)
    const page = await res.json()
    if (!Array.isArray(page) || page.length === 0) break
    out.push(...page); offset += page.length
    if (page.length < 1000) break
  }
  return out
}
const TYPE_MAP: Record<string,string> = { Asset: 'asset', Liability: 'liability', Equity: 'equity', Income: 'income', Expense: 'expense' }

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') return new Response(null, { headers: cors })
  const auth = req.headers.get('Authorization') || ''
  if (claimsOf(auth.replace('Bearer ', '').trim())?.role !== 'service_role') return J({ error: 'Forbidden' }, 403)

  const cid = Deno.env.get('BUILDIUM_API_KEY') ?? '', cs = Deno.env.get('BUILDIUM_API_SECRET') ?? ''
  const sb = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!)
  const acct = sb.schema('accounting')
  const body = await req.json().catch(() => ({}))
  const associationId: string = body.associationId
  const asOfDate: string = body.asOfDate || '2025-12-31'
  const basis: string = body.accountingBasis || 'Accrual'
  if (!associationId) return J({ error: 'associationId required' }, 400)

  const { data: comp } = await acct.from('companies').select('id, name').eq('association_id', associationId).maybeSingle()
  if (!comp) return J({ error: 'No accounting company for this association' }, 400)
  const { data: assocRow } = await sb.from('associations').select('name').eq('id', associationId).maybeSingle()
  const assocName = assocRow?.name || comp.name

  const buildiumAssocs = await bfetchAll('/v1/associations', cid, cs, {})
  const match = buildiumAssocs.find((a: any) => String(a?.Name || '').trim().toLowerCase() === String(assocName).trim().toLowerCase())
  if (!match?.Id) return J({ error: `No Buildium association matched name '${assocName}'` }, 404)

  const balances = await bfetchAll('/v1/glaccounts/balances', cid, cs, {
    entityid: String(match.Id), entitytype: 'Association', asofdate: asOfDate, accountingbasis: basis,
  })

  const netCredit = new Map<string, number>()
  let netIncome = 0
  let reAccountId: string | null = null
  const round2 = (n: number) => Math.round(n * 100) / 100

  for (const b of balances as any[]) {
    const gl = b.GLAccount || {}
    const t = TYPE_MAP[String(gl.Type || '')]
    if (!t) continue
    const bal = Number(b.TotalBalance || 0)
    if (t === 'income') { netIncome += bal; continue }
    if (t === 'expense') { netIncome -= bal; continue }
    const code = String(gl.AccountNumber ?? '').trim() || null
    const name = String(gl.Name || `Buildium GL ${gl.Id}`)
    const { data: accId, error: coaErr } = await acct.rpc('coa_get_or_create', {
      _company_id: comp.id, _match: [name], _code: code, _name: name, _type: t,
    })
    if (coaErr || !accId) continue
    if (gl.IsBankAccount === true) await acct.from('accounts').update({ is_bank: true }).eq('id', accId)
    const signedCredit = (t === 'asset') ? -bal : bal
    netCredit.set(accId, round2((netCredit.get(accId) || 0) + signedCredit))
    if (!reAccountId && t === 'equity' && /retained earnings/i.test(name)) reAccountId = accId
  }

  if (Math.abs(netIncome) > 0.005) {
    if (!reAccountId) {
      const { data: id } = await acct.rpc('coa_get_or_create', { _company_id: comp.id, _match: ['Retained Earnings'], _code: '3010', _name: 'Retained Earnings', _type: 'equity' })
      reAccountId = id as string
    }
    if (reAccountId) netCredit.set(reAccountId, round2((netCredit.get(reAccountId) || 0) + netIncome))
  }

  let net = 0; for (const v of netCredit.values()) net += v; net = round2(net)
  if (Math.abs(net) > 0.005 && reAccountId) netCredit.set(reAccountId, round2((netCredit.get(reAccountId) || 0) - net))

  await acct.from('opening_balances').delete().eq('company_id', comp.id)
  const rows = [...netCredit.entries()]
    .filter(([, v]) => Math.abs(v) > 0.005)
    .map(([account_id, v]) => ({ company_id: comp.id, account_id, debit: v < 0 ? -v : 0, credit: v > 0 ? v : 0 }))
  if (rows.length) {
    const { error: insErr } = await acct.from('opening_balances').insert(rows)
    if (insErr) return J({ error: 'opening_balances insert failed', detail: insErr.message }, 500)
  }
  await acct.from('opening_balances_setup').upsert({ company_id: comp.id, as_of_date: asOfDate, confirmed: true }, { onConflict: 'company_id' })
  const { error: postErr } = await acct.rpc('post_opening_balance_gl', { _company_id: comp.id })
  if (postErr) return J({ error: 'post_opening_balance_gl failed', detail: postErr.message }, 500)

  const totDr = rows.reduce((s, r) => s + r.debit, 0), totCr = rows.reduce((s, r) => s + r.credit, 0)
  return J({
    company_id: comp.id, buildium_association_id: match.Id, as_of_date: asOfDate,
    gl_accounts_from_buildium: balances.length, prior_year_net_income_rolled_to_RE: round2(netIncome),
    opening_rows: rows.length, total_debit: round2(totDr), total_credit: round2(totCr),
    balanced: Math.abs(round2(totDr - totCr)) < 0.005,
  })
})
