-- Phase 1 persistence foundation. UUIDv7 is provided by PostgreSQL 18.

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    username TEXT NOT NULL,
    username_key TEXT NOT NULL,
    display_name TEXT NOT NULL,
    password_hash TEXT NOT NULL,
    root_node_id UUID,
    setup_completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    password_changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT users_username_not_blank CHECK (btrim(username) <> ''),
    CONSTRAINT users_username_key_not_blank CHECK (btrim(username_key) <> ''),
    CONSTRAINT users_display_name_not_blank CHECK (btrim(display_name) <> ''),
    CONSTRAINT users_username_key_unique UNIQUE (username_key)
);

CREATE UNIQUE INDEX users_single_owner ON users ((TRUE));

CREATE TABLE totp_credentials (
    user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
    encrypted_secret BYTEA NOT NULL,
    enabled_at TIMESTAMPTZ,
    verified_step BIGINT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT totp_secret_not_empty CHECK (octet_length(encrypted_secret) > 0),
    CONSTRAINT totp_verified_step_nonnegative CHECK (verified_step IS NULL OR verified_step >= 0)
);

CREATE TABLE recovery_codes (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    code_hash BYTEA NOT NULL,
    used_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT recovery_codes_hash_not_empty CHECK (octet_length(code_hash) > 0),
    CONSTRAINT recovery_codes_hash_unique UNIQUE (user_id, code_hash)
);

CREATE TABLE client_instances (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    display_name TEXT NOT NULL,
    client_type TEXT NOT NULL,
    platform TEXT,
    software_version TEXT,
    protocol_version TEXT,
    last_seen_at TIMESTAMPTZ,
    revoked_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT client_instances_display_name_not_blank CHECK (btrim(display_name) <> ''),
    CONSTRAINT client_instances_type_not_blank CHECK (btrim(client_type) <> '')
);

CREATE INDEX client_instances_owner_active_idx
    ON client_instances (owner_id, created_at DESC)
    WHERE revoked_at IS NULL;

CREATE TABLE sessions (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_instance_id UUID REFERENCES client_instances(id) ON DELETE SET NULL,
    token_hash BYTEA NOT NULL UNIQUE,
    csrf_token_hash BYTEA NOT NULL,
    remote_address INET,
    user_agent TEXT,
    authenticated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at TIMESTAMPTZ NOT NULL,
    revoked_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT sessions_token_hash_not_empty CHECK (octet_length(token_hash) > 0),
    CONSTRAINT sessions_csrf_hash_not_empty CHECK (octet_length(csrf_token_hash) > 0),
    CONSTRAINT sessions_expiry_after_creation CHECK (expires_at > created_at)
);

CREATE INDEX sessions_owner_active_idx
    ON sessions (owner_id, expires_at)
    WHERE revoked_at IS NULL;

CREATE TABLE api_tokens (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_instance_id UUID REFERENCES client_instances(id) ON DELETE SET NULL,
    name TEXT NOT NULL,
    token_hash BYTEA NOT NULL UNIQUE,
    actions TEXT[] NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    last_used_at TIMESTAMPTZ,
    expires_at TIMESTAMPTZ,
    revoked_at TIMESTAMPTZ,
    CONSTRAINT [REDACTED]CHECK (btrim(name) <> ''),
    CONSTRAINT [REDACTED]CHECK (octet_length(token_hash) > 0),
    CONSTRAINT [REDACTED]CHECK (cardinality(actions) > 0),
    CONSTRAINT [REDACTED]CHECK (expires_at IS NULL OR expires_at > created_at)
);

CREATE INDEX [REDACTED]    ON api_tokens (owner_id, created_at DESC)
    WHERE revoked_at IS NULL;

CREATE TABLE blobs (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    object_id UUID NOT NULL DEFAULT uuidv7(),
    sha256 BYTEA NOT NULL,
    size_bytes BIGINT NOT NULL,
    state TEXT NOT NULL,
    verified_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT blobs_object_id_unique UNIQUE (object_id),
    CONSTRAINT blobs_owner_sha256_unique UNIQUE (owner_id, sha256),
    CONSTRAINT blobs_sha256_length CHECK (octet_length(sha256) = 32),
    CONSTRAINT blobs_size_nonnegative CHECK (size_bytes >= 0),
    CONSTRAINT blobs_state_valid CHECK (state IN ('staged', 'available', 'unavailable')),
    CONSTRAINT blobs_available_verified CHECK (state <> 'available' OR verified_at IS NOT NULL)
);

CREATE INDEX blobs_gc_candidates_idx ON blobs (created_at) WHERE state <> 'available';

CREATE TABLE nodes (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    parent_id UUID REFERENCES nodes(id) ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE,
    name TEXT NOT NULL,
    name_key TEXT NOT NULL,
    node_type TEXT NOT NULL,
    current_blob_id UUID REFERENCES blobs(id) ON DELETE RESTRICT,
    size_bytes BIGINT NOT NULL DEFAULT 0,
    mime_type TEXT,
    node_revision BIGINT NOT NULL DEFAULT 1,
    content_revision BIGINT NOT NULL DEFAULT 0,
    content_modified_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    metadata_updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    favorite BOOLEAN NOT NULL DEFAULT FALSE,
    trash_root_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT nodes_name_not_blank CHECK (btrim(name) <> ''),
    CONSTRAINT nodes_name_key_not_blank CHECK (btrim(name_key) <> ''),
    CONSTRAINT nodes_type_valid CHECK (node_type IN ('directory', 'file')),
    CONSTRAINT nodes_file_blob_shape CHECK (
        (node_type = 'directory' AND current_blob_id IS NULL AND size_bytes = 0 AND content_revision = 0)
        OR (node_type = 'file' AND current_blob_id IS NOT NULL)
    ),
    CONSTRAINT nodes_size_nonnegative CHECK (size_bytes >= 0),
    CONSTRAINT nodes_node_revision_positive CHECK (node_revision > 0),
    CONSTRAINT nodes_content_revision_nonnegative CHECK (content_revision >= 0),
    CONSTRAINT nodes_not_own_parent CHECK (parent_id IS NULL OR parent_id <> id)
);

CREATE UNIQUE INDEX nodes_sibling_name_unique
    ON nodes (owner_id, parent_id, name_key) NULLS NOT DISTINCT;
CREATE INDEX nodes_parent_listing_idx ON nodes (owner_id, parent_id, name_key, id);
CREATE INDEX nodes_current_blob_idx ON nodes (current_blob_id) WHERE current_blob_id IS NOT NULL;
CREATE INDEX nodes_trash_roots_idx ON nodes (owner_id, trash_root_at) WHERE trash_root_at IS NOT NULL;
CREATE INDEX nodes_name_trgm_idx ON nodes USING gin (name gin_trgm_ops);

ALTER TABLE users
    ADD CONSTRAINT users_root_node_fk
    FOREIGN KEY (root_node_id) REFERENCES nodes(id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;

CREATE TABLE credential_root_grants (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    session_id UUID REFERENCES sessions(id) ON DELETE CASCADE,
    api_token_id UUID REFERENCES api_tokens(id) ON DELETE CASCADE,
    root_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
    actions TEXT[] NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT credential_root_grants_one_credential CHECK (num_nonnulls(session_id, api_token_id) = 1),
    CONSTRAINT credential_root_grants_actions_not_empty CHECK (cardinality(actions) > 0)
);

CREATE UNIQUE INDEX credential_root_grants_session_unique
    ON credential_root_grants (session_id, root_node_id) WHERE session_id IS NOT NULL;
CREATE UNIQUE INDEX credential_root_grants_token_unique
    ON credential_root_grants (api_token_id, root_node_id) WHERE api_token_id IS NOT NULL;

CREATE TABLE idempotency_records (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    credential_kind TEXT NOT NULL,
    credential_id UUID NOT NULL,
    client_instance_id UUID REFERENCES client_instances(id) ON DELETE SET NULL,
    idempotency_key TEXT NOT NULL,
    request_fingerprint BYTEA NOT NULL,
    operation_type TEXT NOT NULL,
    state TEXT NOT NULL,
    response_reference JSONB,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    completed_at TIMESTAMPTZ,
    expires_at TIMESTAMPTZ NOT NULL,
    CONSTRAINT idempotency_key_not_blank CHECK (btrim(idempotency_key) <> ''),
    CONSTRAINT idempotency_fingerprint_not_empty CHECK (octet_length(request_fingerprint) > 0),
    CONSTRAINT idempotency_operation_not_blank CHECK (btrim(operation_type) <> ''),
    CONSTRAINT idempotency_credential_kind_valid CHECK (credential_kind IN ('session', 'api_token', 'internal')),
    CONSTRAINT idempotency_state_valid CHECK (state IN ('in_progress', 'completed', 'failed')),
    CONSTRAINT idempotency_expiry_after_creation CHECK (expires_at > created_at),
    CONSTRAINT idempotency_scope_unique UNIQUE (owner_id, credential_kind, credential_id, idempotency_key)
);

CREATE INDEX idempotency_records_expiry_idx ON idempotency_records (expires_at);

CREATE TABLE changes (
    sequence BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    event_id UUID NOT NULL DEFAULT uuidv7() UNIQUE,
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    event_type TEXT NOT NULL,
    schema_version INTEGER NOT NULL DEFAULT 1,
    node_id UUID NOT NULL,
    node_revision BIGINT,
    content_revision BIGINT,
    subtree_effect BOOLEAN NOT NULL DEFAULT FALSE,
    resulting_state JSONB,
    tombstone JSONB,
    previous_state JSONB,
    mutation_origin JSONB NOT NULL,
    committed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT changes_event_type_not_blank CHECK (btrim(event_type) <> ''),
    CONSTRAINT changes_schema_version_positive CHECK (schema_version > 0),
    CONSTRAINT changes_node_revision_positive CHECK (node_revision IS NULL OR node_revision > 0),
    CONSTRAINT changes_content_revision_nonnegative CHECK (content_revision IS NULL OR content_revision >= 0),
    CONSTRAINT changes_result_shape CHECK (num_nonnulls(resulting_state, tombstone) = 1)
);

CREATE INDEX changes_owner_sequence_idx ON changes (owner_id, sequence);
CREATE INDEX changes_node_history_idx ON changes (owner_id, node_id, sequence DESC);

CREATE TABLE replacement_recovery (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
    blob_id UUID NOT NULL REFERENCES blobs(id) ON DELETE RESTRICT,
    replaced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at TIMESTAMPTZ NOT NULL,
    restored_at TIMESTAMPTZ,
    CONSTRAINT replacement_recovery_expiry_after_replace CHECK (expires_at > replaced_at)
);

CREATE INDEX replacement_recovery_active_idx
    ON replacement_recovery (owner_id, expires_at)
    WHERE restored_at IS NULL;
CREATE INDEX replacement_recovery_blob_idx ON replacement_recovery (blob_id);

CREATE TABLE commit_intents (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
    operation_id UUID NOT NULL,
    node_id UUID REFERENCES nodes(id) ON DELETE RESTRICT,
    parent_id UUID REFERENCES nodes(id) ON DELETE RESTRICT,
    expected_node_revision BIGINT,
    target_name TEXT NOT NULL,
    target_name_key TEXT NOT NULL,
    object_id UUID NOT NULL,
    sha256 BYTEA NOT NULL,
    size_bytes BIGINT NOT NULL,
    state TEXT NOT NULL,
    promoted_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    failure_code TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT commit_intents_operation_unique UNIQUE (owner_id, operation_id),
    CONSTRAINT commit_intents_target_name_not_blank CHECK (btrim(target_name) <> ''),
    CONSTRAINT commit_intents_target_key_not_blank CHECK (btrim(target_name_key) <> ''),
    CONSTRAINT commit_intents_expected_revision_positive CHECK (expected_node_revision IS NULL OR expected_node_revision > 0),
    CONSTRAINT commit_intents_sha256_length CHECK (octet_length(sha256) = 32),
    CONSTRAINT commit_intents_size_nonnegative CHECK (size_bytes >= 0),
    CONSTRAINT commit_intents_state_valid CHECK (state IN ('pending', 'promoted', 'completed', 'failed'))
);

CREATE INDEX commit_intents_recovery_idx
    ON commit_intents (state, created_at)
    WHERE state IN ('pending', 'promoted');

CREATE TABLE resource_leases (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    purpose TEXT NOT NULL,
    resource_kind TEXT NOT NULL,
    resource_id UUID NOT NULL,
    holder_id UUID NOT NULL,
    acquired_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    heartbeat_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at TIMESTAMPTZ NOT NULL,
    released_at TIMESTAMPTZ,
    CONSTRAINT resource_leases_purpose_not_blank CHECK (btrim(purpose) <> ''),
    CONSTRAINT resource_leases_kind_not_blank CHECK (btrim(resource_kind) <> ''),
    CONSTRAINT resource_leases_expiry_after_acquire CHECK (expires_at > acquired_at)
);

CREATE UNIQUE INDEX resource_leases_active_unique
    ON resource_leases (owner_id, purpose, resource_kind, resource_id)
    WHERE released_at IS NULL;
CREATE INDEX resource_leases_expiry_idx
    ON resource_leases (expires_at)
    WHERE released_at IS NULL;

CREATE TABLE jobs (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID REFERENCES users(id) ON DELETE CASCADE,
    job_type TEXT NOT NULL,
    state TEXT NOT NULL,
    payload JSONB NOT NULL DEFAULT '{}'::jsonb,
    progress JSONB NOT NULL DEFAULT '{}'::jsonb,
    attempt_count BIGINT NOT NULL DEFAULT 0,
    max_attempts BIGINT NOT NULL DEFAULT 5,
    lease_holder UUID,
    lease_expires_at TIMESTAMPTZ,
    heartbeat_at TIMESTAMPTZ,
    next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    cancel_requested_at TIMESTAMPTZ,
    started_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    last_error_code TEXT,
    last_error_detail TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT jobs_type_not_blank CHECK (btrim(job_type) <> ''),
    CONSTRAINT jobs_state_valid CHECK (state IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')),
    CONSTRAINT jobs_attempt_count_nonnegative CHECK (attempt_count >= 0),
    CONSTRAINT jobs_max_attempts_positive CHECK (max_attempts > 0)
);

CREATE INDEX jobs_claim_idx ON jobs (next_attempt_at, created_at)
    WHERE state = 'queued';
CREATE INDEX jobs_lease_idx ON jobs (lease_expires_at)
    WHERE state = 'running';

CREATE TABLE settings (
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    setting_key TEXT NOT NULL,
    value JSONB NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (owner_id, setting_key),
    CONSTRAINT settings_key_not_blank CHECK (btrim(setting_key) <> '')
);

CREATE TABLE audit_events (
    sequence BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    event_id UUID NOT NULL DEFAULT uuidv7() UNIQUE,
    owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
    actor JSONB NOT NULL,
    action TEXT NOT NULL,
    resource_type TEXT,
    resource_id UUID,
    outcome TEXT NOT NULL,
    request_id UUID,
    operation_id UUID,
    details JSONB NOT NULL DEFAULT '{}'::jsonb,
    occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT audit_events_action_not_blank CHECK (btrim(action) <> ''),
    CONSTRAINT audit_events_outcome_valid CHECK (outcome IN ('succeeded', 'denied', 'failed'))
);

CREATE INDEX audit_events_owner_time_idx ON audit_events (owner_id, occurred_at DESC);
CREATE INDEX audit_events_resource_idx ON audit_events (resource_type, resource_id, occurred_at DESC);

CREATE TABLE export_leases (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    operation_id UUID NOT NULL,
    state TEXT NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    released_at TIMESTAMPTZ,
    CONSTRAINT export_leases_operation_unique UNIQUE (owner_id, operation_id),
    CONSTRAINT export_leases_state_valid CHECK (state IN ('active', 'released', 'expired')),
    CONSTRAINT export_leases_expiry_after_creation CHECK (expires_at > created_at)
);

CREATE TABLE export_items (
    export_lease_id UUID NOT NULL REFERENCES export_leases(id) ON DELETE CASCADE,
    node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,
    blob_id UUID REFERENCES blobs(id) ON DELETE RESTRICT,
    node_revision BIGINT NOT NULL,
    relative_path TEXT NOT NULL,
    PRIMARY KEY (export_lease_id, node_id),
    CONSTRAINT export_items_node_revision_positive CHECK (node_revision > 0),
    CONSTRAINT export_items_relative_path_not_blank CHECK (btrim(relative_path) <> '')
);

CREATE INDEX export_items_blob_idx ON export_items (blob_id) WHERE blob_id IS NOT NULL;
