-- Provider-neutral generation foundation.
--
-- Apply after 0002_authoritative_tenancy_postgres.sql.  This migration adds
-- durable generation intent and execution records only; it does not configure
-- WAN, FLUX, external worker endpoints, or any credentials.

begin;

create table if not exists generation_requests (
  id text primary key default gen_random_uuid()::text,
  workspace_id text not null references workspaces(id) on delete restrict,
  created_by_user_id text not null references users(id) on delete restrict,
  provider text not null check (provider ~ '^[a-z][a-z0-9_-]{0,63}$'),
  model_id text not null,
  modality text not null check (modality in ('image', 'video')),
  input_asset_id text references media_assets(id) on delete restrict,
  spec jsonb not null default '{}'::jsonb,
  request_fingerprint char(64) not null check (request_fingerprint ~ '^[0-9a-f]{64}$'),
  idempotency_key text not null,
  status text not null default 'queued' check (status in (
    'queued', 'submitting', 'running', 'retrying', 'succeeded', 'failed',
    'cancel_requested', 'cancelled'
  )),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  completed_at timestamptz,
  constraint uq_generation_request_workspace_idempotency unique (workspace_id, idempotency_key)
);
create index if not exists ix_generation_requests_workspace_created
  on generation_requests(workspace_id, created_at desc);
create index if not exists ix_generation_requests_workspace_status
  on generation_requests(workspace_id, status);

create table if not exists generation_jobs (
  id text primary key default gen_random_uuid()::text,
  generation_request_id text not null unique references generation_requests(id) on delete cascade,
  workspace_id text not null references workspaces(id) on delete restrict,
  provider text not null check (provider ~ '^[a-z][a-z0-9_-]{0,63}$'),
  status text not null default 'queued' check (status in (
    'queued', 'submitting', 'running', 'retrying', 'succeeded', 'failed',
    'cancel_requested', 'cancelled'
  )),
  attempt_count integer not null default 0 check (attempt_count >= 0),
  max_attempts integer not null default 3 check (max_attempts >= 0),
  next_attempt_at timestamptz,
  external_job_id text,
  provider_metadata jsonb not null default '{}'::jsonb,
  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,
  updated_at timestamptz not null default now()
);
create index if not exists ix_generation_jobs_workspace_status
  on generation_jobs(workspace_id, status);
create index if not exists ix_generation_jobs_next_attempt
  on generation_jobs(status, next_attempt_at);
create index if not exists ix_generation_jobs_external
  on generation_jobs(provider, external_job_id) where external_job_id is not null;

create table if not exists generation_job_attempts (
  id text primary key default gen_random_uuid()::text,
  generation_job_id text not null references generation_jobs(id) on delete cascade,
  attempt_number integer not null check (attempt_number > 0),
  status text not null,
  error_code text,
  error_message text,
  provider_request_id text,
  started_at timestamptz not null default now(),
  completed_at timestamptz,
  constraint uq_generation_job_attempt_number unique (generation_job_id, attempt_number)
);
create index if not exists ix_generation_job_attempts_job
  on generation_job_attempts(generation_job_id, attempt_number);

-- Keep denormalised workspace IDs and canonical input/output references
-- internally consistent even when a trusted service role writes the rows.
create or replace function mediarouter_assert_generation_request_asset_workspace()
returns trigger language plpgsql as $$
declare asset_workspace text;
begin
  if new.input_asset_id is not null then
    select workspace_id into asset_workspace from media_assets where id = new.input_asset_id;
    if asset_workspace is null or asset_workspace is distinct from new.workspace_id then
      raise exception 'generation input asset must belong to its request workspace' using errcode = '23503';
    end if;
  end if;
  return new;
end;
$$;
drop trigger if exists mediarouter_generation_request_asset_workspace on generation_requests;
create trigger mediarouter_generation_request_asset_workspace
before insert or update of workspace_id, input_asset_id on generation_requests
for each row execute function mediarouter_assert_generation_request_asset_workspace();

create or replace function mediarouter_assert_generation_job_workspace()
returns trigger language plpgsql as $$
declare request_workspace text;
declare asset_workspace text;
begin
  select workspace_id into request_workspace from generation_requests where id = new.generation_request_id;
  if request_workspace is null or request_workspace is distinct from new.workspace_id then
    raise exception 'generation job must belong to its request workspace' using errcode = '23503';
  end if;
  if new.output_asset_id is not null then
    select workspace_id into asset_workspace from media_assets where id = new.output_asset_id;
    if asset_workspace is null or asset_workspace is distinct from new.workspace_id then
      raise exception 'generation output asset must belong to its job workspace' using errcode = '23503';
    end if;
  end if;
  return new;
end;
$$;
drop trigger if exists mediarouter_generation_job_workspace on generation_jobs;
create trigger mediarouter_generation_job_workspace
before insert or update of workspace_id, generation_request_id, output_asset_id on generation_jobs
for each row execute function mediarouter_assert_generation_job_workspace();

do $$
declare table_name text;
begin
  foreach table_name in array array['generation_requests', 'generation_jobs'] loop
    execute format('drop trigger if exists mediarouter_tenant_touch_updated_at on %I', table_name);
    execute format('create trigger mediarouter_tenant_touch_updated_at before update on %I for each row execute function mediarouter_tenant_touch_updated_at()', table_name);
  end loop;
end $$;

-- API paths set app.workspace_id and app.user_id through SecurityDatabase's
-- tenant session.  The security/service role is backend-only; these policies
-- remain defence in depth and independently verifiable with a tenant role.
alter table generation_requests enable row level security;
alter table generation_requests force row level security;
drop policy if exists generation_requests_workspace_isolation on generation_requests;
create policy generation_requests_workspace_isolation on generation_requests
  using (
    workspace_id = current_setting('app.workspace_id', true)
    and exists (
      select 1 from workspace_memberships m
      where m.workspace_id = generation_requests.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 created_by_user_id = current_setting('app.user_id', true)
    and exists (
      select 1 from workspace_memberships m
      where m.workspace_id = generation_requests.workspace_id
        and m.user_id = current_setting('app.user_id', true)
        and m.status = 'active'
    )
  );

alter table generation_jobs enable row level security;
alter table generation_jobs force row level security;
drop policy if exists generation_jobs_workspace_isolation on generation_jobs;
create policy generation_jobs_workspace_isolation on generation_jobs
  using (
    workspace_id = current_setting('app.workspace_id', true)
    and exists (
      select 1 from workspace_memberships m
      where m.workspace_id = generation_jobs.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 = generation_jobs.workspace_id
        and m.user_id = current_setting('app.user_id', true)
        and m.status = 'active'
    )
  );

alter table generation_job_attempts enable row level security;
alter table generation_job_attempts force row level security;
drop policy if exists generation_job_attempts_workspace_isolation on generation_job_attempts;
create policy generation_job_attempts_workspace_isolation on generation_job_attempts
  using (exists (
    select 1 from generation_jobs j
    where j.id = generation_job_attempts.generation_job_id
      and j.workspace_id = current_setting('app.workspace_id', true)
  ))
  with check (exists (
    select 1 from generation_jobs j
    where j.id = generation_job_attempts.generation_job_id
      and j.workspace_id = current_setting('app.workspace_id', true)
  ));

commit;
