-- Post the AR/AP sub-ledgers (invoices, payments_received, bills) to the general
-- ledger (journal_entries/_lines) so the GL-driven P&L and Balance Sheet show
-- the real accounts. This implements the long-referenced (never-built)
-- "syncBillsInvoicesToLedger".
--
-- Scope guard: only companies whose GL is "managed" by us — i.e. they have no
-- foreign/imported journal entries (external_source null or not 'acmacc_%').
-- That excludes associations with an imported GL (e.g. Bridgewater) so we never
-- double-count. Every posting is keyed by journal_entries.external_source/
-- external_id, so it is idempotent.
--
-- Postings (double-entry):
--   invoice  -> Dr Accounts Receivable / Cr income (keyword-mapped)
--   payment  -> Dr Undeposited Funds   / Cr Accounts Receivable
--   bill     -> Dr expense (bill_items) / Cr Accounts Payable
--   bill paid-> Dr Accounts Payable     / Cr operating bank

-- Is this company's GL managed by the sub-ledger postings (no imported GL)?
create or replace function accounting.gl_managed(_company_id uuid)
returns boolean language sql stable security definer set search_path to 'public','accounting' as $$
  select not exists (
    select 1 from accounting.journal_entries je
    where je.company_id = _company_id
      and (je.external_source is null or je.external_source not like 'acmacc_%')
  );
$$;

-- Find an account by code (then by any name pattern), else create it.
create or replace function accounting.coa_get_or_create(
  _company_id uuid, _match text[], _code text, _name text, _type accounting.account_type
) returns uuid
language plpgsql security definer set search_path to 'public','accounting' as $$
declare _id uuid; _m text;
begin
  if _code is not null then
    select id into _id from accounting.accounts
    where company_id=_company_id and code=_code and type=_type limit 1;
    if _id is not null then return _id; end if;
  end if;
  foreach _m in array coalesce(_match, array[]::text[]) loop
    select id into _id from accounting.accounts
    where company_id=_company_id and type=_type and name ilike _m
    order by code nulls last limit 1;
    if _id is not null then return _id; end if;
  end loop;
  insert into accounting.accounts (company_id, name, code, type, balance, is_bank, is_system)
  values (_company_id, _name, _code, _type, 0, false, false)
  returning id into _id;
  return _id;
end$$;

create or replace function accounting.coa_ar(_company_id uuid) returns uuid
language sql security definer set search_path to 'public','accounting' as $$
  select accounting.coa_get_or_create(_company_id, array['%receivable%'], '1100', 'Accounts Receivable', 'asset'::accounting.account_type);
$$;
create or replace function accounting.coa_ap(_company_id uuid) returns uuid
language sql security definer set search_path to 'public','accounting' as $$
  select accounting.coa_get_or_create(_company_id, array['%payable%'], '2000', 'Accounts Payable', 'liability'::accounting.account_type);
$$;
create or replace function accounting.coa_undeposited(_company_id uuid) returns uuid
language sql security definer set search_path to 'public','accounting' as $$
  select accounting.coa_get_or_create(_company_id, array['Undeposited Funds'], '1499', 'Undeposited Funds', 'asset'::accounting.account_type);
$$;
create or replace function accounting.coa_default_expense(_company_id uuid) returns uuid
language sql security definer set search_path to 'public','accounting' as $$
  select accounting.coa_get_or_create(_company_id, array['%administrative%','%operating%','%management%'], '5000', 'Administrative', 'expense'::accounting.account_type);
$$;
create or replace function accounting.coa_operating_bank(_company_id uuid) returns uuid
language sql stable security definer set search_path to 'public','accounting' as $$
  select id from accounting.accounts
  where company_id=_company_id and is_bank and name not ilike '%undeposited%'
  order by code nulls last limit 1;
$$;

-- Income account for an invoice, by keyword on its description.
create or replace function accounting.coa_income_for(_company_id uuid, _desc text) returns uuid
language plpgsql security definer set search_path to 'public','accounting' as $$
declare _d text := lower(coalesce(_desc,''));
begin
  if _d like '%late%' then return accounting.coa_get_or_create(_company_id, array['%late%'], '4070', 'Late Fees', 'income'); end if;
  if _d like '%admin%' then return accounting.coa_get_or_create(_company_id, array['%admin%'], '4080', 'Admin. Fees', 'income'); end if;
  if _d like '%interest%' then return accounting.coa_get_or_create(_company_id, array['%interest%'], '4060', 'Interest', 'income'); end if;
  if _d like '%violation%' or _d like '%fine%' then return accounting.coa_get_or_create(_company_id, array['%violation%','%fine%'], '4045', 'Violations', 'income'); end if;
  return accounting.coa_get_or_create(_company_id, array['%assessment%','%dues%'], '4010', 'Assessment Fees', 'income');
end$$;

-- Remove a keyed journal entry (lines first) for re-posting / cleanup.
create or replace function accounting._gl_clear(_company_id uuid, _src text, _ext text) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
begin
  delete from accounting.journal_entry_lines
  where journal_entry_id in (
    select id from accounting.journal_entries
    where company_id=_company_id and external_source=_src and external_id=_ext);
  delete from accounting.journal_entries
  where company_id=_company_id and external_source=_src and external_id=_ext;
end$$;

create or replace function accounting.post_invoice_gl(_invoice_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare i accounting.invoices%rowtype; _ar uuid; _inc uuid; _je uuid;
begin
  select * into i from accounting.invoices where id=_invoice_id;
  if not found then return; end if;
  perform accounting._gl_clear(i.company_id, 'acmacc_inv', i.id::text);
  if not accounting.gl_managed(i.company_id) then return; end if;
  if coalesce(i.total,0) = 0 or i.status = 'void' then return; end if;
  _ar := accounting.coa_ar(i.company_id);
  _inc := accounting.coa_income_for(i.company_id, coalesce(nullif(i.notes,''), i.number));
  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (i.company_id, i.issue_date, coalesce(nullif(i.notes,''), 'Invoice ' || i.number), i.number, 'acmacc_inv', i.id::text)
  returning id into _je;
  insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
    (_je, _ar,  i.total, 0, 'Invoice ' || i.number),
    (_je, _inc, 0, i.total, 'Invoice ' || i.number);
end$$;

create or replace function accounting.post_payment_gl(_payment_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare p accounting.payments_received%rowtype; _ar uuid; _cash uuid; _je uuid;
begin
  select * into p from accounting.payments_received where id=_payment_id;
  if not found then return; end if;
  perform accounting._gl_clear(p.company_id, 'acmacc_pay', p.id::text);
  if not accounting.gl_managed(p.company_id) then return; end if;
  if coalesce(p.amount,0) = 0 then return; end if;
  _ar := accounting.coa_ar(p.company_id);
  _cash := accounting.coa_undeposited(p.company_id);
  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (p.company_id, p.payment_date, coalesce(nullif(p.memo,''), 'Payment received'), p.reference, 'acmacc_pay', p.id::text)
  returning id into _je;
  insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
    (_je, _cash, p.amount, 0, 'Payment'),
    (_je, _ar,   0, p.amount, 'Payment');
end$$;

create or replace function accounting.post_bill_gl(_bill_id uuid) returns void
language plpgsql security definer set search_path to 'public','accounting' as $$
declare
  b accounting.bills%rowtype; _ap uuid; _defexp uuid; _bank uuid; _je uuid;
  _debits numeric := 0; it record;
begin
  select * into b from accounting.bills where id=_bill_id;
  if not found then return; end if;
  perform accounting._gl_clear(b.company_id, 'acmacc_bill', b.id::text);
  perform accounting._gl_clear(b.company_id, 'acmacc_billpay', b.id::text);
  if not accounting.gl_managed(b.company_id) then return; end if;
  if coalesce(b.total,0) = 0 or b.status = 'void' then return; end if;

  _ap := accounting.coa_ap(b.company_id);
  _defexp := accounting.coa_default_expense(b.company_id);

  -- Bill: Dr expense(s) / Cr A/P
  insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
  values (b.company_id, b.issue_date, coalesce(nullif(b.notes,''), 'Bill ' || b.number), b.number, 'acmacc_bill', b.id::text)
  returning id into _je;
  for it in select coalesce(account_id, _defexp) as acct, amount from accounting.bill_items where bill_id=b.id and amount <> 0 loop
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description)
    values (_je, it.acct, it.amount, 0, 'Bill ' || b.number);
    _debits := _debits + it.amount;
  end loop;
  if _debits < b.total then
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description)
    values (_je, _defexp, b.total - _debits, 0, 'Bill ' || b.number);
  end if;
  insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description)
  values (_je, _ap, 0, b.total, 'Bill ' || b.number);

  -- Paid portion: Dr A/P / Cr operating bank
  if coalesce(b.paid_amount,0) > 0 then
    _bank := coalesce(accounting.coa_operating_bank(b.company_id), accounting.coa_undeposited(b.company_id));
    insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
    values (b.company_id, coalesce(b.updated_at::date, b.issue_date), 'Bill payment ' || b.number, b.number, 'acmacc_billpay', b.id::text)
    returning id into _je;
    insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
      (_je, _ap,   b.paid_amount, 0, 'Bill payment ' || b.number),
      (_je, _bank, 0, b.paid_amount, 'Bill payment ' || b.number);
  end if;
end$$;

-- ── Triggers (error-swallowed) ───────────────────────────────────────────────
create or replace function accounting.tg_invoice_gl() returns trigger
language plpgsql security definer set search_path to 'public','accounting' as $$
begin
  begin
    if tg_op='DELETE' then perform accounting._gl_clear(old.company_id,'acmacc_inv',old.id::text); return old; end if;
    perform accounting.post_invoice_gl(new.id);
  exception when others then raise warning 'accounting: invoice GL post failed for %: %', coalesce(new.id,old.id), sqlerrm; end;
  return coalesce(new, old);
end$$;

create or replace function accounting.tg_payment_gl() returns trigger
language plpgsql security definer set search_path to 'public','accounting' as $$
begin
  begin
    if tg_op='DELETE' then perform accounting._gl_clear(old.company_id,'acmacc_pay',old.id::text); return old; end if;
    perform accounting.post_payment_gl(new.id);
  exception when others then raise warning 'accounting: payment GL post failed for %: %', coalesce(new.id,old.id), sqlerrm; end;
  return coalesce(new, old);
end$$;

create or replace function accounting.tg_bill_gl() returns trigger
language plpgsql security definer set search_path to 'public','accounting' as $$
begin
  begin
    if tg_op='DELETE' then
      perform accounting._gl_clear(old.company_id,'acmacc_bill',old.id::text);
      perform accounting._gl_clear(old.company_id,'acmacc_billpay',old.id::text);
      return old;
    end if;
    perform accounting.post_bill_gl(new.id);
  exception when others then raise warning 'accounting: bill GL post failed for %: %', coalesce(new.id,old.id), sqlerrm; end;
  return coalesce(new, old);
end$$;

drop trigger if exists trg_acct_invoice_gl on accounting.invoices;
create trigger trg_acct_invoice_gl after insert or update or delete on accounting.invoices
  for each row execute function accounting.tg_invoice_gl();

drop trigger if exists trg_acct_payment_gl on accounting.payments_received;
create trigger trg_acct_payment_gl after insert or update or delete on accounting.payments_received
  for each row execute function accounting.tg_payment_gl();

drop trigger if exists trg_acct_bill_gl on accounting.bills;
create trigger trg_acct_bill_gl after insert or update or delete on accounting.bills
  for each row execute function accounting.tg_bill_gl();
