-- Content Studio Phase 2: authoritative editor state and project render jobs.
-- Apply after projects/0001_projects_foundation.sql, projects/0002_project_resources.sql,
-- and security generation migrations.  Production migration remains explicit.

begin;

create table if not exists project_editor_states (
  id text primary key default gen_random_uuid()::text,
  workspace_id text not null references workspaces(id) on delete restrict,
  project_id text not null references projects(id) on delete restrict,
  revision integer not null default 1 check (revision > 0),
  schema_version integer not null check (schema_version = 1),
  state jsonb not null check (jsonb_typeof(state) = 'object'),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  updated_by text not null references users(id) on delete restrict,
  constraint uq_project_editor_state_project unique (project_id)
);
create index if not exists ix_project_editor_states_workspace_project
  on project_editor_states(workspace_id, project_id);

create table if not exists project_render_jobs (
  id text primary key default gen_random_uuid()::text,
  workspace_id text not null references workspaces(id) on delete restrict,
  project_id text not null references projects(id) on delete restrict,
  editor_revision integer not null check (editor_revision > 0),
  editor_schema_version integer not null check (editor_schema_version = 1),
  editor_state jsonb not null check (jsonb_typeof(editor_state) = 'object'),
  render_settings jsonb not null check (jsonb_typeof(render_settings) = 'object'),
  request_fingerprint text not null check (char_length(request_fingerprint) = 64),
  idempotency_key text not null check (char_length(idempotency_key) between 1 and 255),
  requested_by text not null references users(id) on delete restrict,
  status text not null default 'queued'
    check (status in ('queued', 'processing', 'completed', 'failed', 'cancelling', 'cancelled')),
  attempt_count integer not null default 0 check (attempt_count >= 0),
  max_attempts integer not null default 3 check (max_attempts between 1 and 10),
  next_attempt_at timestamptz,
  output_asset_id text references media_assets(id) on delete restrict,
  error_code text,
  error_message text,
  created_at timestamptz not null default now(),
  started_at timestamptz,
  completed_at timestamptz,
  cancelled_at timestamptz,
  updated_at timestamptz not null default now(),
  constraint uq_project_render_idempotency unique (project_id, editor_revision, idempotency_key)
);
create index if not exists ix_project_render_jobs_workspace_status
  on project_render_jobs(workspace_id, status);
create index if not exists ix_project_render_jobs_project_created
  on project_render_jobs(project_id, created_at desc);
create index if not exists ix_project_render_jobs_dispatch
  on project_render_jobs(status, next_attempt_at);

drop trigger if exists mediarouter_project_editor_touch_updated_at on project_editor_states;
create trigger mediarouter_project_editor_touch_updated_at
before update on project_editor_states
for each row execute function mediarouter_tenant_touch_updated_at();
drop trigger if exists mediarouter_project_render_touch_updated_at on project_render_jobs;
create trigger mediarouter_project_render_touch_updated_at
before update on project_render_jobs
for each row execute function mediarouter_tenant_touch_updated_at();

create or replace function mediarouter_assert_project_editor_ownership()
returns trigger language plpgsql as $$
declare project_workspace text;
declare project_status text;
begin
  if tg_op = 'INSERT' and new.revision <> 1 then
    raise exception 'initial editor revision must be one' using errcode = '23514';
  end if;
  if tg_op = 'UPDATE' and (
    new.workspace_id is distinct from old.workspace_id
    or new.project_id is distinct from old.project_id
    or new.created_at is distinct from old.created_at
  ) then
    raise exception 'project editor ownership fields are immutable' using errcode = '23514';
  end if;
  if tg_op = 'UPDATE' and new.revision <> old.revision + 1 then
    raise exception 'editor revision must increment exactly once' using errcode = '23514';
  end if;
  if new.state->>'projectId' is distinct from new.project_id
     or new.state->>'schemaVersion' is distinct from new.schema_version::text then
    raise exception 'editor state identity does not match its row' using errcode = '23514';
  end if;
  select workspace_id, status into project_workspace, project_status from projects where id = new.project_id;
  if project_workspace is null or project_workspace is distinct from new.workspace_id then
    raise exception 'editor state must belong to its project workspace' using errcode = '23503';
  end if;
  if project_status is distinct from 'active' then
    raise exception 'archived projects cannot change editor state' using errcode = '23514';
  end if;
  if not exists (
    select 1 from workspace_memberships m
    where m.workspace_id = new.workspace_id and m.user_id = new.updated_by and m.status = 'active'
  ) then
    raise exception 'editor state update requires active workspace membership' using errcode = '23503';
  end if;
  return new;
end;
$$;
drop trigger if exists mediarouter_project_editor_ownership on project_editor_states;
create trigger mediarouter_project_editor_ownership
before insert or update on project_editor_states
for each row execute function mediarouter_assert_project_editor_ownership();

create or replace function mediarouter_assert_project_render_ownership()
returns trigger language plpgsql as $$
declare project_workspace text;
declare project_status text;
declare output_workspace text;
declare output_project text;
begin
  if tg_op = 'UPDATE' and (
    new.workspace_id is distinct from old.workspace_id
    or new.project_id is distinct from old.project_id
    or new.editor_revision is distinct from old.editor_revision
    or new.editor_schema_version is distinct from old.editor_schema_version
    or new.editor_state is distinct from old.editor_state
    or new.render_settings is distinct from old.render_settings
    or new.request_fingerprint is distinct from old.request_fingerprint
    or new.idempotency_key is distinct from old.idempotency_key
    or new.requested_by is distinct from old.requested_by
    or new.max_attempts is distinct from old.max_attempts
    or new.created_at is distinct from old.created_at
  ) then
    raise exception 'render identity fields are immutable' using errcode = '23514';
  end if;
  if new.editor_state->>'projectId' is distinct from new.project_id
     or new.editor_state->>'schemaVersion' is distinct from new.editor_schema_version::text then
    raise exception 'render editor snapshot identity does not match its row' using errcode = '23514';
  end if;
  if tg_op = 'UPDATE' and new.status is distinct from old.status and not (
    (old.status = 'queued' and new.status in ('processing', 'cancelled', 'failed'))
    or (old.status = 'processing' and new.status in ('queued', 'completed', 'failed', 'cancelling', 'cancelled'))
    or (old.status = 'cancelling' and new.status in ('completed', 'failed', 'cancelled'))
  ) then
    raise exception 'invalid render status transition' using errcode = '23514';
  end if;
  select workspace_id, status into project_workspace, project_status from projects where id = new.project_id;
  if project_workspace is null or project_workspace is distinct from new.workspace_id then
    raise exception 'render must belong to its project workspace' using errcode = '23503';
  end if;
  if tg_op = 'INSERT' and project_status is distinct from 'active' then
    raise exception 'archived projects cannot be rendered' using errcode = '23514';
  end if;
  if new.output_asset_id is not null then
    select workspace_id, project_id into output_workspace, output_project
      from media_assets where id = new.output_asset_id;
    if output_workspace is null
       or output_workspace is distinct from new.workspace_id
       or output_project is distinct from new.project_id then
      raise exception 'render output must belong to its project workspace' using errcode = '23503';
    end if;
  end if;
  if new.status = 'completed' and (new.output_asset_id is null or new.completed_at is null) then
    raise exception 'completed render requires an output asset and timestamp' using errcode = '23514';
  end if;
  if new.status = 'cancelled' and (new.cancelled_at is null or new.completed_at is null) then
    raise exception 'cancelled render requires cancellation timestamps' using errcode = '23514';
  end if;
  if new.status = 'failed' and new.completed_at is null then
    raise exception 'failed render requires a completion timestamp' using errcode = '23514';
  end if;
  if new.status in ('queued', 'processing', 'cancelling') and new.output_asset_id is not null then
    raise exception 'active render cannot expose an output asset' using errcode = '23514';
  end if;
  if tg_op = 'INSERT' and not exists (
    select 1 from workspace_memberships m
    where m.workspace_id = new.workspace_id and m.user_id = new.requested_by and m.status = 'active'
  ) then
    raise exception 'render requires active workspace membership' using errcode = '23503';
  end if;
  return new;
end;
$$;
drop trigger if exists mediarouter_project_render_ownership on project_render_jobs;
create trigger mediarouter_project_render_ownership
before insert or update on project_render_jobs
for each row execute function mediarouter_assert_project_render_ownership();

alter table project_editor_states enable row level security;
alter table project_editor_states force row level security;
drop policy if exists project_editor_states_select on project_editor_states;
create policy project_editor_states_select on project_editor_states for select using (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (select 1 from workspace_memberships m where m.workspace_id = project_editor_states.workspace_id
    and m.user_id = current_setting('app.user_id', true) and m.status = 'active')
);
drop policy if exists project_editor_states_insert on project_editor_states;
create policy project_editor_states_insert on project_editor_states for insert with check (
  workspace_id = current_setting('app.workspace_id', true)
  and updated_by = current_setting('app.user_id', true)
  and exists (select 1 from workspace_memberships m where m.workspace_id = project_editor_states.workspace_id
    and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer')
);
drop policy if exists project_editor_states_update on project_editor_states;
create policy project_editor_states_update on project_editor_states for update using (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (select 1 from workspace_memberships m where m.workspace_id = project_editor_states.workspace_id
    and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer')
) with check (
  workspace_id = current_setting('app.workspace_id', true)
  and updated_by = current_setting('app.user_id', true)
  and exists (select 1 from workspace_memberships m where m.workspace_id = project_editor_states.workspace_id
    and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer')
);

alter table project_render_jobs enable row level security;
alter table project_render_jobs force row level security;
drop policy if exists project_render_jobs_select on project_render_jobs;
create policy project_render_jobs_select on project_render_jobs for select using (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (select 1 from workspace_memberships m where m.workspace_id = project_render_jobs.workspace_id
    and m.user_id = current_setting('app.user_id', true) and m.status = 'active')
);
drop policy if exists project_render_jobs_insert on project_render_jobs;
create policy project_render_jobs_insert on project_render_jobs for insert with check (
  workspace_id = current_setting('app.workspace_id', true)
  and requested_by = current_setting('app.user_id', true)
  and exists (select 1 from workspace_memberships m where m.workspace_id = project_render_jobs.workspace_id
    and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer')
);
drop policy if exists project_render_jobs_update on project_render_jobs;
create policy project_render_jobs_update on project_render_jobs for update using (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (select 1 from workspace_memberships m where m.workspace_id = project_render_jobs.workspace_id
    and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer')
) with check (
  workspace_id = current_setting('app.workspace_id', true)
  and exists (select 1 from workspace_memberships m where m.workspace_id = project_render_jobs.workspace_id
    and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer')
);

commit;
