-- Bidirectional bill sync between the main app and the Accounting platform.
--   Forward: an approved/paid public.bills row (created by the bill-approvals
--            flow) is mirrored into accounting.bills (Payables), creating the
--            accounting vendor + a single line item.
--   Back:    when an accounting.bills row is marked paid (paid_amount >= total),
--            the linked public.bills is set status='paid' (+ paid_date,
--            amount_paid) and its bill_approvals are marked paid.
-- Scoped to associations with an accounting.companies row. Keyed by
-- external_source='acmacc_bill' / 'acmacc_vendor', external_id = public row id.

-- ---------------------------------------------------------------------------
-- Find-or-create the accounting vendor mirroring a public vendor.
-- Adopts an existing same-name accounting vendor before creating a new one.
-- ---------------------------------------------------------------------------
create or replace function accounting.ensure_vendor_for_public(_company_id uuid, _public_vendor_id uuid)
returns uuid
language plpgsql security definer set search_path to 'public','accounting'
as $$
declare _vid uuid; _name text; _email text; _phone text; _addr text;
begin
  if _public_vendor_id is null then return null; end if;

  select id into _vid from accounting.vendors
  where company_id=_company_id and external_source='acmacc_vendor' and external_id=_public_vendor_id::text;
  if _vid is not null then return _vid; end if;

  select name, email, phone, address into _name, _email, _phone, _addr
  from public.vendors where id=_public_vendor_id;
  if _name is null then _name := 'Vendor'; end if;

  -- adopt an existing same-name vendor (avoid duplicates), else create
  select id into _vid from accounting.vendors
  where company_id=_company_id and lower(trim(name))=lower(trim(_name)) limit 1;
  if _vid is not null then
    update accounting.vendors
      set external_source='acmacc_vendor', external_id=_public_vendor_id::text,
          email=coalesce(email,_email), phone=coalesce(phone,_phone), address=coalesce(address,_addr),
          updated_at=now()
      where id=_vid;
    return _vid;
  end if;

  insert into accounting.vendors (company_id, name, email, phone, address, external_source, external_id)
  values (_company_id, _name, _email, _phone, _addr, 'acmacc_vendor', _public_vendor_id::text)
  returning id into _vid;
  return _vid;
end;
$$;

-- Should a public bill of this status appear in accounting Payables?
create or replace function accounting.bill_should_mirror(_status text)
returns boolean language sql immutable as $$
  select coalesce(lower(_status), '') not in
    ('draft','pending','pending_approval','rejected','void','voided','cancelled','denied');
$$;

-- ---------------------------------------------------------------------------
-- Forward: mirror one public.bills row into accounting.bills (idempotent).
-- ---------------------------------------------------------------------------
create or replace function accounting.sync_public_bill(_bill_id uuid)
returns void
language plpgsql security definer set search_path to 'public','accounting'
as $$
declare
  b public.bills%rowtype;
  _company_id uuid;
  _vendor_id uuid;
  _status accounting.bill_status;
  _paid numeric; _tot numeric;
  _acct_bill_id uuid;
begin
  select * into b from public.bills where id=_bill_id;
  if not found then return; end if;

  select id into _company_id from accounting.companies where association_id=b.association_id;
  if _company_id is null then return; end if;

  if not accounting.bill_should_mirror(b.status) then
    delete from accounting.bills where company_id=_company_id and external_source='acmacc_bill' and external_id=b.id::text;
    return;
  end if;

  _vendor_id := accounting.ensure_vendor_for_public(_company_id, b.vendor_id);
  _paid := coalesce(b.amount_paid, 0);
  _tot  := coalesce(b.amount, 0);
  _status := (case when _paid >= _tot and _tot > 0 then 'paid'
                   when _paid > 0 then 'partially_paid'
                   else 'open' end)::accounting.bill_status;

  insert into accounting.bills
    (company_id, vendor_id, number, issue_date, due_date, status, subtotal, tax, total,
     notes, paid_amount, attachment_url, external_source, external_id)
  values
    (_company_id, _vendor_id,
     coalesce(nullif(b.invoice_number,''), 'BILL-' || left(replace(b.id::text,'-',''),8)),
     b.bill_date, b.due_date, _status, _tot, 0, _tot,
     b.description, _paid, b.attachment_url, 'acmacc_bill', b.id::text)
  on conflict (company_id, external_source, external_id) where external_id is not null
  do update set vendor_id=excluded.vendor_id, number=excluded.number, issue_date=excluded.issue_date,
     due_date=excluded.due_date, status=excluded.status, subtotal=excluded.subtotal,
     total=excluded.total, notes=excluded.notes, paid_amount=excluded.paid_amount,
     attachment_url=excluded.attachment_url, updated_at=now()
  returning id into _acct_bill_id;

  if _acct_bill_id is null then
    select id into _acct_bill_id from accounting.bills
    where company_id=_company_id and external_source='acmacc_bill' and external_id=b.id::text;
  end if;

  -- Single line item mirroring the bill amount (refresh on each sync).
  delete from accounting.bill_items where bill_id=_acct_bill_id;
  insert into accounting.bill_items (bill_id, description, quantity, rate, amount)
  values (_acct_bill_id, coalesce(nullif(b.description,''), 'Bill ' || coalesce(b.invoice_number,'')), 1, _tot, _tot);
end;
$$;

-- ---------------------------------------------------------------------------
-- Back: when an accounting bill is fully paid, reflect it on the public bill.
-- ---------------------------------------------------------------------------
create or replace function accounting.sync_accounting_bill_paid(_acct_bill_id uuid)
returns void
language plpgsql security definer set search_path to 'public','accounting'
as $$
declare ab accounting.bills%rowtype; _public_id uuid;
begin
  select * into ab from accounting.bills where id=_acct_bill_id;
  if not found then return; end if;
  if ab.external_source <> 'acmacc_bill' or ab.external_id is null then return; end if;
  if not (coalesce(ab.paid_amount,0) >= coalesce(ab.total,0) and coalesce(ab.total,0) > 0) then return; end if;

  _public_id := ab.external_id::uuid;
  update public.bills
    set status='paid', amount_paid=ab.total,
        paid_date=coalesce(paid_date, current_date), updated_at=now()
    where id=_public_id and status is distinct from 'paid';

  update public.bill_approvals
    set status='paid', updated_at=now()
    where bill_id=_public_id and status is distinct from 'paid';
end;
$$;

-- ---------------------------------------------------------------------------
-- Triggers (error-swallowed).
-- ---------------------------------------------------------------------------
create or replace function accounting.tg_public_bill_sync()
returns trigger
language plpgsql security definer set search_path to 'public','accounting'
as $$
declare _company_id uuid;
begin
  begin
    if tg_op='DELETE' then
      select id into _company_id from accounting.companies where association_id=old.association_id;
      if _company_id is not null then
        delete from accounting.bills where company_id=_company_id and external_source='acmacc_bill' and external_id=old.id::text;
      end if;
      return old;
    end if;
    perform accounting.sync_public_bill(new.id);
    return new;
  exception when others then
    raise warning 'accounting: public bill sync failed for %: %', coalesce(new.id, old.id), sqlerrm;
    return coalesce(new, old);
  end;
end;
$$;

create or replace function accounting.tg_accounting_bill_paid_sync()
returns trigger
language plpgsql security definer set search_path to 'public','accounting'
as $$
begin
  begin
    -- only act when the paid position actually changed, to avoid loops
    if old.paid_amount is distinct from new.paid_amount or old.total is distinct from new.total then
      perform accounting.sync_accounting_bill_paid(new.id);
    end if;
  exception when others then
    raise warning 'accounting: accounting bill paid sync failed for %: %', new.id, sqlerrm;
  end;
  return new;
end;
$$;

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

drop trigger if exists trg_acct_bill_paid_back on accounting.bills;
create trigger trg_acct_bill_paid_back
  after update on accounting.bills
  for each row execute function accounting.tg_accounting_bill_paid_sync();
