-- Import-mode register double-count guard.
--
-- Background: import-mode companies (gl_auto_post=false) get their GL from the
-- Buildium GL pull (external_source='buildium_gl'). buildium-gl-sync materializes
-- a bank-register row per GL bank line WITH journal_entry_line_id set, so those
-- are recognized as "already in GL" and never re-post. But register rows created
-- by an ad-hoc materialization (e.g. Village Woods' finishing step) were left
-- UNLINKED (journal_entry_line_id null, exclude_from_gl false). They sat harmless
-- until the "categorize register transactions from Buildium GL lines" op
-- (buildium-payee-backfill partymap) assigned them a vendor/customer/coa — which
-- gave post_transaction_gl a counter account and made it post them, DUPLICATING
-- the Buildium GL pull (A/P, A/R and bank all double-counted).
--
-- This migration:
--   #1  Freezes any remaining unguarded register row whose exact bank movement
--       already exists in buildium_gl, across ALL import-mode companies. Rows that
--       are genuinely missing from Buildium (intentionally-posted manual items)
--       have no matching buildium_gl line and are left postable.
--   #2  Adds a backstop inside post_transaction_gl: it never posts a register row
--       whose exact bank movement (bank account + date + amount + direction) is
--       already recorded in buildium_gl — regardless of what a later categorization
--       run assigns. This enforces the rule at the GL choke point (the ad-hoc
--       apply step that writes party/coa onto the register has no committed home).
--
-- Match key = (company, bank account_id, date, amount, direction). For a register
-- row, type 'credit' = money in = bank debit; otherwise = money out = bank credit.
-- Genuinely-missing manual items don't match (verified against Bent Oak's
-- insurance/electric/water and Bridgewater's misc-income donations), so they keep
-- posting. A coincidental same-account/date/amount/direction collision with an
-- unrelated buildium_gl movement is rare and resolves conservatively (no double
-- count); such a row can still be surfaced and posted as an explicit manual JE.

create or replace function accounting.post_transaction_gl(_txn_id uuid)
 returns void language plpgsql security definer set search_path to 'public', 'accounting'
as $function$
declare t accounting.transactions%rowtype; _counter uuid; _je uuid; _amt numeric;
begin
  select * into t from accounting.transactions where id=_txn_id;
  if not found then return; end if;
  perform accounting._gl_clear(t.company_id, 'acmacc_txn', t.id::text);
  if coalesce(t.voided,false) then return; end if;
  if t.journal_entry_line_id is not null then return; end if;  -- already in GL via Buildium pull
  if coalesce(t.exclude_from_gl,false) then return; end if;    -- frozen legacy/duplicate register row
  if t.transfer_id is not null or t.deposit_id is not null then return; end if;
  if t.account_id is null then return; end if;
  _amt := coalesce(t.amount,0);
  if _amt = 0 then return; end if;

  -- Backstop: if buildium_gl already records this exact bank movement, this
  -- register row is a duplicate of the Buildium GL pull. Never double-post it,
  -- no matter what party/category a categorization run later assigns.
  if exists (
    select 1
    from accounting.journal_entries je
    join accounting.journal_entry_lines l on l.journal_entry_id = je.id
    where je.company_id = t.company_id
      and je.external_source = 'buildium_gl'
      and je.date = t.date
      and l.account_id = t.account_id
      and ((t.type = 'credit' and l.debit = _amt) or (t.type <> 'credit' and l.credit = _amt))
  ) then
    return;
  end if;

  _counter := case
    when t.customer_id is not null then accounting.coa_ar(t.company_id)
    when t.coa_account_id is not null then t.coa_account_id
    when t.vendor_id is not null then accounting.coa_ap(t.company_id)
    else null end;
  if _counter is null then return; end if;

  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (t.company_id, t.date, coalesce(nullif(t.description,''), 'Bank transaction'), t.reference, 'acmacc_txn', t.id::text)
  returning id into _je;
  if t.type = 'credit' then
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
      (_je, t.account_id, _amt, 0, t.description), (_je, _counter, 0, _amt, t.description);
  else
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
      (_je, _counter, _amt, 0, t.description), (_je, t.account_id, 0, _amt, t.description);
  end if;
end$function$;

-- One-time cleanup: freeze remaining unguarded buildium-overlapping register rows
-- across all import-mode companies. The UPDATE fires trg_acct_txn_gl, which clears
-- any duplicate acmacc_txn JE these rows had posted. Idempotent (already-frozen and
-- already-linked rows are skipped; intentionally-posted manual items don't match).
update accounting.transactions t
set exclude_from_gl = true
from accounting.companies c
where t.company_id = c.id
  and coalesce(c.gl_auto_post, true) = false
  and t.journal_entry_line_id is null
  and coalesce(t.exclude_from_gl, false) = false
  and t.deposit_id is null and t.transfer_id is null
  and t.account_id is not null
  and coalesce(t.voided, false) = false
  and exists (
    select 1 from accounting.journal_entries je
    join accounting.journal_entry_lines l on l.journal_entry_id = je.id
    where je.company_id = t.company_id and je.external_source = 'buildium_gl' and je.date = t.date
      and l.account_id = t.account_id
      and ((t.type = 'credit' and l.debit = t.amount) or (t.type <> 'credit' and l.credit = t.amount))
  );
