begin;

alter table public.admin_users
  add column if not exists updated_at timestamptz not null default now();

create table if not exists public.admin_roles (
  id uuid primary key default gen_random_uuid(),
  code text not null unique check (code ~ '^[a-z][a-z0-9_]{1,39}$'),
  name text not null check (char_length(name) between 1 and 80),
  description text not null default '' check (char_length(description) <= 500),
  requires_mfa boolean not null default false,
  system_role boolean not null default true,
  created_at timestamptz not null default now()
);

create table if not exists public.admin_permissions (
  id uuid primary key default gen_random_uuid(),
  permission_key text not null unique check (permission_key ~ '^[a-z][a-z0-9_.]{2,79}$'),
  description text not null default '' check (char_length(description) <= 500)
);

create table if not exists public.admin_user_roles (
  admin_user_id uuid not null references public.admin_users(user_id) on delete cascade,
  role_id uuid not null references public.admin_roles(id) on delete restrict,
  assigned_by uuid not null references auth.users(id) on delete restrict,
  assigned_at timestamptz not null default now(),
  primary key (admin_user_id, role_id)
);

create table if not exists public.admin_role_permissions (
  role_id uuid not null references public.admin_roles(id) on delete cascade,
  permission_id uuid not null references public.admin_permissions(id) on delete cascade,
  primary key (role_id, permission_id)
);

create table if not exists public.admin_session_revocations (
  id uuid primary key default gen_random_uuid(),
  admin_user_id uuid not null references public.admin_users(user_id) on delete cascade,
  revoked_by uuid not null references auth.users(id) on delete restrict,
  reason text not null check (char_length(btrim(reason)) between 1 and 500),
  revoked_at timestamptz not null default clock_timestamp()
);

create index if not exists admin_session_revocations_user_idx
  on public.admin_session_revocations (admin_user_id, revoked_at desc);

insert into public.admin_permissions (permission_key, description) values
  ('admin.access', '进入管理后台'),
  ('admin.customers.read', '查看客户基本资料'),
  ('admin.customers.birth_data.read', '查看客户出生资料'),
  ('admin.users.read', '查看管理员'),
  ('admin.users.manage_roles', '管理管理员角色'),
  ('billing.products.read', '查看商品'),
  ('billing.products.write', '编辑商品草稿'),
  ('billing.products.publish', '发布商品版本'),
  ('billing.orders.read', '查看订单'),
  ('billing.adjustments.write', '调整订阅和补偿'),
  ('models.read', '查看模型配置'),
  ('models.write', '编辑模型草稿'),
  ('models.test', '测试模型连接'),
  ('models.publish', '发布模型配置'),
  ('models.rollback', '回滚模型配置'),
  ('ops.flags.write', '修改功能开关'),
  ('audit.read', '查看审计日志')
on conflict (permission_key) do update set description = excluded.description;

insert into public.admin_roles (code, name, description, requires_mfa) values
  ('owner', 'Owner', '管理员、角色、模型、账务、配置和审计', true),
  ('model_admin', 'Model Admin', '模型草稿、测试、发布和回滚', true),
  ('billing_admin', 'Billing Admin', '商品、订单、补偿和订阅调整', true),
  ('operations', 'Operations', '监控、熔断、切流和任务重试', true),
  ('support', 'Support', '用户查询和有限补偿', false),
  ('auditor', 'Auditor', '只读账本、配置版本和审计', false)
on conflict (code) do update set
  name = excluded.name,
  description = excluded.description,
  requires_mfa = excluded.requires_mfa;

with role_grants(role_code, permission_key) as (values
  ('owner', 'admin.access'), ('owner', 'admin.customers.read'), ('owner', 'admin.customers.birth_data.read'),
  ('owner', 'admin.users.read'), ('owner', 'admin.users.manage_roles'),
  ('owner', 'billing.products.read'), ('owner', 'billing.products.write'), ('owner', 'billing.products.publish'),
  ('owner', 'billing.orders.read'), ('owner', 'billing.adjustments.write'),
  ('owner', 'models.read'), ('owner', 'models.write'), ('owner', 'models.test'),
  ('owner', 'models.publish'), ('owner', 'models.rollback'), ('owner', 'ops.flags.write'), ('owner', 'audit.read'),
  ('model_admin', 'admin.access'), ('model_admin', 'models.read'), ('model_admin', 'models.write'),
  ('model_admin', 'models.test'), ('model_admin', 'models.publish'), ('model_admin', 'models.rollback'),
  ('billing_admin', 'admin.access'), ('billing_admin', 'admin.customers.read'), ('billing_admin', 'billing.products.read'),
  ('billing_admin', 'billing.products.write'), ('billing_admin', 'billing.products.publish'),
  ('billing_admin', 'billing.orders.read'), ('billing_admin', 'billing.adjustments.write'),
  ('operations', 'admin.access'), ('operations', 'admin.customers.read'), ('operations', 'billing.orders.read'), ('operations', 'models.read'),
  ('operations', 'ops.flags.write'), ('operations', 'audit.read'),
  ('support', 'admin.access'), ('support', 'admin.customers.read'), ('support', 'billing.orders.read'),
  ('auditor', 'admin.access'), ('auditor', 'admin.customers.read'), ('auditor', 'billing.products.read'), ('auditor', 'billing.orders.read'),
  ('auditor', 'models.read'), ('auditor', 'audit.read')
)
insert into public.admin_role_permissions (role_id, permission_id)
select r.id, p.id
from role_grants g
join public.admin_roles r on r.code = g.role_code
join public.admin_permissions p on p.permission_key = g.permission_key
on conflict do nothing;

-- Existing self-hosted administrators are the one-time bootstrap source. Runtime
-- authorization below uses only public.admin_users + RBAC, never ADMIN_EMAILS.
insert into public.admin_users (user_id, created_by)
select u.id, u.id
from identity.users u
where 'admin' = any(regexp_split_to_array(u.role, '\\s*,\\s*'))
  and exists (select 1 from auth.users a where a.id = u.id)
on conflict on constraint admin_users_pkey do update set revoked_at = null, revoked_by = null, updated_at = now();

insert into public.admin_user_roles (admin_user_id, role_id, assigned_by)
select a.user_id, r.id, a.user_id
from public.admin_users a
cross join public.admin_roles r
where a.revoked_at is null and r.code = 'owner'
  and not exists (select 1 from public.admin_user_roles existing where existing.admin_user_id = a.user_id)
on conflict do nothing;

create or replace function public.admin_has_permission(p_user_id uuid, p_permission_key text)
returns boolean
language sql
stable
security definer
set search_path = ''
as $$
  select exists (
    select 1
    from public.admin_users au
    join public.admin_user_roles aur on aur.admin_user_id = au.user_id
    join public.admin_role_permissions arp on arp.role_id = aur.role_id
    join public.admin_permissions ap on ap.id = arp.permission_id
    where au.user_id = p_user_id
      and au.revoked_at is null
      and ap.permission_key = p_permission_key
  );
$$;

create or replace function public.admin_permission_keys(p_user_id uuid)
returns table (permission_key text, role_code text, requires_mfa boolean)
language sql
stable
security definer
set search_path = ''
as $$
  select distinct ap.permission_key, ar.code, ar.requires_mfa
  from public.admin_users au
  join public.admin_user_roles aur on aur.admin_user_id = au.user_id
  join public.admin_roles ar on ar.id = aur.role_id
  join public.admin_role_permissions arp on arp.role_id = ar.id
  join public.admin_permissions ap on ap.id = arp.permission_id
  where au.user_id = p_user_id and au.revoked_at is null
  order by ar.code, ap.permission_key;
$$;

create or replace function public.assert_active_admin_owner_exists()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_removes_active_owner boolean := false;
begin
  if tg_table_name = 'admin_users' then
    v_removes_active_owner := old.revoked_at is null
      and (tg_op = 'DELETE' or new.revoked_at is not null)
      and exists (
        select 1
        from public.admin_user_roles aur
        join public.admin_roles ar on ar.id = aur.role_id
        where aur.admin_user_id = old.user_id and ar.code = 'owner'
      );
  elsif tg_table_name = 'admin_user_roles' then
    v_removes_active_owner := exists (
      select 1 from public.admin_roles ar where ar.id = old.role_id and ar.code = 'owner'
    ) and (
      tg_op = 'DELETE'
      or new.admin_user_id is distinct from old.admin_user_id
      or new.role_id is distinct from old.role_id
    );
  elsif tg_table_name = 'admin_roles' then
    v_removes_active_owner := old.code = 'owner'
      and (tg_op = 'DELETE' or new.code is distinct from old.code);
  end if;

  if v_removes_active_owner then
    perform pg_catalog.pg_advisory_xact_lock(1096040772, 1);
    if not exists (
      select 1
      from public.admin_users au
      join public.admin_user_roles aur on aur.admin_user_id = au.user_id
      join public.admin_roles ar on ar.id = aur.role_id
      where au.revoked_at is null and ar.code = 'owner'
    ) then
      raise exception 'last_owner_protected' using errcode = '23514';
    end if;
  end if;

  if tg_op = 'DELETE' then
    return old;
  end if;
  return new;
end;
$$;

revoke all on function public.assert_active_admin_owner_exists()
  from public, anon, authenticated, service_role;

drop trigger if exists admin_users_require_active_owner on public.admin_users;
create constraint trigger admin_users_require_active_owner
  after update of revoked_at or delete on public.admin_users
  deferrable initially immediate
  for each row execute function public.assert_active_admin_owner_exists();

drop trigger if exists admin_user_roles_require_active_owner on public.admin_user_roles;
create constraint trigger admin_user_roles_require_active_owner
  after update of admin_user_id, role_id or delete on public.admin_user_roles
  deferrable initially immediate
  for each row execute function public.assert_active_admin_owner_exists();

drop trigger if exists admin_roles_require_active_owner on public.admin_roles;
create constraint trigger admin_roles_require_active_owner
  after update of code or delete on public.admin_roles
  deferrable initially immediate
  for each row execute function public.assert_active_admin_owner_exists();

alter table audit.admin_audit_logs
  drop constraint if exists admin_audit_logs_action_check,
  drop constraint if exists admin_audit_logs_target_type_check,
  drop constraint if exists admin_audit_logs_actor_role_check;

alter table audit.admin_audit_logs
  add column if not exists permission_used text,
  add column if not exists reason text;

create or replace function public.admin_manage_role(
  p_actor_user_id uuid,
  p_target_user_id uuid,
  p_role_code text,
  p_assign boolean,
  p_reason text,
  p_request_id text
)
returns table (user_id uuid, role_code text, assigned boolean)
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_role_id uuid;
  v_actor_email text;
  v_target_email text;
  v_owner_count integer;
begin
  if not public.admin_has_permission(p_actor_user_id, 'admin.users.manage_roles') 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 id into v_role_id from public.admin_roles where code = p_role_code;
  if v_role_id is null then raise exception 'admin_role_not_found' using errcode = '22023'; end if;
  select lower(btrim(email)) into v_actor_email from identity.users where id = p_actor_user_id;
  select lower(btrim(email)) into v_target_email from identity.users where id = p_target_user_id;
  if v_target_email is null then raise exception 'admin_user_not_found' using errcode = '22023'; end if;

  insert into public.admin_users (user_id, created_by)
  values (p_target_user_id, p_actor_user_id)
  on conflict on constraint admin_users_pkey do update set revoked_at = null, revoked_by = null, updated_at = now();

  if p_assign then
    insert into public.admin_user_roles (admin_user_id, role_id, assigned_by)
    values (p_target_user_id, v_role_id, p_actor_user_id)
    on conflict do nothing;
  else
    if p_role_code = 'owner' then
      -- Fixed transaction-level lock serializes every last-Owner decision.
      perform pg_catalog.pg_advisory_xact_lock(1096040772, 1);
      select count(*) into v_owner_count
      from public.admin_users au
      join public.admin_user_roles aur on aur.admin_user_id = au.user_id
      join public.admin_roles ar on ar.id = aur.role_id
      where au.revoked_at is null and ar.code = 'owner';
      if v_owner_count <= 1 and exists (
        select 1 from public.admin_user_roles where admin_user_id = p_target_user_id and role_id = v_role_id
      ) then
        raise exception 'last_owner_protected' using errcode = '23514';
      end if;
    end if;
    delete from public.admin_user_roles where admin_user_id = p_target_user_id and role_id = v_role_id;
    delete from identity.sessions where user_id = p_target_user_id;
    insert into public.admin_session_revocations (admin_user_id, revoked_by, reason)
    values (p_target_user_id, p_actor_user_id, btrim(p_reason));
  end if;

  insert into audit.admin_audit_logs (
    actor_user_id, actor_email, actor_role, action, target_type, target_id,
    before_value, after_value, request_id, permission_used, reason
  ) values (
    p_actor_user_id, v_actor_email, 'admin',
    case when p_assign then 'admin.role.assign' else 'admin.role.revoke' end,
    'administrator', p_target_user_id,
    null, jsonb_build_object('email', v_target_email, 'role', p_role_code, 'assigned', p_assign),
    p_request_id, 'admin.users.manage_roles', btrim(p_reason)
  ) on conflict do nothing;

  return query select p_target_user_id, p_role_code, p_assign;
end;
$$;

create or replace function public.admin_read_customer_birth_data(
  p_actor_user_id uuid,
  p_target_user_ids uuid[],
  p_request_id text
)
returns table (
  user_id uuid,
  birth_date date,
  birth_time_status text,
  birth_place_label text
)
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_actor_email text;
begin
  if not public.admin_has_permission(p_actor_user_id, 'admin.customers.birth_data.read') then
    raise exception 'admin_permission_denied' using errcode = '42501';
  end if;
  if p_target_user_ids is null or cardinality(p_target_user_ids) <> 1 then
    raise exception 'admin_customer_scope_invalid' using errcode = '22023';
  end if;
  if char_length(btrim(coalesce(p_request_id, ''))) not between 1 and 200 then
    raise exception 'admin_request_id_invalid' using errcode = '22023';
  end if;

  select lower(btrim(email)) into v_actor_email
  from identity.users
  where id = p_actor_user_id;
  if v_actor_email is null then
    raise exception 'admin_user_not_found' using errcode = '22023';
  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, v_actor_email, 'admin', 'admin.customer.birth_data.read',
    'customer', p.id,
    jsonb_build_object('fields', array['birth_date', 'birth_time_status', 'birth_place_label']),
    btrim(p_request_id), 'admin.customers.birth_data.read', 'sensitive customer birth data view'
  from public.profiles p
  where p.id = any(p_target_user_ids)
  on conflict do nothing;

  return query
  select p.id, p.birth_date, p.birth_time_status, p.birth_place_label
  from public.profiles p
  where p.id = any(p_target_user_ids);
end;
$$;

revoke all on table public.admin_roles, public.admin_permissions, public.admin_user_roles,
  public.admin_role_permissions, public.admin_session_revocations from public, anon, authenticated;
grant select on table public.admin_roles, public.admin_permissions, public.admin_user_roles,
  public.admin_role_permissions, public.admin_session_revocations to service_role;
grant select, insert, update on table public.admin_users to service_role;

revoke all on function public.admin_has_permission(uuid, text),
  public.admin_permission_keys(uuid), public.admin_manage_role(uuid, uuid, text, boolean, text, text),
  public.admin_read_customer_birth_data(uuid, uuid[], text)
  from public, anon, authenticated;
grant execute on function public.admin_has_permission(uuid, text),
  public.admin_permission_keys(uuid), public.admin_manage_role(uuid, uuid, text, boolean, text, text),
  public.admin_read_customer_birth_data(uuid, uuid[], text)
  to service_role;

do $$
begin
  if exists (select 1 from pg_roles where rolname = 'admin_runtime') then
    if pg_catalog.pg_has_role('admin_runtime', 'service_role', 'MEMBER') then
      raise exception 'admin_runtime_service_role_membership_must_be_revoked_by_bootstrap';
    end if;
    revoke select (birth_date, birth_time_status, birth_place_label)
      on table public.profiles from admin_runtime;
    grant select on table public.admin_users, public.admin_roles, public.admin_permissions,
      public.admin_user_roles, public.admin_role_permissions, public.admin_session_revocations
      to admin_runtime;
    grant execute on function public.admin_has_permission(uuid, text),
      public.admin_permission_keys(uuid), public.admin_manage_role(uuid, uuid, text, boolean, text, text),
      public.admin_read_customer_birth_data(uuid, uuid[], text)
      to admin_runtime;
  end if;
end;
$$;

commit;
