begin;
alter table public.model_providers add column if not exists encrypted_api_key text;
do $$
declare v_constraint record;
begin
  for v_constraint in
    select conname
    from pg_constraint
    where conrelid='public.model_providers'::regclass
      and contype='c'
      and (
        pg_get_constraintdef(oid) ilike '%provider_type%'
        or pg_get_constraintdef(oid) ilike '%base_url%'
        or pg_get_constraintdef(oid) ilike '%secret_ref%'
      )
  loop
    execute format('alter table public.model_providers drop constraint %I',v_constraint.conname);
  end loop;
end $$;
alter table public.model_providers add constraint model_providers_provider_type_check check (provider_type in ('openai','openai-compatible','anthropic'));
alter table public.model_providers add constraint model_providers_base_url_check check ((provider_type in ('openai','anthropic') and base_url is null) or (provider_type='openai-compatible' and public.model_provider_base_url_is_safe(base_url)));
create or replace function public.model_provider_code(p_name text,p_provider_type text)
returns text language sql immutable set search_path='' as $$
  with slug as (
    select trim(both '-' from regexp_replace(lower(coalesce(p_name,'')),'[^a-z0-9]+','-','g')) value
  ), normalized as (
    select case
      when value='' then replace(p_provider_type,'openai-compatible','compatible')
      when value !~ '^[a-z]' then 'p-'||value
      when char_length(value)=1 then value||'-p'
      else value
    end value
    from slug
  )
  select left(value,48) from normalized
$$;
drop function if exists public.admin_save_model_provider(uuid,uuid,text,text,text,text,text,boolean,text,text);
create or replace function public.admin_save_model_provider(p_actor_user_id uuid,p_id uuid,p_name text,p_provider_type text,p_base_url text,p_encrypted_api_key text,p_enabled boolean,p_reason text,p_request_id text)
returns uuid language plpgsql security definer set search_path='' as $$
declare v_id uuid;v_code text;v_suffix integer:=1;v_existing public.model_providers%rowtype;
begin
 if not public.admin_has_permission(p_actor_user_id,'models.write') then raise exception 'admin_permission_denied' using errcode='42501'; end if;
 if char_length(btrim(coalesce(p_name,''))) not between 1 and 80 or p_provider_type not in ('openai','openai-compatible','anthropic') or char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'invalid_model_provider';end if;
 if p_provider_type='openai-compatible' and not public.model_provider_base_url_is_safe(p_base_url) then raise exception 'unsafe_model_provider_base_url';end if;
 if p_id is null then
  perform pg_advisory_xact_lock(hashtext('model_provider_code:'||public.model_provider_code(p_name,p_provider_type)));
  v_code:=public.model_provider_code(p_name,p_provider_type);
  while exists(select 1 from public.model_providers where code=v_code) loop v_suffix:=v_suffix+1;v_code:=left(public.model_provider_code(p_name,p_provider_type),55)||'-'||v_suffix;end loop;
  if p_enabled and nullif(p_encrypted_api_key,'') is null then raise exception 'model_provider_key_required';end if;
  insert into public.model_providers(code,name,provider_type,base_url,encrypted_api_key,enabled,created_by,updated_by) values(v_code,btrim(p_name),p_provider_type,case when p_provider_type='openai-compatible' then nullif(btrim(p_base_url),'') else null end,nullif(p_encrypted_api_key,''),p_enabled,p_actor_user_id,p_actor_user_id) returning id into v_id;
 else
  select * into v_existing from public.model_providers where id=p_id for update;if not found then raise exception 'model_provider_not_found';end if;
  if exists(select 1 from public.model_config_versions where provider_id=p_id and status in ('published','retired')) and (v_existing.provider_type<>p_provider_type or v_existing.base_url is distinct from case when p_provider_type='openai-compatible' then nullif(btrim(p_base_url),'') else null end) then raise exception 'model_provider_runtime_immutable';end if;
  if p_enabled and coalesce(nullif(p_encrypted_api_key,''),v_existing.encrypted_api_key) is null then raise exception 'model_provider_key_required';end if;
  update public.model_providers set name=btrim(p_name),provider_type=p_provider_type,base_url=case when p_provider_type='openai-compatible' then nullif(btrim(p_base_url),'') else null end,encrypted_api_key=coalesce(nullif(p_encrypted_api_key,''),encrypted_api_key),enabled=p_enabled,updated_by=p_actor_user_id,updated_at=clock_timestamp() where id=p_id returning id into v_id;
 end if;
 insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value,request_id,permission_used,reason) select p_actor_user_id,lower(btrim(u.email)),'admin','models.provider.save','model_provider',v_id,jsonb_build_object('providerType',p_provider_type,'enabled',p_enabled,'secretConfigured',coalesce(nullif(p_encrypted_api_key,''),(select encrypted_api_key from public.model_providers where id=v_id)) is not null),p_request_id,'models.write',btrim(p_reason) from identity.users u where u.id=p_actor_user_id on conflict do nothing;
 return v_id;
end $$;
revoke all on function public.admin_save_model_provider(uuid,uuid,text,text,text,text,boolean,text,text) from public,anon,authenticated;
grant execute on function public.admin_save_model_provider(uuid,uuid,text,text,text,text,boolean,text,text) to service_role;
do $$ begin if exists(select 1 from pg_roles where rolname='admin_runtime') then grant execute on function public.admin_save_model_provider(uuid,uuid,text,text,text,text,boolean,text,text) to admin_runtime;end if;end $$;
create or replace function public.model_version_config_hash(p_version_id uuid)
returns text language sql stable security definer set search_path=''
as $$
  select encode(public.digest(convert_to(jsonb_build_object(
    'versionId',v.id,
    'configId',v.config_id,
    'version',v.version,
    'providerId',v.provider_id,
    'label',v.label,
    'description',v.description,
    'providerModel',v.provider_model,
    'modelTier',v.model_tier,
    'creditCost',v.credit_cost,
    'contextWindow',v.context_window,
    'inputCost',v.input_cost_microusd_per_million,
    'outputCost',v.output_cost_microusd_per_million,
    'enabled',v.enabled,
    'isDefault',v.is_default,
    'fallbackModelId',v.fallback_model_id,
    'settings',v.settings,
    'providerCode',p.code,
    'providerType',p.provider_type,
    'providerBaseUrl',p.base_url,
    'providerCredentialHash',case when p.encrypted_api_key is null then null else encode(public.digest(convert_to(p.encrypted_api_key,'utf8'),'sha256'),'hex') end,
    'providerEnabled',p.enabled
  )::text,'utf8'),'sha256'),'hex')
  from public.model_config_versions v
  join public.model_providers p on p.id=v.provider_id
  where v.id=p_version_id
$$;

drop trigger if exists model_providers_prevent_published_runtime_mutation on public.model_providers;
drop function if exists public.prevent_published_model_provider_runtime_mutation();
create function public.prevent_published_model_provider_runtime_mutation()
returns trigger language plpgsql set search_path=''
as $$
begin
  if (new.code,new.provider_type,new.base_url,new.enabled)
      is distinct from
     (old.code,old.provider_type,old.base_url,old.enabled)
    and exists(
      select 1 from public.model_config_versions
      where provider_id=old.id and status in ('published','retired')
    )
  then
    raise exception 'model_provider_runtime_immutable' using
      errcode='23514',
      hint='Create a new provider and model config version, then test and publish it.';
  end if;
  return new;
end $$;
alter table public.model_providers drop column if exists secret_ref;
drop function if exists public.expected_model_provider_secret_ref(text,text);
create trigger model_providers_prevent_published_runtime_mutation
before update of code,provider_type,base_url,enabled on public.model_providers
for each row execute function public.prevent_published_model_provider_runtime_mutation();

create table if not exists public.model_connection_test_evidence (
  id uuid primary key default gen_random_uuid(),
  version_id uuid not null references public.model_config_versions(id) on delete cascade,
  provider_id uuid not null references public.model_providers(id) on delete restrict,
  config_hash text not null check (config_hash ~ '^[0-9a-f]{64}$'),
  http_status integer not null check (http_status between 0 and 599),
  actor_user_id uuid not null references auth.users(id) on delete restrict,
  request_id text not null check (char_length(btrim(request_id)) between 1 and 200),
  tested_at timestamptz not null,
  expires_at timestamptz not null,
  created_at timestamptz not null default clock_timestamp(),
  check (expires_at > tested_at),
  unique(actor_user_id,request_id,version_id)
);
create index if not exists model_connection_test_evidence_lookup_idx
  on public.model_connection_test_evidence(version_id,tested_at desc);


create or replace function public.admin_publish_model(
  p_actor_user_id uuid,p_version_id uuid,p_reason text,p_request_id text
)
returns uuid language plpgsql security definer set search_path=''
as $$
declare v_draft public.model_config_versions%rowtype; v_model_id text; v_from uuid; v_cycle boolean;
begin
  if not public.admin_has_permission(p_actor_user_id,'models.publish') then raise exception 'admin_permission_denied' using errcode='42501'; end if;
  if char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'admin_reason_required' using errcode='22023'; end if;
  select * into v_draft from public.model_config_versions where id=p_version_id and status='draft' for update;
  if not found then raise exception 'model_draft_not_found' using errcode='22023'; end if;
  if v_draft.is_default and not v_draft.enabled then raise exception 'default_model_disabled' using errcode='23514'; end if;
  if public.model_settings_contain_secrets(v_draft.settings) then raise exception 'model_settings_secret_forbidden' using errcode='23514'; end if;
  perform 1 from public.model_providers p where p.id=v_draft.provider_id and p.enabled
    and p.encrypted_api_key is not null
    and (p.provider_type in ('openai','anthropic') or public.model_provider_base_url_is_safe(p.base_url))
    for update;
  if not found then raise exception 'model_provider_unavailable' using errcode='23514'; end if;
  if not public.model_connection_test_is_fresh(p_version_id,null) then
    raise exception 'model_connection_test_required' using errcode='23514';
  end if;
  select model_id into v_model_id from public.model_configs where id=v_draft.config_id;
  if v_draft.fallback_model_id=v_model_id then raise exception 'model_fallback_cycle' using errcode='23514'; end if;
  if v_draft.fallback_model_id is not null and not exists(
    select 1 from public.model_configs c
    join public.model_config_versions v on v.config_id=c.id
    join public.model_providers p on p.id=v.provider_id
    where c.model_id=v_draft.fallback_model_id and v.status='published' and v.enabled and p.enabled
      and p.encrypted_api_key is not null
      and (p.provider_type in ('openai','anthropic') or public.model_provider_base_url_is_safe(p.base_url))
  ) then raise exception 'fallback_model_unavailable' using errcode='23514'; end if;
  with recursive edges(model_id,fallback_model_id) as (
    select c.model_id,case when v.id=p_version_id then v_draft.fallback_model_id else v.fallback_model_id end
    from public.model_configs c join public.model_config_versions v on v.config_id=c.id
    where (v.status='published' and v.config_id<>v_draft.config_id) or v.id=p_version_id
  ), walk(origin,node,path,cycle) as (
    select model_id,fallback_model_id,array[model_id],false from edges where fallback_model_id is not null
    union all
    select w.origin,e.fallback_model_id,w.path||e.model_id,e.model_id=any(w.path)
    from walk w join edges e on e.model_id=w.node where not w.cycle and e.fallback_model_id is not null
  ) select coalesce(bool_or(cycle),false) into v_cycle from walk;
  if v_cycle then raise exception 'model_fallback_cycle' using errcode='23514'; end if;
  select id into v_from from public.model_config_versions where config_id=v_draft.config_id and status='published' for update;
  if v_from is not null and not v_draft.is_default and exists(
    select 1 from public.model_config_versions where id=v_from and is_default
  ) then
    if not v_draft.enabled then raise exception 'default_model_disabled' using errcode='23514'; end if;
    update public.model_config_versions set is_default=true where id=p_version_id;
    v_draft.is_default:=true;
  end if;
  if v_draft.is_default then update public.model_config_versions set is_default=false where status='published' and is_default; end if;
  update public.model_config_versions set status='retired',is_default=false,retired_at=clock_timestamp() where id=v_from;
  update public.model_config_versions set status='published',published_at=clock_timestamp(),retired_at=null where id=p_version_id;
  if not exists(
    select 1 from public.model_config_versions v join public.model_providers p on p.id=v.provider_id
    where v.status='published' and v.enabled and v.is_default and p.enabled
      and p.encrypted_api_key is not null
      and (p.provider_type in ('openai','anthropic') or public.model_provider_base_url_is_safe(p.base_url))
  ) then raise exception 'default_model_required' using errcode='23514'; end if;
  insert into public.model_publish_events(config_id,from_version_id,to_version_id,action,actor_user_id,reason,request_id)
    values(v_draft.config_id,v_from,p_version_id,'publish',p_actor_user_id,btrim(p_reason),p_request_id) on conflict do nothing;
  insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value,
    request_id,permission_used,reason)
  select p_actor_user_id,lower(btrim(u.email)),'admin','models.publish','model_config_version',p_version_id,
    jsonb_build_object('modelId',v_model_id,'version',v_draft.version),p_request_id,'models.publish',btrim(p_reason)
    from identity.users u where u.id=p_actor_user_id on conflict do nothing;
  return p_version_id;
end $$;


create or replace function public.admin_rollback_model(
  p_actor_user_id uuid,p_config_id uuid,p_target_version integer,p_reason text,p_request_id text
)
returns uuid language plpgsql security definer set search_path=''
as $$
declare v_current public.model_config_versions%rowtype; v_target public.model_config_versions%rowtype;
begin
  if not public.admin_has_permission(p_actor_user_id,'models.rollback') then raise exception 'admin_permission_denied' using errcode='42501'; end if;
  if char_length(btrim(coalesce(p_reason,''))) not between 1 and 500 then raise exception 'admin_reason_required' using errcode='22023'; end if;
  select * into v_current from public.model_config_versions where config_id=p_config_id and status='published' for update;
  select * into v_target from public.model_config_versions where config_id=p_config_id and version=p_target_version and status='retired' for update;
  if v_current.id is null or v_target.id is null then raise exception 'rollback_version_not_found' using errcode='22023'; end if;
  if not v_target.enabled then raise exception 'rollback_model_disabled' using errcode='23514'; end if;
  if public.model_settings_contain_secrets(v_target.settings) then raise exception 'model_settings_secret_forbidden' using errcode='23514'; end if;
  if not exists(
    select 1 from public.model_providers p where p.id=v_target.provider_id and p.enabled
      and p.encrypted_api_key is not null
      and (p.provider_type in ('openai','anthropic') or public.model_provider_base_url_is_safe(p.base_url))
  ) then raise exception 'model_provider_unavailable' using errcode='23514'; end if;
  if not public.model_connection_test_is_fresh(v_target.id,p_request_id) then
    raise exception 'model_connection_test_required' using errcode='23514';
  end if;
  v_target.is_default:=v_current.is_default;
  if v_target.is_default then update public.model_config_versions set is_default=false where status='published' and is_default; end if;
  update public.model_config_versions set status='retired',is_default=false,retired_at=clock_timestamp() where id=v_current.id;
  update public.model_config_versions set status='published',is_default=v_target.is_default,published_at=clock_timestamp(),retired_at=null where id=v_target.id;
  if not exists(
    select 1 from public.model_config_versions v join public.model_providers p on p.id=v.provider_id
    where v.status='published' and v.enabled and v.is_default and p.enabled
      and p.encrypted_api_key is not null
      and (p.provider_type in ('openai','anthropic') or public.model_provider_base_url_is_safe(p.base_url))
  ) then raise exception 'default_model_required' using errcode='23514'; end if;
  insert into public.model_publish_events(config_id,from_version_id,to_version_id,action,actor_user_id,reason,request_id)
    values(p_config_id,v_current.id,v_target.id,'rollback',p_actor_user_id,btrim(p_reason),p_request_id) on conflict do nothing;
  insert into audit.admin_audit_logs(actor_user_id,actor_email,actor_role,action,target_type,target_id,after_value,
    request_id,permission_used,reason)
  select p_actor_user_id,lower(btrim(u.email)),'admin','models.rollback','model_config_version',v_target.id,
    jsonb_build_object('version',p_target_version),p_request_id,'models.rollback',btrim(p_reason)
    from identity.users u where u.id=p_actor_user_id on conflict do nothing;
  return v_target.id;
end $$;


commit;
