-- 0002_path_trigger.sql
-- Maintain projax.items.path as dot-joined slug walk from root.
-- Enforce: areas at root only, projects not at root, no cycles.
-- See docs/design.md §3.1

create or replace function projax.compute_item_path(p_parent_id uuid, p_slug text, p_self_id uuid)
returns text
language plpgsql
stable
as $$
declare
  parts text[] := array[p_slug];
  cur_id uuid := p_parent_id;
  cur_slug text;
  cur_parent uuid;
  hops int := 0;
begin
  while cur_id is not null loop
    hops := hops + 1;
    if hops > 64 then
      raise exception 'projax.items: path depth exceeds 64 (cycle or pathological tree?)';
    end if;
    if p_self_id is not null and cur_id = p_self_id then
      raise exception 'projax.items: cycle detected (item % is ancestor of itself)', p_self_id
        using errcode = 'check_violation';
    end if;

    select slug, parent_id into cur_slug, cur_parent
      from projax.items
      where id = cur_id;

    if cur_slug is null then
      raise exception 'projax.items: parent % not found', cur_id;
    end if;

    parts := array_prepend(cur_slug, parts);
    cur_id := cur_parent;
  end loop;

  return array_to_string(parts, '.');
end;
$$;

create or replace function projax.items_before_write()
returns trigger
language plpgsql
as $$
begin
  -- Structural rules
  if 'area' = any(new.kind) and new.parent_id is not null then
    raise exception 'projax.items: area must have parent_id = NULL (got %)', new.parent_id
      using errcode = 'check_violation';
  end if;

  if 'project' = any(new.kind) and new.parent_id is null then
    raise exception 'projax.items: project must have a non-null parent_id'
      using errcode = 'check_violation';
  end if;

  -- Cycle check on UPDATE: a node cannot be its own ancestor
  if tg_op = 'UPDATE' and new.parent_id is not null and new.parent_id = new.id then
    raise exception 'projax.items: parent_id cannot equal id'
      using errcode = 'check_violation';
  end if;

  -- Compute path
  new.path := projax.compute_item_path(new.parent_id, new.slug,
    case when tg_op = 'UPDATE' then new.id else null end);

  return new;
end;
$$;

drop trigger if exists items_before_write on projax.items;
create trigger items_before_write
  before insert or update of slug, parent_id, kind on projax.items
  for each row execute function projax.items_before_write();

-- After update of slug or parent_id: recompute paths of all descendants.
create or replace function projax.items_after_reparent()
returns trigger
language plpgsql
as $$
begin
  if (tg_op = 'UPDATE') and (old.slug is distinct from new.slug or old.parent_id is distinct from new.parent_id) then
    -- Use recursive CTE to refresh descendant paths
    with recursive descendants as (
      select id, parent_id, slug
        from projax.items
        where parent_id = new.id
      union all
      select i.id, i.parent_id, i.slug
        from projax.items i
        join descendants d on i.parent_id = d.id
    )
    update projax.items i
      set path = projax.compute_item_path(i.parent_id, i.slug, i.id)
      from descendants d
      where i.id = d.id;
  end if;
  return null;
end;
$$;

drop trigger if exists items_after_reparent on projax.items;
create trigger items_after_reparent
  after update of slug, parent_id on projax.items
  for each row execute function projax.items_after_reparent();
