-- Sync accounting Homeowners (accounting.customers) from the app roster
-- (public.units + public.owners), one customer per UNIT with all of that
-- unit's owners combined. Units/Owners are the source of truth for contact
-- fields; balances and ledger links (invoices, payments, transactions,
-- work orders, estimates) are always preserved.
--
-- Scope: only associations that already have an accounting.companies row
-- (i.e. have switched on platform accounting). Associations without a company
-- are ignored.

-- ---------------------------------------------------------------------------
-- Helper: normalized "street" key (first address line, lowercased, despaced).
-- Matches the differing roster vs. accounting address formats, e.g.
--   units:     "1015 Cady Circle, Titusville, FL, 32780"
--   customers: "1015 Cady Circle\nTitusville, FL 32780"
-- both -> "1015 cady circle"
-- ---------------------------------------------------------------------------
create or replace function accounting._roster_street_key(_addr text)
returns text
language sql
immutable
as $$
  select nullif(
    lower(regexp_replace(
      trim(split_part(replace(coalesce(_addr, ''), E'\n', ','), ',', 1)),
      '\s+', ' ', 'g'
    )),
    ''
  );
$$;

-- ---------------------------------------------------------------------------
-- Core: ensure exactly one accounting.customers row represents a unit.
-- Adopts the best-matching existing customer (by external link, account #, or
-- street), folds any same-unit duplicates into it (reassigning ledger rows and
-- summing balances), then refreshes its contact fields from the roster.
-- Idempotent.
-- ---------------------------------------------------------------------------
create or replace function accounting.sync_customer_for_unit(_unit_id uuid)
returns uuid
language plpgsql
security definer
set search_path to 'public', 'accounting'
as $$
declare
  _u          public.units%rowtype;
  _company_id uuid;
  _name       text;
  _email      text;
  _phone      text;
  _mailing    text;
  _movein     date;
  _prop       text;
  _acct       text;
  _street     text;
  _ids        uuid[];
  _survivor   uuid;
  _dups       uuid[];
begin
  select * into _u from public.units where id = _unit_id;
  if not found then
    return null;
  end if;

  select id into _company_id
  from accounting.companies
  where association_id = _u.association_id;
  if _company_id is null then
    return null; -- association not on platform accounting; nothing to sync
  end if;

  -- Combine all (non-tenant) owners on the unit. Primary owner first.
  select
    nullif(string_agg(nm, ' & ' order by pr, dord, cat), ''),
    (array_agg(email          order by pr, dord, cat) filter (where email is not null))[1],
    (array_agg(phone          order by pr, dord, cat) filter (where phone is not null))[1],
    (array_agg(mailing_address order by pr, dord, cat) filter (where mailing_address is not null))[1],
    (array_agg(move_in_date   order by pr, dord, cat) filter (where move_in_date is not null))[1],
    (array_agg(property_address order by pr, dord, cat) filter (where property_address is not null))[1],
    (array_agg(account_number order by pr, dord, cat) filter (where account_number is not null))[1]
  into _name, _email, _phone, _mailing, _movein, _prop, _acct
  from (
    select
      coalesce(nullif(trim(business_name), ''),
               nullif(trim(concat_ws(' ', first_name, last_name)), '')) as nm,
      email, phone, mailing_address, move_in_date, property_address, account_number,
      case when is_primary then 0 else 1 end as pr,
      coalesce(display_order, 999999)        as dord,
      created_at                             as cat
    from public.owners
    where unit_id = _unit_id
      and coalesce(is_tenant, false) = false
  ) o;

  -- Unit is the source of truth for property address / account number.
  _prop := coalesce(nullif(trim(_u.address), ''), _prop);
  _acct := coalesce(nullif(trim(_u.account_number), ''), _acct);
  if _name is null or _name = '' then
    _name := coalesce(
      nullif('Unit ' || nullif(trim(_u.unit_number), ''), 'Unit '),
      nullif(trim(_u.address), ''),
      'Unit'
    );
  end if;

  _street := accounting._roster_street_key(_prop);

  -- Candidate existing customers for THIS unit: already-linked, same account #,
  -- or same street. Prefer the already-linked row, then the earliest created.
  select array_agg(id order by pri, cat) into _ids
  from (
    select
      id,
      created_at as cat,
      case when external_source = 'acmacc_unit' and external_id = _unit_id::text
           then 0 else 1 end as pri
    from accounting.customers
    where company_id = _company_id
      and (
            (external_source = 'acmacc_unit' and external_id = _unit_id::text)
         or (unit_id = _unit_id::text)
         or (_acct   is not null and lower(trim(account_number)) = lower(trim(_acct)))
         or (_street is not null and accounting._roster_street_key(property_address) = _street)
      )
  ) s;

  if _ids is not null and array_length(_ids, 1) >= 1 then
    _survivor := _ids[1];

    -- Fold any duplicates into the survivor (preserve all financial history).
    if array_length(_ids, 1) > 1 then
      _dups := _ids[2:array_length(_ids, 1)];
      update accounting.invoices         set customer_id = _survivor where customer_id = any(_dups);
      update accounting.payments_received set customer_id = _survivor where customer_id = any(_dups);
      update accounting.transactions     set customer_id = _survivor where customer_id = any(_dups);
      update accounting.work_orders      set customer_id = _survivor where customer_id = any(_dups);
      update accounting.estimates        set customer_id = _survivor where customer_id = any(_dups);

      update accounting.customers
        set balance = (select coalesce(sum(balance), 0) from accounting.customers where id = any(_ids))
        where id = _survivor;

      delete from accounting.customers where id = any(_dups);
    end if;

    update accounting.customers set
      name             = _name,
      email            = _email,
      phone            = _phone,
      property_address = _prop,
      mailing_address  = _mailing,
      billing_address  = coalesce(_mailing, _prop),
      lot_number       = nullif(trim(_u.lot), ''),
      unit_number      = nullif(trim(_u.unit_number), ''),
      account_number   = _acct,
      move_in_date     = _movein,
      unit_id          = _unit_id::text,
      external_source  = 'acmacc_unit',
      external_id      = _unit_id::text,
      updated_at       = now()
    where id = _survivor;
  else
    insert into accounting.customers
      (company_id, name, email, phone, property_address, mailing_address, billing_address,
       lot_number, unit_number, account_number, move_in_date, unit_id,
       external_source, external_id, balance)
    values
      (_company_id, _name, _email, _phone, _prop, _mailing, coalesce(_mailing, _prop),
       nullif(trim(_u.lot), ''), nullif(trim(_u.unit_number), ''), _acct, _movein, _unit_id::text,
       'acmacc_unit', _unit_id::text, 0)
    returning id into _survivor;
  end if;

  return _survivor;
end;
$$;

-- ---------------------------------------------------------------------------
-- When a unit goes away: delete its synced customer if it carries no ledger,
-- otherwise just detach the link so financial history is never lost.
-- ---------------------------------------------------------------------------
create or replace function accounting.unlink_customer_for_unit(_unit_id uuid)
returns void
language plpgsql
security definer
set search_path to 'public', 'accounting'
as $$
declare
  _cid  uuid;
  _refs int;
begin
  for _cid in
    select id from accounting.customers
    where external_source = 'acmacc_unit' and external_id = _unit_id::text
  loop
    select count(*) into _refs from (
      select 1 from accounting.invoices          where customer_id = _cid
      union all select 1 from accounting.payments_received where customer_id = _cid
      union all select 1 from accounting.transactions     where customer_id = _cid
      union all select 1 from accounting.work_orders      where customer_id = _cid
      union all select 1 from accounting.estimates        where customer_id = _cid
    ) t;

    if _refs = 0 then
      delete from accounting.customers where id = _cid;
    else
      update accounting.customers
        set external_source = null, external_id = null, updated_at = now()
        where id = _cid;
    end if;
  end loop;
end;
$$;

-- ---------------------------------------------------------------------------
-- Trigger glue. Errors are swallowed (logged as warnings) so a sync hiccup can
-- never block a roster edit.
-- ---------------------------------------------------------------------------
create or replace function accounting.tg_sync_customer_from_unit()
returns trigger
language plpgsql
security definer
set search_path to 'public', 'accounting'
as $$
begin
  begin
    if tg_op = 'DELETE' then
      perform accounting.unlink_customer_for_unit(old.id);
      return old;
    end if;
    perform accounting.sync_customer_for_unit(new.id);
    return new;
  exception when others then
    raise warning 'accounting: unit sync failed for %: %', coalesce(new.id, old.id), sqlerrm;
    return coalesce(new, old);
  end;
end;
$$;

create or replace function accounting.tg_sync_customer_from_owner()
returns trigger
language plpgsql
security definer
set search_path to 'public', 'accounting'
as $$
begin
  begin
    if tg_op in ('INSERT', 'UPDATE') and new.unit_id is not null then
      perform accounting.sync_customer_for_unit(new.unit_id);
    end if;
    if tg_op in ('UPDATE', 'DELETE') and old.unit_id is not null
       and (tg_op = 'DELETE' or old.unit_id is distinct from new.unit_id) then
      perform accounting.sync_customer_for_unit(old.unit_id);
    end if;
  exception when others then
    raise warning 'accounting: owner sync failed: %', sqlerrm;
  end;
  return coalesce(new, old);
end;
$$;

drop trigger if exists trg_acct_sync_customer_unit on public.units;
create trigger trg_acct_sync_customer_unit
  after insert or update or delete on public.units
  for each row execute function accounting.tg_sync_customer_from_unit();

drop trigger if exists trg_acct_sync_customer_owner on public.owners;
create trigger trg_acct_sync_customer_owner
  after insert or update or delete on public.owners
  for each row execute function accounting.tg_sync_customer_from_owner();
