-- Recurring bills & journal entries for platform accounting.
-- A template stores a bill/journal definition + a schedule. generate_due_recurring()
-- materialises real accounting.bills / accounting.journal_entries on cadence, which
-- post to the GL through the existing bill/JE triggers. Run nightly by pg_cron and
-- on demand from the Recurring page ("Generate due now").

create table if not exists accounting.recurring_templates (
  id uuid primary key default gen_random_uuid(),
  company_id uuid not null references accounting.companies(id) on delete cascade,
  kind text not null check (kind in ('bill','journal')),
  name text not null,
  active boolean not null default true,
  frequency text not null default 'monthly' check (frequency in ('weekly','monthly','quarterly','yearly')),
  interval_count integer not null default 1 check (interval_count >= 1),
  day_of_month integer check (day_of_month between 1 and 31),
  start_date date not null default current_date,
  next_run_date date not null,
  end_date date,
  last_run_date date,
  last_generated_id uuid,
  generated_count integer not null default 0,
  -- bill: {vendor_id, due_days, tax_pct, memo, items:[{account_id,description,quantity,rate}]}
  -- journal: {description, reference, lines:[{account_id, amount, description}]}  (+amount=debit, -amount=credit)
  payload jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create index if not exists idx_recurring_templates_company on accounting.recurring_templates(company_id);
create index if not exists idx_recurring_templates_due on accounting.recurring_templates(next_run_date) where active;

alter table accounting.recurring_templates enable row level security;

create policy "Accounting staff full access" on accounting.recurring_templates
  for all using (accounting.is_accounting_staff()) with check (accounting.is_accounting_staff());
create policy "Members CRUD recurring_templates" on accounting.recurring_templates
  for all using (accounting.is_company_member(company_id, auth.uid()))
  with check (accounting.is_company_member(company_id, auth.uid()));
create policy "Board view recurring_templates" on accounting.recurring_templates
  for select using (accounting.is_company_board_member(company_id));

create trigger trg_recurring_templates_updated
  before update on accounting.recurring_templates
  for each row execute function public.update_updated_at_column();

grant select, insert, update, delete on accounting.recurring_templates to authenticated;
grant all on accounting.recurring_templates to service_role;

-- Advance a date by N periods of a frequency, optionally pinned to a day-of-month
-- (clamped to the month length).
create or replace function accounting.advance_recurrence(p_date date, p_freq text, p_n integer, p_dom integer default null)
returns date language plpgsql immutable as $$
declare d date; dim integer;
begin
  d := case p_freq
    when 'weekly'    then p_date + (7 * p_n)
    when 'quarterly' then (p_date + ((3 * p_n) || ' month')::interval)::date
    when 'yearly'    then (p_date + (p_n || ' year')::interval)::date
    else                  (p_date + (p_n || ' month')::interval)::date   -- monthly
  end;
  if p_dom is not null and p_freq in ('monthly','quarterly','yearly') then
    dim := extract(day from (date_trunc('month', d) + interval '1 month - 1 day'))::int;
    d := (date_trunc('month', d) + ((least(p_dom, dim) - 1) || ' day')::interval)::date;
  end if;
  return d;
end $$;

-- Materialise every recurrence due on/before p_as_of (catches up missed periods).
-- Returns the number of bills/journals created.
create or replace function accounting.generate_due_recurring(p_as_of date default current_date, p_company_id uuid default null)
returns integer
language plpgsql
security definer
set search_path = accounting, public
as $$
declare
  t           accounting.recurring_templates;
  v_count     int := 0;
  v_made      int;
  v_run       date;
  v_bill_id   uuid;
  v_je_id     uuid;
  v_last_id   uuid;
  v_item      jsonb;
  v_line      jsonb;
  v_sub       numeric;
  v_tax       numeric;
  v_amt       numeric;
begin
  for t in
    select * from accounting.recurring_templates
    where active and next_run_date <= p_as_of
      and (p_company_id is null or company_id = p_company_id)
    order by next_run_date
  loop
    v_run := t.next_run_date;
    v_made := 0;
    while v_run <= p_as_of and (t.end_date is null or v_run <= t.end_date) and v_made < 60 loop
      if t.kind = 'bill' then
        v_sub := 0;
        for v_item in select value from jsonb_array_elements(coalesce(t.payload->'items','[]'::jsonb)) loop
          v_sub := v_sub + round(coalesce((v_item->>'quantity')::numeric,1) * coalesce((v_item->>'rate')::numeric,0), 2);
        end loop;
        v_tax := round(v_sub * coalesce((t.payload->>'tax_pct')::numeric,0) / 100, 2);
        insert into accounting.bills (company_id, vendor_id, number, issue_date, due_date, subtotal, tax, total, status, notes)
        values (t.company_id, nullif(t.payload->>'vendor_id','')::uuid,
                'REC-'||to_char(v_run,'YYYYMMDD')||'-'||left(t.id::text,4),
                v_run,
                case when nullif(t.payload->>'due_days','') is not null then v_run + (t.payload->>'due_days')::int end,
                v_sub, v_tax, v_sub + v_tax, 'open', nullif(t.payload->>'memo',''))
        returning id into v_bill_id;
        for v_item in select value from jsonb_array_elements(coalesce(t.payload->'items','[]'::jsonb)) loop
          insert into accounting.bill_items (bill_id, description, quantity, rate, amount, account_id)
          values (v_bill_id, v_item->>'description',
                  coalesce((v_item->>'quantity')::numeric,1), coalesce((v_item->>'rate')::numeric,0),
                  round(coalesce((v_item->>'quantity')::numeric,1) * coalesce((v_item->>'rate')::numeric,0), 2),
                  nullif(v_item->>'account_id','')::uuid);
        end loop;
        v_last_id := v_bill_id;
      else
        insert into accounting.journal_entries (company_id, date, description, reference)
        values (t.company_id, v_run, coalesce(nullif(t.payload->>'description',''), t.name), nullif(t.payload->>'reference',''))
        returning id into v_je_id;
        for v_line in select value from jsonb_array_elements(coalesce(t.payload->'lines','[]'::jsonb)) loop
          v_amt := coalesce((v_line->>'amount')::numeric, 0);
          if v_amt = 0 then continue; end if;
          insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description)
          values (v_je_id, nullif(v_line->>'account_id','')::uuid,
                  case when v_amt > 0 then v_amt else 0 end,
                  case when v_amt < 0 then -v_amt else 0 end,
                  nullif(v_line->>'description',''));
        end loop;
        v_last_id := v_je_id;
      end if;

      v_count := v_count + 1;
      v_made := v_made + 1;
      t.last_run_date := v_run;
      v_run := accounting.advance_recurrence(v_run, t.frequency, t.interval_count, t.day_of_month);
    end loop;

    update accounting.recurring_templates
      set next_run_date     = v_run,
          last_run_date     = coalesce(t.last_run_date, last_run_date),
          last_generated_id = coalesce(v_last_id, last_generated_id),
          generated_count   = generated_count + v_made,
          active            = case when end_date is not null and v_run > end_date then false else active end
      where id = t.id;
  end loop;

  return v_count;
end $$;

grant execute on function accounting.advance_recurrence(date, text, integer, integer) to authenticated, service_role;
grant execute on function accounting.generate_due_recurring(date, uuid) to authenticated, service_role;

-- Nightly auto-generation (07:15 UTC). Re-schedule idempotently.
do $$
begin
  perform cron.unschedule('accounting-recurring-daily');
exception when others then null;
end $$;
select cron.schedule('accounting-recurring-daily', '15 7 * * *', $cron$ select accounting.generate_due_recurring(current_date); $cron$);
