CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TABLE users (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    username varchar(80) NOT NULL,
    password_hash text NOT NULL,
    role varchar(16) NOT NULL,
    active boolean NOT NULL DEFAULT true,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT users_username_format CHECK (username = lower(username) AND username ~ '^[a-z0-9][a-z0-9._-]{2,79}$'),
    CONSTRAINT users_role_check CHECK (role IN ('ADMIN', 'STAFF')),
    CONSTRAINT users_username_unique UNIQUE (username)
);

CREATE TABLE projects (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    code varchar(24) NOT NULL,
    name varchar(120) NOT NULL,
    status varchar(16) NOT NULL DEFAULT 'RUNNING',
    timezone varchar(64) NOT NULL DEFAULT 'Asia/Shanghai',
    ticket_prefix varchar(8) NOT NULL DEFAULT 'A',
    call_batch_size integer NOT NULL DEFAULT 5,
    grace_period_minutes integer NOT NULL DEFAULT 5,
    eta_mode varchar(24) NOT NULL DEFAULT 'FIXED_BATCH',
    average_batch_interval_seconds integer NOT NULL DEFAULT 600,
    continuous_rate_per_minute numeric(10,2) NOT NULL DEFAULT 2.0,
    eta_buffer_minutes integer NOT NULL DEFAULT 5,
    display_token_hash char(64),
    device_simulation_mode varchar(16) NOT NULL DEFAULT 'SUCCESS',
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT projects_code_format CHECK (code = upper(code) AND code ~ '^[A-Z0-9][A-Z0-9_-]{1,23}$'),
    CONSTRAINT projects_code_unique UNIQUE (code),
    CONSTRAINT projects_status_check CHECK (status IN ('NOT_OPEN', 'RUNNING', 'PAUSED', 'ENDED')),
    CONSTRAINT projects_ticket_prefix_check CHECK (ticket_prefix ~ '^[A-Z0-9]{1,8}$'),
    CONSTRAINT projects_batch_size_check CHECK (call_batch_size BETWEEN 1 AND 100),
    CONSTRAINT projects_grace_check CHECK (grace_period_minutes BETWEEN 0 AND 120),
    CONSTRAINT projects_eta_mode_check CHECK (eta_mode IN ('FIXED_BATCH', 'CONTINUOUS')),
    CONSTRAINT projects_batch_interval_check CHECK (average_batch_interval_seconds BETWEEN 1 AND 86400),
    CONSTRAINT projects_rate_check CHECK (continuous_rate_per_minute > 0 AND continuous_rate_per_minute <= 10000),
    CONSTRAINT projects_eta_buffer_check CHECK (eta_buffer_minutes BETWEEN 0 AND 1440),
    CONSTRAINT projects_display_token_hash_check CHECK (display_token_hash IS NULL OR display_token_hash ~ '^[0-9a-f]{64}$'),
    CONSTRAINT projects_display_token_hash_unique UNIQUE (display_token_hash),
    CONSTRAINT projects_device_mode_check CHECK (device_simulation_mode IN ('DISABLED', 'SUCCESS', 'FAILURE'))
);

CREATE TABLE user_projects (
    user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    created_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (user_id, project_id)
);

CREATE TABLE auth_sessions (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash char(64) NOT NULL,
    expires_at timestamptz NOT NULL,
    last_seen_at timestamptz NOT NULL DEFAULT now(),
    revoked_at timestamptz,
    created_ip inet,
    user_agent varchar(512) NOT NULL DEFAULT '',
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT auth_sessions_token_hash_check CHECK (token_hash ~ '^[0-9a-f]{64}$'),
    CONSTRAINT auth_sessions_token_hash_unique UNIQUE (token_hash)
);

CREATE INDEX auth_sessions_user_active_idx
    ON auth_sessions (user_id, expires_at)
    WHERE revoked_at IS NULL;

CREATE TABLE queue_sessions (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
    business_date date NOT NULL,
    status varchar(16) NOT NULL DEFAULT 'RUNNING',
    next_ticket_number integer NOT NULL DEFAULT 1,
    revision bigint NOT NULL DEFAULT 0,
    opened_at timestamptz NOT NULL DEFAULT now(),
    closed_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT queue_sessions_status_check CHECK (status IN ('RUNNING', 'PAUSED', 'ENDED')),
    CONSTRAINT queue_sessions_next_ticket_check CHECK (next_ticket_number > 0),
    CONSTRAINT queue_sessions_revision_check CHECK (revision >= 0),
    CONSTRAINT queue_sessions_project_date_unique UNIQUE (project_id, business_date),
    CONSTRAINT queue_sessions_id_project_unique UNIQUE (id, project_id)
);

CREATE TABLE queue_tickets (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
    queue_session_id uuid NOT NULL,
    ticket_number integer NOT NULL,
    display_number varchar(24) NOT NULL,
    public_token_hash char(64) NOT NULL,
    phone_ciphertext bytea NOT NULL,
    phone_nonce bytea NOT NULL,
    phone_hmac char(64) NOT NULL,
    last_name_ciphertext bytea,
    last_name_nonce bytea,
    honorific varchar(16) NOT NULL DEFAULT '游客',
    status varchar(16) NOT NULL DEFAULT 'WAITING',
    joined_at timestamptz NOT NULL DEFAULT now(),
    called_at timestamptz,
    arrived_at timestamptz,
    completed_at timestamptz,
    missed_at timestamptz,
    personal_data_purge_at timestamptz NOT NULL DEFAULT (now() + interval '30 days'),
    created_by uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT queue_tickets_session_project_fk FOREIGN KEY (queue_session_id, project_id)
        REFERENCES queue_sessions(id, project_id) ON DELETE RESTRICT,
    CONSTRAINT queue_tickets_number_check CHECK (ticket_number > 0),
    CONSTRAINT queue_tickets_public_token_hash_check CHECK (public_token_hash ~ '^[0-9a-f]{64}$'),
    CONSTRAINT queue_tickets_phone_nonce_check CHECK (octet_length(phone_nonce) = 12),
    CONSTRAINT queue_tickets_phone_hmac_check CHECK (phone_hmac ~ '^[0-9a-f]{64}$'),
    CONSTRAINT queue_tickets_last_name_pair_check CHECK (
        (last_name_ciphertext IS NULL AND last_name_nonce IS NULL)
        OR (last_name_ciphertext IS NOT NULL AND octet_length(last_name_nonce) = 12)
    ),
    CONSTRAINT queue_tickets_honorific_check CHECK (honorific IN ('游客', '先生', '女士')),
    CONSTRAINT queue_tickets_status_check CHECK (status IN ('WAITING', 'CALLED', 'ARRIVED', 'COMPLETED', 'MISSED', 'CANCELED')),
    CONSTRAINT queue_tickets_project_display_unique UNIQUE (project_id, queue_session_id, display_number),
    CONSTRAINT queue_tickets_project_number_unique UNIQUE (project_id, queue_session_id, ticket_number),
    CONSTRAINT queue_tickets_public_token_unique UNIQUE (public_token_hash),
    CONSTRAINT queue_tickets_id_project_unique UNIQUE (id, project_id)
);

CREATE INDEX queue_tickets_fifo_idx
    ON queue_tickets (project_id, queue_session_id, ticket_number)
    WHERE status = 'WAITING';

CREATE INDEX queue_tickets_active_phone_idx
    ON queue_tickets (project_id, phone_hmac, joined_at DESC)
    WHERE status IN ('WAITING', 'CALLED', 'ARRIVED');

CREATE TABLE call_batches (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
    queue_session_id uuid NOT NULL,
    batch_sequence integer NOT NULL,
    revision bigint NOT NULL,
    status varchar(16) NOT NULL DEFAULT 'CALLED',
    requested_by uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    called_at timestamptz NOT NULL DEFAULT now(),
    completed_at timestamptz,
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT call_batches_session_project_fk FOREIGN KEY (queue_session_id, project_id)
        REFERENCES queue_sessions(id, project_id) ON DELETE RESTRICT,
    CONSTRAINT call_batches_sequence_check CHECK (batch_sequence > 0),
    CONSTRAINT call_batches_revision_check CHECK (revision > 0),
    CONSTRAINT call_batches_status_check CHECK (status IN ('CALLED', 'COMPLETED')),
    CONSTRAINT call_batches_session_sequence_unique UNIQUE (queue_session_id, batch_sequence),
    CONSTRAINT call_batches_session_revision_unique UNIQUE (queue_session_id, revision),
    CONSTRAINT call_batches_id_project_unique UNIQUE (id, project_id)
);

CREATE TABLE call_batch_tickets (
    call_batch_id uuid NOT NULL,
    ticket_id uuid NOT NULL,
    project_id uuid NOT NULL,
    position integer NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (call_batch_id, ticket_id),
    CONSTRAINT call_batch_tickets_batch_project_fk FOREIGN KEY (call_batch_id, project_id)
        REFERENCES call_batches(id, project_id) ON DELETE CASCADE,
    CONSTRAINT call_batch_tickets_ticket_project_fk FOREIGN KEY (ticket_id, project_id)
        REFERENCES queue_tickets(id, project_id) ON DELETE RESTRICT,
    CONSTRAINT call_batch_tickets_position_check CHECK (position > 0),
    CONSTRAINT call_batch_tickets_ticket_once_unique UNIQUE (ticket_id),
    CONSTRAINT call_batch_tickets_position_unique UNIQUE (call_batch_id, position)
);

CREATE TABLE audit_entries (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id uuid REFERENCES projects(id) ON DELETE RESTRICT,
    actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
    action varchar(64) NOT NULL,
    entity_type varchar(64) NOT NULL,
    entity_id uuid,
    details jsonb NOT NULL DEFAULT '{}'::jsonb,
    request_id varchar(80) NOT NULL DEFAULT '',
    ip_address inet,
    user_agent varchar(512) NOT NULL DEFAULT '',
    retain_until timestamptz NOT NULL DEFAULT (now() + interval '1 year'),
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX audit_entries_project_created_idx
    ON audit_entries (project_id, created_at DESC);

CREATE INDEX audit_entries_actor_created_idx
    ON audit_entries (actor_user_id, created_at DESC);

CREATE TABLE idempotency_keys (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    scope varchar(40) NOT NULL,
    key varchar(128) NOT NULL,
    request_hash char(64) NOT NULL,
    response_code integer NOT NULL,
    response_body jsonb NOT NULL,
    resource_id uuid,
    expires_at timestamptz NOT NULL DEFAULT (now() + interval '24 hours'),
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT idempotency_request_hash_check CHECK (request_hash ~ '^[0-9a-f]{64}$'),
    CONSTRAINT idempotency_response_code_check CHECK (response_code BETWEEN 200 AND 599),
    CONSTRAINT idempotency_scope_key_unique UNIQUE (project_id, user_id, scope, key)
);

CREATE INDEX idempotency_keys_expiry_idx ON idempotency_keys (expires_at);

CREATE TABLE device_simulations (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
    call_batch_id uuid NOT NULL,
    adapter varchar(40) NOT NULL DEFAULT 'SIMULATOR',
    outcome varchar(16) NOT NULL,
    detail varchar(500) NOT NULL DEFAULT '',
    attempted_at timestamptz NOT NULL DEFAULT now(),
    completed_at timestamptz NOT NULL DEFAULT now(),
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT device_simulations_batch_project_fk FOREIGN KEY (call_batch_id, project_id)
        REFERENCES call_batches(id, project_id) ON DELETE CASCADE,
    CONSTRAINT device_simulations_outcome_check CHECK (outcome IN ('SUCCESS', 'FAILURE', 'SKIPPED')),
    CONSTRAINT device_simulations_batch_unique UNIQUE (call_batch_id)
);

CREATE INDEX device_simulations_project_created_idx
    ON device_simulations (project_id, created_at DESC);
