-- =========================================
-- PostgreSQL schema initialization script
-- Safe to re-run (幂等)
-- =========================================

SET statement_timeout = 0;
SET lock_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET row_security = off;
SET search_path = public;

-- 必要扩展
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;

-- =========================================
-- Function: set_updated_at()
-- =========================================
DROP FUNCTION IF EXISTS set_updated_at() CASCADE;

CREATE FUNCTION set_updated_at() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    NEW.updated_at := now();
    RETURN NEW;
END;
$$;

-- =========================================
-- Tables
-- =========================================

CREATE TABLE IF NOT EXISTS users (
    username TEXT NOT NULL,
    password TEXT NOT NULL,
    email TEXT,
    level INTEGER DEFAULT 20 NOT NULL,
    role TEXT DEFAULT 'user' NOT NULL,
    groups JSONB DEFAULT '[]'::jsonb NOT NULL,
    permissions JSONB DEFAULT '[]'::jsonb NOT NULL,
    created_at TIMESTAMPTZ DEFAULT now(),
    uuid UUID DEFAULT gen_random_uuid() NOT NULL,
    mfa_totp_secret TEXT,
    mfa_enabled BOOLEAN DEFAULT false NOT NULL,
    mfa_secret_issued_at TIMESTAMPTZ,
    mfa_confirmed_at TIMESTAMPTZ,
    updated_at TIMESTAMPTZ DEFAULT now(),
    email_verified_at TIMESTAMPTZ,
    email_verified BOOLEAN GENERATED ALWAYS AS ((email_verified_at IS NOT NULL)) STORED,
    CONSTRAINT users_pkey PRIMARY KEY (uuid),
    CONSTRAINT users_username_uk UNIQUE (username),
    CONSTRAINT users_email_uk UNIQUE (email)
);

CREATE UNIQUE INDEX IF NOT EXISTS users_username_lower_uk ON users (lower(username));
CREATE UNIQUE INDEX IF NOT EXISTS users_email_lower_uk ON users (lower(email)) WHERE email IS NOT NULL;

CREATE TABLE IF NOT EXISTS identities (
    provider TEXT NOT NULL,
    external_id TEXT NOT NULL,
    uuid UUID DEFAULT gen_random_uuid() NOT NULL,
    user_uuid UUID NOT NULL,
    CONSTRAINT identities_pkey PRIMARY KEY (uuid),
    CONSTRAINT identities_provider_external_id_uk UNIQUE (provider, external_id),
    CONSTRAINT identities_user_fk FOREIGN KEY (user_uuid)
        REFERENCES users(uuid) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS sessions (
    token TEXT NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    uuid UUID DEFAULT gen_random_uuid() NOT NULL,
    user_uuid UUID NOT NULL,
    CONSTRAINT sessions_pkey PRIMARY KEY (uuid),
    CONSTRAINT sessions_user_fk FOREIGN KEY (user_uuid)
        REFERENCES users(uuid) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS admin_settings (
    id BIGSERIAL PRIMARY KEY,
    module_key TEXT NOT NULL,
    role TEXT NOT NULL,
    enabled BOOLEAN NOT NULL DEFAULT FALSE,
    version BIGINT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT admin_settings_module_role_uk UNIQUE (module_key, role)
);

-- =========================================
-- Indexes
-- =========================================
CREATE INDEX IF NOT EXISTS idx_identities_provider ON identities (provider);
CREATE INDEX IF NOT EXISTS idx_identities_user_uuid ON identities (user_uuid);
CREATE INDEX IF NOT EXISTS idx_sessions_user_uuid ON sessions (user_uuid);
CREATE INDEX IF NOT EXISTS idx_admin_settings_version ON admin_settings (version);

-- =========================================
-- Trigger
-- =========================================
DROP TRIGGER IF EXISTS trg_users_set_updated_at ON users;
CREATE TRIGGER trg_users_set_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();

DROP TRIGGER IF EXISTS trg_admin_settings_set_updated_at ON admin_settings;
CREATE TRIGGER trg_admin_settings_set_updated_at
BEFORE UPDATE ON admin_settings
FOR EACH ROW EXECUTE FUNCTION set_updated_at();

-- =========================================
-- End of schema.sql
-- =========================================
