-- Per-association Chart of Accounts.
--
-- Today public.chart_of_accounts shares one row across many associations via the
-- association_ids[] array (buildium system: 94 rows shared by up to 13 associations).
-- Editing a number for one association edits the row the others use. This migration
-- gives every association its own row: it splits each shared buildium row into one
-- row per association in its association_ids, repoints all references by each
-- referencing record's association, and swaps the uniqueness to be per-association.
--
-- Nothing is deleted: the original shared row simply becomes the per-association row
-- for its own association_id; clones are added for the other associations. So no
-- foreign key can dangle. association_ids is kept as a single-element mirror of
-- association_id during the transition (existing `association_ids @> [assoc]`
-- callers keep working); drop it in a later cleanup pass.
--
-- Pre-audited on live data: association_id is always in association_ids; account
-- numbers are globally unique within buildium (so no per-association collisions);
-- all buildium parents are buildium rows; every reference's association is genuinely
-- in its target row's array (zero mismatches). One row carries a stray NULL array
-- element (excluded). Two child accounts (4000, 4004) are shared with associations
-- that don't own their parent (4999) — those clones become top-level.

-- Pristine snapshot for rollback / original-parent lookups.
drop table if exists public._coa_perassoc_backup;
create table public._coa_perassoc_backup as table public.chart_of_accounts;

-- Drop the old per-system uniqueness up front: clones reuse the same account_number
-- within buildium, which the old (account_number, accounting_system) index forbids.
-- The new per-association index is created at the end, after the data is consistent.
drop index if exists public.chart_of_accounts_number_per_system_unique;

-- (old_id, association) -> new_id. The owning association keeps the original row id;
-- every other (non-null) association in the array gets a fresh clone id.
-- Only map to associations that still exist — some arrays carry ids of deleted
-- associations (stale memberships); those simply don't get a row.
create temp table coa_map as
select c.id as old_id, u.assoc as association_id,
       case when u.assoc = c.association_id then c.id else gen_random_uuid() end as new_id,
       (u.assoc = c.association_id) as is_keep
from public.chart_of_accounts c
cross join lateral (
  select distinct e as assoc from unnest(c.association_ids) e
  where e is not null and exists (select 1 from public.associations a where a.id = e)
) u
where c.accounting_system = 'buildium';

-- Clone rows for the non-owning associations.
insert into public.chart_of_accounts
  (id, association_id, account_number, account_name, account_type, parent_account_id,
   is_active, description, association_ids, accounting_system, created_at, updated_at)
select m.new_id, m.association_id, c.account_number, c.account_name, c.account_type,
       c.parent_account_id, c.is_active, c.description, array[m.association_id],
       c.accounting_system, c.created_at, now()
from coa_map m
join public.chart_of_accounts c on c.id = m.old_id
where not m.is_keep;

-- Collapse the kept rows' arrays to their single association.
update public.chart_of_accounts c
set association_ids = array[c.association_id], updated_at = now()
where c.accounting_system = 'buildium' and association_ids <> array[c.association_id];

-- Remap parent_account_id to the same-association copy of the parent (original
-- parents read from the pristine snapshot so in-place updates don't interfere).
update public.chart_of_accounts r
set parent_account_id = pm.new_id, updated_at = now()
from coa_map rm
join public._coa_perassoc_backup b on b.id = rm.old_id
join coa_map pm on pm.old_id = b.parent_account_id and pm.association_id = rm.association_id
where r.id = rm.new_id and b.parent_account_id is not null
  and r.parent_account_id is distinct from pm.new_id;

-- Any parent that still isn't owned by the row's association (a child shared with an
-- association that doesn't own its parent) can't nest there — make it top-level.
update public.chart_of_accounts c
set parent_account_id = null, updated_at = now()
from public.chart_of_accounts p
where c.parent_account_id = p.id and c.accounting_system = 'buildium'
  and p.association_id is distinct from c.association_id;

-- Repoint every reference to the association-specific row, by the record's association.
update public.bills x set expense_account_id = m.new_id
from coa_map m where m.old_id = x.expense_account_id and m.association_id = x.association_id and m.new_id <> x.expense_account_id;

update public.budgets x set gl_account_id = m.new_id
from coa_map m where m.old_id = x.gl_account_id and m.association_id = x.association_id and m.new_id <> x.gl_account_id;

update public.owner_ledger_entries x set gl_account_id = m.new_id
from coa_map m where m.old_id = x.gl_account_id and m.association_id = x.association_id and m.new_id <> x.gl_account_id;

-- budget_actuals_monthly is a VIEW derived from the base tables above; it recomputes
-- and needs no repointing (it is still read in verification below).

update public.vendor_coa_mappings x set chart_of_accounts_id = m.new_id
from coa_map m where m.old_id = x.chart_of_accounts_id and m.association_id = x.association_id and m.new_id <> x.chart_of_accounts_id;

update public.units x set assessment_account_id = m.new_id
from coa_map m where m.old_id = x.assessment_account_id and m.association_id = x.association_id and m.new_id <> x.assessment_account_id;

update public.vendors x set default_expense_account_id = m.new_id
from coa_map m where m.old_id = x.default_expense_account_id and m.association_id = x.association_id and m.new_id <> x.default_expense_account_id;

update public.journal_entries x set chart_of_account_id = m.new_id
from coa_map m where m.old_id = x.chart_of_account_id and m.association_id = x.association_id and m.new_id <> x.chart_of_account_id;

-- Add per-association uniqueness. Excludes 'platform' (whose rows are id-keyed
-- mirrors of accounting.accounts and may carry blank/duplicate codes), matching the
-- old index's exclusion. Also validates the importer's existing onConflict target.
create unique index chart_of_accounts_assoc_number_unique
  on public.chart_of_accounts (association_id, account_number)
  where accounting_system <> 'platform';

-- Verify (raises -> rolls back the whole migration on any inconsistency).
do $$
declare _n int; _bad int;
begin
  select count(*) into _n from public.chart_of_accounts where accounting_system='buildium';
  if _n <> 370 then raise exception 'buildium row count % <> expected 370', _n; end if;

  if exists (select 1 from public.chart_of_accounts where accounting_system='buildium' and array_length(association_ids,1) <> 1) then
    raise exception 'buildium rows with non-singleton association_ids remain';
  end if;

  select count(*) into _bad from (
    select x.association_id ra, c.association_id ca from public.bills x join public.chart_of_accounts c on c.id=x.expense_account_id where c.accounting_system='buildium'
    union all select x.association_id, c.association_id from public.budgets x join public.chart_of_accounts c on c.id=x.gl_account_id where c.accounting_system='buildium'
    union all select x.association_id, c.association_id from public.owner_ledger_entries x join public.chart_of_accounts c on c.id=x.gl_account_id where c.accounting_system='buildium'
    union all select x.association_id, c.association_id from public.budget_actuals_monthly x join public.chart_of_accounts c on c.id=x.gl_account_id where c.accounting_system='buildium'
    union all select x.association_id, c.association_id from public.vendor_coa_mappings x join public.chart_of_accounts c on c.id=x.chart_of_accounts_id where c.accounting_system='buildium'
    union all select x.association_id, c.association_id from public.units x join public.chart_of_accounts c on c.id=x.assessment_account_id where c.accounting_system='buildium'
    union all select x.association_id, c.association_id from public.vendors x join public.chart_of_accounts c on c.id=x.default_expense_account_id where c.accounting_system='buildium'
    union all select x.association_id, c.association_id from public.journal_entries x join public.chart_of_accounts c on c.id=x.chart_of_account_id where c.accounting_system='buildium'
  ) t where ra is distinct from ca;
  if _bad > 0 then raise exception '% references point to a different-association COA row', _bad; end if;

  select count(*) into _bad from public.chart_of_accounts c
    join public.chart_of_accounts p on p.id=c.parent_account_id
    where c.accounting_system='buildium' and p.association_id is distinct from c.association_id;
  if _bad > 0 then raise exception '% buildium rows have a cross-association parent', _bad; end if;
end $$;
