-- Authoritative workspace-owned Project foundation.
-- Apply after app/security/migrations/0002_authoritative_tenancy_postgres.sql.
-- IDs remain UUID values stored as text to match MediaRouter's existing
-- tenancy and canonical-asset foreign-key types.

begin;
create extension if not exists pgcrypto;

create table if not exists projects (
  id text primary key default gen_random_uuid()::text,
  workspace_id text not null references workspaces(id) on delete restrict,
  created_by text not null references users(id) on delete restrict,
  name text not null check (char_length(name) between 1 and 200),
  description text check (description is null or char_length(description) <= 4000),
  status text not null default 'active' check (status in ('active', 'archived')),
  thumbnail_asset_id text references media_assets(id) on delete set null,
  metadata jsonb not null default '{}'::jsonb
    check (jsonb_typeof(metadata) = 'object'),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  archived_at timestamptz,
  constraint ck_projects_archive_timestamp check (
    (status = 'active' and archived_at is null)
    or (status = 'archived' and archived_at is not null)
  )
);
create index if not exists ix_projects_workspace on projects(workspace_id);
create index if not exists ix_projects_workspace_status on projects(workspace_id, status);
create index if not exists ix_projects_workspace_updated on projects(workspace_id, updated_at desc);
create index if not exists ix_projects_created_by on projects(created_by);

-- The shared AuditService owns this generic event table. It is deliberately
-- not project-specific so future domains can reuse the same safe boundary.
create table if not exists audit_events (
  id text primary key default gen_random_uuid()::text,
  workspace_id text not null references workspaces(id) on delete restrict,
  actor_user_id text references users(id) on delete set null,
  api_key_id text references api_keys(id) on delete set null,
  event_type text not null check (char_length(event_type) between 1 and 100),
  entity_type text not null check (char_length(entity_type) between 1 and 64),
  entity_id text not null,
  request_id text,
  metadata jsonb not null default '{}'::jsonb check (jsonb_typeof(metadata) = 'object'),
  created_at timestamptz not null default now()
);
create index if not exists ix_audit_events_workspace_created
  on audit_events(workspace_id, created_at desc);
create index if not exists ix_audit_events_type on audit_events(event_type);
create index if not exists ix_audit_events_entity on audit_events(entity_type, entity_id);

drop trigger if exists mediarouter_tenant_touch_updated_at on projects;
create trigger mediarouter_tenant_touch_updated_at
before update on projects
for each row execute function mediarouter_tenant_touch_updated_at();

create or replace function mediarouter_assert_project_ownership()
returns trigger language plpgsql as $$
declare asset_workspace text;
begin
  if tg_op = 'UPDATE' then
    if new.workspace_id is distinct from old.workspace_id
       or new.created_by is distinct from old.created_by then
      raise exception 'project ownership fields are immutable' using errcode = '23514';
    end if;
  end if;
  if new.thumbnail_asset_id is not null then
    select workspace_id into asset_workspace from media_assets
      where id = new.thumbnail_asset_id;
    if asset_workspace is null or asset_workspace is distinct from new.workspace_id then
      raise exception 'project thumbnail must belong to its workspace' using errcode = '23503';
    end if;
  end if;
  return new;
end;
$$;
drop trigger if exists mediarouter_project_ownership on projects;
create trigger mediarouter_project_ownership
before insert or update of workspace_id, created_by, thumbnail_asset_id on projects
for each row execute function mediarouter_assert_project_ownership();

alter table projects enable row level security;
alter table projects force row level security;

drop policy if exists projects_select on projects;
create policy projects_select on projects for select using (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (
    select 1 from workspace_memberships m
    where m.workspace_id = projects.workspace_id
      and m.user_id = current_setting('app.user_id', true)
      and m.status = 'active'
  )
);

drop policy if exists projects_insert on projects;
create policy projects_insert on projects for insert with check (
  workspace_id = current_setting('app.workspace_id', true)
  and created_by = current_setting('app.user_id', true)
  and exists (
    select 1 from workspace_memberships m
    where m.workspace_id = projects.workspace_id
      and m.user_id = current_setting('app.user_id', true)
      and m.status = 'active'
  )
);

drop policy if exists projects_update on projects;
create policy projects_update on projects for update using (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (
    select 1 from workspace_memberships m
    where m.workspace_id = projects.workspace_id
      and m.user_id = current_setting('app.user_id', true)
      and m.status = 'active'
  )
) with check (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (
    select 1 from workspace_memberships m
    where m.workspace_id = projects.workspace_id
      and m.user_id = current_setting('app.user_id', true)
      and m.status = 'active'
  )
);

drop policy if exists projects_delete on projects;
create policy projects_delete on projects for delete using (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (
    select 1 from workspace_memberships m
    where m.workspace_id = projects.workspace_id
      and m.user_id = current_setting('app.user_id', true)
      and m.status = 'active'
  )
);

alter table audit_events enable row level security;
alter table audit_events force row level security;
drop policy if exists audit_events_select on audit_events;
create policy audit_events_select on audit_events for select using (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (
    select 1 from workspace_memberships m
    where m.workspace_id = audit_events.workspace_id
      and m.user_id = current_setting('app.user_id', true)
      and m.status = 'active'
  )
);
drop policy if exists audit_events_insert on audit_events;
create policy audit_events_insert on audit_events for insert with check (
  workspace_id = current_setting('app.workspace_id', true)
  and actor_user_id = current_setting('app.user_id', true)
  and exists (
    select 1 from workspace_memberships m
    where m.workspace_id = audit_events.workspace_id
      and m.user_id = current_setting('app.user_id', true)
      and m.status = 'active'
  )
);

commit;

