create extension if not exists pgcrypto;
create extension if not exists citext;

create schema if not exists app;
create schema if not exists app_private;

revoke all on schema app_private from public;
revoke all on schema app_private from anon;
revoke all on schema app_private from authenticated;

create or replace function app.touch_updated_at()
returns trigger
language plpgsql
as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

create or replace function app.jwt_text(claim_name text)
returns text
language sql
stable
as $$
  select nullif(coalesce(auth.jwt() ->> claim_name, current_setting('request.jwt.claim.' || claim_name, true)), '')
$$;

create or replace function app.current_tenant_id()
returns uuid
language sql
stable
as $$
  select app.jwt_text('tenant_id')::uuid
$$;

create or replace function app.current_role()
returns text
language sql
stable
as $$
  select coalesce(app.jwt_text('app_role'), app.jwt_text('role'), '')
$$;

create or replace function app.is_platform_admin()
returns boolean
language sql
stable
as $$
  select app.current_role() in ('platform_admin', 'service_role')
$$;

create table if not exists public.tenants (
  id uuid primary key default gen_random_uuid(),
  slug citext not null unique,
  name text not null,
  legal_name text,
  status text not null default 'active' check (status in ('draft', 'active', 'suspended', 'archived')),
  mode text not null default 'saas' check (mode in ('platform_owned', 'saas', 'dedicated')),
  billing_status text not null default 'trial' check (billing_status in ('trial', 'active', 'past_due', 'cancelled')),
  owner_user_id uuid,
  legacy_id text,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists public.platform_users (
  id uuid primary key default gen_random_uuid(),
  auth_user_id uuid unique references auth.users(id) on delete set null,
  legacy_id text unique,
  username text,
  email citext,
  phone text,
  name text,
  avatar_url text,
  primary_role text not null default 'student',
  score integer not null default 0,
  last_seen_at timestamptz,
  legacy_password_hash text,
  password_migration_required boolean not null default false,
  raw_profile jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

alter table public.tenants
  drop constraint if exists tenants_owner_user_id_fkey,
  add constraint tenants_owner_user_id_fkey
  foreign key (owner_user_id) references public.platform_users(id) on delete set null;

create table if not exists public.user_identities (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references public.platform_users(id) on delete cascade,
  provider text not null,
  provider_subject text not null,
  union_id text,
  open_id text,
  phone text,
  email citext,
  secret_payload jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (provider, provider_subject)
);

create table if not exists public.tenant_memberships (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid not null references public.platform_users(id) on delete cascade,
  role text not null check (role in ('platform_admin', 'tenant_owner', 'tenant_admin', 'tenant_operator', 'teacher', 'sales', 'agent', 'student')),
  status text not null default 'active' check (status in ('active', 'invited', 'disabled')),
  permissions jsonb not null default '{}'::jsonb,
  legacy_role text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, user_id, role)
);

create table if not exists public.tenant_domains (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  host citext not null unique,
  domain_type text not null default 'custom' check (domain_type in ('system', 'custom', 'miniapp')),
  status text not null default 'pending' check (status in ('pending', 'active', 'failed', 'disabled')),
  is_primary boolean not null default false,
  verification_token text,
  verified_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists public.tenant_branding (
  tenant_id uuid primary key references public.tenants(id) on delete cascade,
  brand_name text not null,
  short_name text,
  slogan text,
  org_name text,
  logo_url text,
  favicon_url text,
  service_wechat text,
  service_account_name text,
  theme jsonb not null default '{}'::jsonb,
  public_assets jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists public.tenant_settings (
  tenant_id uuid primary key references public.tenants(id) on delete cascade,
  feature_flags jsonb not null default '{}'::jsonb,
  admin_feature_flags jsonb not null default '{}'::jsonb,
  public_config jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists public.tenant_payment_accounts (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  provider text not null,
  mode text not null default 'platform_collect' check (mode in ('platform_collect', 'tenant_collect', 'service_provider')),
  display_name text,
  status text not null default 'disabled' check (status in ('active', 'disabled', 'pending')),
  config_public jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, provider)
);

create table if not exists app_private.tenant_secrets (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  secret_scope text not null check (secret_scope in ('payment', 'sms', 'oauth', 'storage', 'crm', 'ai', 'system')),
  secret_key text not null,
  secret_value text,
  secret_json jsonb not null default '{}'::jsonb,
  provider text,
  last_rotated_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, secret_scope, secret_key)
);

comment on table app_private.tenant_secrets is
  'Private tenant secrets. Do not expose through PostgREST/anon/authenticated roles. Prefer external vault or encrypted values in production.';

create table if not exists public.tenant_subscriptions (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  plan_code text not null,
  status text not null default 'trial' check (status in ('trial', 'active', 'past_due', 'cancelled')),
  starts_at timestamptz,
  expires_at timestamptz,
  billing_cycle text default 'yearly',
  amount_cents integer not null default 0,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists public.tenant_usage_records (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  metric_key text not null,
  metric_value numeric not null default 0,
  period_start date not null,
  period_end date not null,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now()
);

create table if not exists public.regions (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  legacy_id text,
  name text not null,
  code text,
  short_name text,
  full_name text,
  icon text,
  pinyin text,
  sort_order integer not null default 0,
  is_hot boolean not null default false,
  is_active boolean not null default true,
  config jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.region_modules (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  legacy_id text,
  name text not null,
  type text,
  icon text,
  color text,
  text_color text,
  description text,
  route text,
  sort_order integer not null default 0,
  is_primary_school_module boolean not null default false,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.module_nodes (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  module_id uuid references public.region_modules(id) on delete set null,
  parent_id uuid references public.module_nodes(id) on delete cascade,
  legacy_id text,
  legacy_parent_id text,
  legacy_module_id text,
  type text not null check (type in ('category', 'subject', 'chapter', 'paper', 'school', 'major', 'custom')),
  name text not null,
  path text,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.schools (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  module_id uuid references public.region_modules(id) on delete set null,
  legacy_id text,
  name text not null,
  professional_exam_date text,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.majors (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  school_id uuid references public.schools(id) on delete cascade,
  legacy_id text,
  name text not null,
  description text,
  study_tips text,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.subjects (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  module_id uuid references public.region_modules(id) on delete set null,
  school_id uuid references public.schools(id) on delete set null,
  major_id uuid references public.majors(id) on delete set null,
  node_id uuid references public.module_nodes(id) on delete set null,
  legacy_id text,
  name text not null,
  type text check (type in ('cultural', 'professional')),
  major_legacy_ids jsonb not null default '[]'::jsonb,
  icon text,
  description text,
  stats jsonb not null default '{}'::jsonb,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.categories (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  subject_id uuid references public.subjects(id) on delete cascade,
  node_id uuid references public.module_nodes(id) on delete set null,
  legacy_id text,
  name text not null,
  category_type text check (category_type in ('chapter', 'paper')),
  sort_order integer not null default 0,
  svip_question_limit integer,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.question_banks (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  name text not null,
  source_scope text not null default 'tenant' check (source_scope in ('platform', 'tenant')),
  status text not null default 'active' check (status in ('active', 'archived')),
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists public.questions (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  question_bank_id uuid references public.question_banks(id) on delete set null,
  subject_id uuid references public.subjects(id) on delete set null,
  category_id uuid references public.categories(id) on delete set null,
  node_id uuid references public.module_nodes(id) on delete set null,
  legacy_id text,
  legacy_subject_id text,
  legacy_category_id text,
  legacy_node_id text,
  type text not null default 'choice',
  type_label text,
  difficulty integer,
  tags jsonb not null default '[]'::jsonb,
  media_url text,
  has_video_explanation boolean not null default false,
  status text not null default 'published' check (status in ('draft', 'published', 'archived')),
  current_version_id uuid,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.question_versions (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  question_id uuid not null references public.questions(id) on delete cascade,
  version_no integer not null default 1,
  content text,
  options jsonb not null default '[]'::jsonb,
  correct_option_index integer,
  correct_option_indices jsonb not null default '[]'::jsonb,
  answer_text text,
  explanation text,
  sub_questions jsonb not null default '[]'::jsonb,
  code_lang text,
  code_template text,
  source_hash text,
  created_by uuid references public.platform_users(id) on delete set null,
  created_at timestamptz not null default now(),
  unique (question_id, version_no)
);

alter table public.questions
  drop constraint if exists questions_current_version_id_fkey,
  add constraint questions_current_version_id_fkey
  foreign key (current_version_id) references public.question_versions(id) on delete set null;

create table if not exists public.student_profiles (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid not null references public.platform_users(id) on delete cascade,
  legacy_user_id text,
  region_id uuid references public.regions(id) on delete set null,
  selected_school_id uuid references public.schools(id) on delete set null,
  selected_major_id uuid references public.majors(id) on delete set null,
  questions_answered_today integer not null default 0,
  mastered_words_count integer not null default 0,
  last_check_in_date date,
  stats jsonb not null default '{}'::jsonb,
  progress jsonb not null default '{}'::jsonb,
  module_selections jsonb not null default '{}'::jsonb,
  recent_activities jsonb not null default '[]'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, user_id)
);

create table if not exists public.practice_sessions (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid not null references public.platform_users(id) on delete cascade,
  mode text not null default 'chapter',
  target_type text,
  target_id uuid,
  started_at timestamptz not null default now(),
  finished_at timestamptz,
  metadata jsonb not null default '{}'::jsonb
);

create table if not exists public.answer_records (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid not null references public.platform_users(id) on delete cascade,
  question_id uuid references public.questions(id) on delete set null,
  question_version_id uuid references public.question_versions(id) on delete set null,
  practice_session_id uuid references public.practice_sessions(id) on delete set null,
  legacy_id text,
  legacy_question_id text,
  legacy_category_id text,
  selected_options jsonb not null default '[]'::jsonb,
  answer_text text,
  is_correct boolean,
  answered_at timestamptz not null default now(),
  created_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.favorite_questions (
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid not null references public.platform_users(id) on delete cascade,
  question_id uuid not null references public.questions(id) on delete cascade,
  source text not null default 'imported',
  created_at timestamptz not null default now(),
  primary key (tenant_id, user_id, question_id)
);

create table if not exists public.wrong_questions (
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid not null references public.platform_users(id) on delete cascade,
  question_id uuid not null references public.questions(id) on delete cascade,
  wrong_count integer not null default 1,
  last_wrong_at timestamptz not null default now(),
  resolved_at timestamptz,
  primary key (tenant_id, user_id, question_id)
);

create table if not exists public.svip_plans (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  legacy_id text,
  name text not null,
  price_cents integer not null default 0,
  original_price_cents integer,
  days integer not null default 0,
  description text,
  per_day_label text,
  badge text,
  recommended boolean not null default false,
  coupon_only boolean not null default false,
  vp_product_id text,
  vp_enabled boolean not null default false,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.orders (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid references public.platform_users(id) on delete set null,
  legacy_id text,
  legacy_user_id text,
  order_no text not null,
  status text not null default 'pending' check (status in ('pending', 'paid', 'failed', 'closed', 'refunded')),
  product_type text,
  product_name text,
  amount_cents integer not null default 0,
  pay_method text,
  pay_provider text,
  trade_no text,
  plan_id uuid references public.svip_plans(id) on delete set null,
  legacy_plan_id text,
  days integer,
  region_id uuid references public.regions(id) on delete set null,
  legacy_region_id text,
  paid_at timestamptz,
  raw_payload jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, order_no),
  unique (tenant_id, legacy_id)
);

create table if not exists public.order_items (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  order_id uuid not null references public.orders(id) on delete cascade,
  legacy_id text,
  item_type text not null,
  item_id uuid,
  name text not null,
  quantity integer not null default 1,
  unit_amount_cents integer not null default 0,
  total_amount_cents integer not null default 0,
  metadata jsonb not null default '{}'::jsonb,
  unique (tenant_id, legacy_id)
);

create table if not exists public.payments (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  order_id uuid not null references public.orders(id) on delete cascade,
  legacy_id text,
  legacy_order_id text,
  provider text not null,
  method text,
  status text not null default 'pending' check (status in ('pending', 'paid', 'failed', 'cancelled', 'refunded')),
  amount_cents integer not null,
  provider_trade_no text,
  paid_at timestamptz,
  raw_payload jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.payment_events (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  payment_id uuid references public.payments(id) on delete set null,
  provider text not null,
  event_type text not null,
  event_id text,
  signature_valid boolean,
  payload jsonb not null,
  processed_at timestamptz,
  error text,
  created_at timestamptz not null default now(),
  unique (provider, event_id)
);

create table if not exists public.entitlements (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  user_id uuid not null references public.platform_users(id) on delete cascade,
  entitlement_type text not null default 'svip',
  scope_type text not null default 'tenant' check (scope_type in ('tenant', 'region', 'module', 'subject', 'question_bank')),
  scope_id uuid,
  source_type text not null default 'migration',
  source_id uuid,
  legacy_source_id text,
  starts_at timestamptz not null default now(),
  expires_at timestamptz,
  status text not null default 'active' check (status in ('active', 'revoked', 'expired')),
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now()
);

create table if not exists public.code_batches (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  legacy_id text,
  name text not null,
  sale_type text,
  channel text,
  campaign_name text,
  default_unit_price_cents integer not null default 0,
  cost_price_cents integer not null default 0,
  total_count integer not null default 0,
  days integer,
  region_id uuid references public.regions(id) on delete set null,
  legacy_region_id text,
  issued_at timestamptz,
  created_by uuid references public.platform_users(id) on delete set null,
  remark text,
  commission_rate numeric(6,4),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.activation_codes (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  legacy_id text,
  code citext not null,
  days integer not null default 0,
  is_used boolean not null default false,
  used_by uuid references public.platform_users(id) on delete set null,
  used_at timestamptz,
  agent_user_id uuid references public.platform_users(id) on delete set null,
  batch_id uuid references public.code_batches(id) on delete set null,
  sale_type text,
  unit_price_cents integer,
  sold_to text,
  used_region_id uuid references public.regions(id) on delete set null,
  coupon_code text,
  coupon_redemption_id uuid,
  remark text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, code),
  unique (tenant_id, legacy_id)
);

create table if not exists public.coupons (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  legacy_id text,
  code citext not null,
  plan_id uuid references public.svip_plans(id) on delete set null,
  discount_type text check (discount_type in ('percent', 'fixed')),
  discount_value numeric,
  valid_from timestamptz,
  valid_to timestamptz,
  max_uses integer,
  used_count integer not null default 0,
  source text,
  remark text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, code),
  unique (tenant_id, legacy_id)
);

create table if not exists public.coupon_redemptions (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  legacy_id text,
  coupon_id uuid references public.coupons(id) on delete set null,
  coupon_code text,
  user_id uuid references public.platform_users(id) on delete set null,
  plan_id uuid references public.svip_plans(id) on delete set null,
  order_id uuid references public.orders(id) on delete set null,
  status text not null default 'claimed',
  discount_applied_cents integer,
  region_id uuid references public.regions(id) on delete set null,
  source text,
  remark text,
  claimed_at timestamptz,
  used_at timestamptz,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.vocabulary_units (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  legacy_id text,
  name text not null,
  description text,
  word_count integer,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.vocabulary_words (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  unit_id uuid references public.vocabulary_units(id) on delete set null,
  legacy_id text,
  word text not null,
  phonetic text,
  meaning text,
  example text,
  example_translation text,
  difficulty integer,
  tags jsonb not null default '[]'::jsonb,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.handbook_subjects (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  legacy_id text,
  name text not null,
  type text,
  icon text,
  color text,
  description text,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.handbook_chapters (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  subject_id uuid references public.handbook_subjects(id) on delete cascade,
  legacy_id text,
  name text not null,
  description text,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.handbook_entries (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  chapter_id uuid references public.handbook_chapters(id) on delete cascade,
  legacy_id text,
  title text not null,
  summary text,
  content text,
  tags jsonb not null default '[]'::jsonb,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.banners (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  legacy_id text,
  title text,
  subtitle text,
  content text,
  button_text text,
  button_link text,
  bg_color text,
  border_color text,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.faqs (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  region_id uuid references public.regions(id) on delete set null,
  legacy_id text,
  question text,
  answer text,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.announcements (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  legacy_id text,
  content text,
  link text,
  bg_color text,
  sort_order integer not null default 0,
  is_active boolean not null default true,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.reports (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  legacy_id text,
  question_id uuid references public.questions(id) on delete set null,
  user_id uuid references public.platform_users(id) on delete set null,
  type text,
  description text,
  status text not null default 'pending',
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id, legacy_id)
);

create table if not exists public.audit_logs (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid references public.tenants(id) on delete cascade,
  actor_user_id uuid references public.platform_users(id) on delete set null,
  action text not null,
  target_type text,
  target_id text,
  details jsonb not null default '{}'::jsonb,
  ip_address text,
  user_agent text,
  created_at timestamptz not null default now()
);

create table if not exists public.crm_config (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  enabled boolean not null default false,
  url text,
  secret_ref text,
  form_name text,
  exam_type text,
  timeout_sec integer,
  delay_sec integer,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  unique (tenant_id)
);

create table if not exists public.pb_import_runs (
  id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  source_name text not null,
  source_kind text not null default 'json',
  status text not null default 'running' check (status in ('running', 'completed', 'failed')),
  stats jsonb not null default '{}'::jsonb,
  started_at timestamptz not null default now(),
  finished_at timestamptz
);

create table if not exists public.pb_raw_records (
  run_id uuid not null references public.pb_import_runs(id) on delete cascade,
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  collection_name text not null,
  legacy_id text not null,
  record jsonb not null,
  normalized boolean not null default false,
  errors jsonb not null default '[]'::jsonb,
  imported_at timestamptz not null default now(),
  primary key (run_id, collection_name, legacy_id)
);

create table if not exists public.pb_import_issues (
  id uuid primary key default gen_random_uuid(),
  run_id uuid references public.pb_import_runs(id) on delete cascade,
  tenant_id uuid not null references public.tenants(id) on delete cascade,
  collection_name text not null,
  legacy_id text,
  severity text not null check (severity in ('info', 'warning', 'error', 'critical')),
  issue_code text not null,
  message text not null,
  field_path text,
  raw_value_sample text,
  created_at timestamptz not null default now()
);

create index if not exists idx_tenant_domains_host on public.tenant_domains(host);
create index if not exists idx_memberships_tenant_user on public.tenant_memberships(tenant_id, user_id);
create index if not exists idx_regions_tenant_active on public.regions(tenant_id, is_active, sort_order);
create index if not exists idx_module_nodes_tenant_parent on public.module_nodes(tenant_id, parent_id, sort_order);
create index if not exists idx_questions_tenant_subject on public.questions(tenant_id, subject_id);
create index if not exists idx_questions_tenant_node on public.questions(tenant_id, node_id);
create index if not exists idx_question_versions_question on public.question_versions(question_id, version_no desc);
create index if not exists idx_orders_tenant_status on public.orders(tenant_id, status, created_at desc);
create index if not exists idx_orders_tenant_user on public.orders(tenant_id, user_id, created_at desc);
create index if not exists idx_entitlements_tenant_user on public.entitlements(tenant_id, user_id, status, expires_at);
create index if not exists idx_raw_records_lookup on public.pb_raw_records(tenant_id, collection_name, legacy_id);
create index if not exists idx_import_issues_run on public.pb_import_issues(run_id, severity, collection_name);
create index if not exists idx_tenant_secrets_lookup on app_private.tenant_secrets(tenant_id, secret_scope, secret_key);

alter table public.tenants enable row level security;
alter table public.platform_users enable row level security;
alter table public.user_identities enable row level security;
alter table app_private.tenant_secrets enable row level security;

drop policy if exists platform_admin_tenants on public.tenants;
create policy platform_admin_tenants on public.tenants
  for all
  using (app.is_platform_admin())
  with check (app.is_platform_admin());

drop policy if exists tenant_member_can_read_own_tenants on public.tenants;
create policy tenant_member_can_read_own_tenants on public.tenants
  for select
  using (
    exists (
      select 1
      from public.tenant_memberships tm
      join public.platform_users pu on pu.id = tm.user_id
      where tm.tenant_id = tenants.id
        and pu.auth_user_id = auth.uid()
        and tm.status = 'active'
    )
  );

drop policy if exists platform_admin_platform_users on public.platform_users;
create policy platform_admin_platform_users on public.platform_users
  for all
  using (app.is_platform_admin() or auth_user_id = auth.uid())
  with check (app.is_platform_admin() or auth_user_id = auth.uid());

drop policy if exists platform_admin_user_identities on public.user_identities;
create policy platform_admin_user_identities on public.user_identities
  for all
  using (
    app.is_platform_admin()
    or exists (
      select 1 from public.platform_users pu
      where pu.id = user_id and pu.auth_user_id = auth.uid()
    )
  )
  with check (
    app.is_platform_admin()
    or exists (
      select 1 from public.platform_users pu
      where pu.id = user_id and pu.auth_user_id = auth.uid()
    )
  );

drop policy if exists platform_admin_tenant_secrets on app_private.tenant_secrets;
create policy platform_admin_tenant_secrets on app_private.tenant_secrets
  for all
  using (app.is_platform_admin())
  with check (app.is_platform_admin());

do $$
declare
  table_name text;
begin
  foreach table_name in array array[
    'tenant_domains', 'tenant_branding', 'tenant_settings', 'tenant_payment_accounts',
    'tenant_subscriptions', 'tenant_usage_records', 'tenant_memberships',
    'regions', 'region_modules', 'module_nodes', 'schools', 'majors', 'subjects',
    'categories', 'question_banks', 'questions', 'question_versions',
    'student_profiles', 'practice_sessions', 'answer_records', 'favorite_questions',
    'wrong_questions', 'svip_plans', 'orders', 'order_items', 'payments',
    'payment_events', 'entitlements', 'code_batches', 'activation_codes',
    'coupons', 'coupon_redemptions', 'vocabulary_units', 'vocabulary_words',
    'handbook_subjects', 'handbook_chapters', 'handbook_entries', 'banners',
    'faqs', 'announcements', 'reports', 'audit_logs', 'crm_config',
    'pb_import_runs', 'pb_raw_records', 'pb_import_issues'
  ]
  loop
    execute format('alter table public.%I enable row level security', table_name);
    execute format('drop policy if exists tenant_isolation on public.%I', table_name);
    execute format(
      'create policy tenant_isolation on public.%I for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin())',
      table_name
    );
  end loop;
end $$;

do $$
declare
  table_name text;
begin
  foreach table_name in array array[
    'tenants', 'platform_users', 'user_identities', 'tenant_domains', 'tenant_branding',
    'tenant_settings', 'tenant_payment_accounts', 'tenant_subscriptions',
    'regions', 'region_modules', 'module_nodes', 'schools', 'majors', 'subjects',
    'categories', 'question_banks', 'questions', 'student_profiles', 'svip_plans',
    'orders', 'payments', 'code_batches', 'activation_codes', 'coupons',
    'coupon_redemptions', 'vocabulary_units', 'vocabulary_words', 'handbook_subjects',
    'handbook_chapters', 'handbook_entries', 'banners', 'faqs', 'announcements',
    'reports', 'crm_config'
  ]
  loop
    execute format('drop trigger if exists set_updated_at on public.%I', table_name);
    execute format('create trigger set_updated_at before update on public.%I for each row execute function app.touch_updated_at()', table_name);
  end loop;
end $$;
