-- Migration: Identity and Authentication
-- Description: Creates identity, permission, and policy tables
-- Version: 20250101000002

-- ============================================================================
-- IDENTITY TABLE
-- ============================================================================

CREATE TABLE identity (
    id BIGSERIAL PRIMARY KEY,
    login TEXT NOT NULL UNIQUE,
    display_name TEXT,
    password_hash TEXT,
    attributes JSONB NOT NULL DEFAULT '{}'::jsonb,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_identity_login ON identity(login);
CREATE INDEX idx_identity_created ON identity(created DESC);
CREATE INDEX idx_identity_password_hash ON identity(password_hash) WHERE password_hash IS NOT NULL;
CREATE INDEX idx_identity_attributes_gin ON identity USING GIN (attributes);

-- Trigger
CREATE TRIGGER update_identity_updated
    BEFORE UPDATE ON identity
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_column();

-- Comments
COMMENT ON TABLE identity IS 'Identities represent users or service accounts';
COMMENT ON COLUMN identity.login IS 'Unique login identifier';
COMMENT ON COLUMN identity.display_name IS 'Human-readable name';
COMMENT ON COLUMN identity.password_hash IS 'Argon2 hashed password for authentication (NULL for service accounts or external auth)';
COMMENT ON COLUMN identity.attributes IS 'Custom attributes (email, groups, etc.)';

-- ============================================================================
-- ADD FOREIGN KEY CONSTRAINTS TO EXISTING TABLES
-- ============================================================================

-- Add foreign key constraint for pack.installed_by now that identity table exists
ALTER TABLE pack
    ADD CONSTRAINT fk_pack_installed_by
    FOREIGN KEY (installed_by)
    REFERENCES identity(id)
    ON DELETE SET NULL;

-- ============================================================================

-- ============================================================================
-- PERMISSION_SET TABLE
-- ============================================================================

CREATE TABLE permission_set (
    id BIGSERIAL PRIMARY KEY,
    ref TEXT NOT NULL UNIQUE,
    pack BIGINT REFERENCES pack(id) ON DELETE CASCADE,
    pack_ref TEXT,
    label TEXT,
    description TEXT,
    grants JSONB NOT NULL DEFAULT '[]'::jsonb,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT permission_set_ref_lowercase CHECK (ref = LOWER(ref)),
    CONSTRAINT permission_set_ref_format CHECK (ref ~ '^[^.]+\.[^.]+$')
);

-- Indexes
CREATE INDEX idx_permission_set_ref ON permission_set(ref);
CREATE INDEX idx_permission_set_pack ON permission_set(pack);
CREATE INDEX idx_permission_set_created ON permission_set(created DESC);

-- Trigger
CREATE TRIGGER update_permission_set_updated
    BEFORE UPDATE ON permission_set
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_column();

-- Comments
COMMENT ON TABLE permission_set IS 'Permission sets group permissions together (like roles)';
COMMENT ON COLUMN permission_set.ref IS 'Unique permission set reference (format: pack.name)';
COMMENT ON COLUMN permission_set.label IS 'Human-readable name';
COMMENT ON COLUMN permission_set.grants IS 'Array of permission grants';

-- ============================================================================

-- ============================================================================
-- PERMISSION_ASSIGNMENT TABLE
-- ============================================================================

CREATE TABLE permission_assignment (
    id BIGSERIAL PRIMARY KEY,
    identity BIGINT NOT NULL REFERENCES identity(id) ON DELETE CASCADE,
    permset BIGINT NOT NULL REFERENCES permission_set(id) ON DELETE CASCADE,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Unique constraint to prevent duplicate assignments
    CONSTRAINT unique_identity_permset UNIQUE (identity, permset)
);

-- Indexes
CREATE INDEX idx_permission_assignment_identity ON permission_assignment(identity);
CREATE INDEX idx_permission_assignment_permset ON permission_assignment(permset);
CREATE INDEX idx_permission_assignment_created ON permission_assignment(created DESC);
CREATE INDEX idx_permission_assignment_identity_created ON permission_assignment(identity, created DESC);
CREATE INDEX idx_permission_assignment_permset_created ON permission_assignment(permset, created DESC);

-- Comments
COMMENT ON TABLE permission_assignment IS 'Links identities to permission sets (many-to-many)';
COMMENT ON COLUMN permission_assignment.identity IS 'Identity being granted permissions';
COMMENT ON COLUMN permission_assignment.permset IS 'Permission set being assigned';

-- ============================================================================

ALTER TABLE identity
    ADD COLUMN frozen BOOLEAN NOT NULL DEFAULT false;

CREATE INDEX idx_identity_frozen ON identity(frozen);

COMMENT ON COLUMN identity.frozen IS 'If true, authentication is blocked for this identity';

CREATE TABLE identity_role_assignment (
    id BIGSERIAL PRIMARY KEY,
    identity BIGINT NOT NULL REFERENCES identity(id) ON DELETE CASCADE,
    role TEXT NOT NULL,
    source TEXT NOT NULL DEFAULT 'manual',
    managed BOOLEAN NOT NULL DEFAULT false,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT unique_identity_role_assignment UNIQUE (identity, role)
);

CREATE INDEX idx_identity_role_assignment_identity
    ON identity_role_assignment(identity);
CREATE INDEX idx_identity_role_assignment_role
    ON identity_role_assignment(role);
CREATE INDEX idx_identity_role_assignment_source
    ON identity_role_assignment(source);

CREATE TRIGGER update_identity_role_assignment_updated
    BEFORE UPDATE ON identity_role_assignment
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_column();

COMMENT ON TABLE identity_role_assignment IS 'Links identities to role labels from manual assignment or external identity providers';
COMMENT ON COLUMN identity_role_assignment.role IS 'Opaque role/group label (e.g. IDP group name)';
COMMENT ON COLUMN identity_role_assignment.source IS 'Where the role assignment originated (manual, oidc, ldap, sync, etc.)';
COMMENT ON COLUMN identity_role_assignment.managed IS 'True when the assignment is managed by external sync and should not be edited manually';

CREATE TABLE permission_set_role_assignment (
    id BIGSERIAL PRIMARY KEY,
    permset BIGINT NOT NULL REFERENCES permission_set(id) ON DELETE CASCADE,
    role TEXT NOT NULL,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT unique_permission_set_role_assignment UNIQUE (permset, role)
);

CREATE INDEX idx_permission_set_role_assignment_permset
    ON permission_set_role_assignment(permset);
CREATE INDEX idx_permission_set_role_assignment_role
    ON permission_set_role_assignment(role);

COMMENT ON TABLE permission_set_role_assignment IS 'Links permission sets to role labels for role-based grant expansion';
COMMENT ON COLUMN permission_set_role_assignment.role IS 'Opaque role/group label associated with the permission set';

-- ============================================================================

-- ============================================================================
-- POLICY TABLE
-- ============================================================================

CREATE TABLE policy (
    id BIGSERIAL PRIMARY KEY,
    ref TEXT NOT NULL UNIQUE,
    pack BIGINT REFERENCES pack(id) ON DELETE CASCADE,
    pack_ref TEXT,
    action BIGINT, -- Forward reference to action table, will add constraint in next migration
    action_ref TEXT,
    parameters TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
    method policy_method_enum NOT NULL,
    threshold INTEGER NOT NULL,
    name TEXT NOT NULL,
    description TEXT,
    tags TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT policy_ref_lowercase CHECK (ref = LOWER(ref)),
    CONSTRAINT policy_ref_format CHECK (ref ~ '^[^.]+\.[^.]+$'),
    CONSTRAINT policy_threshold_positive CHECK (threshold > 0)
);

-- Indexes
CREATE INDEX idx_policy_ref ON policy(ref);
CREATE INDEX idx_policy_pack ON policy(pack);
CREATE INDEX idx_policy_action ON policy(action);
CREATE INDEX idx_policy_created ON policy(created DESC);
CREATE INDEX idx_policy_action_created ON policy(action, created DESC);
CREATE INDEX idx_policy_pack_created ON policy(pack, created DESC);
CREATE INDEX idx_policy_parameters_gin ON policy USING GIN (parameters);
CREATE INDEX idx_policy_tags_gin ON policy USING GIN (tags);

-- Trigger
CREATE TRIGGER update_policy_updated
    BEFORE UPDATE ON policy
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_column();

-- Comments
COMMENT ON TABLE policy IS 'Policies define execution controls (rate limiting, concurrency)';
COMMENT ON COLUMN policy.ref IS 'Unique policy reference (format: pack.name)';
COMMENT ON COLUMN policy.action IS 'Action this policy applies to';
COMMENT ON COLUMN policy.parameters IS 'Parameter names used for policy grouping';
COMMENT ON COLUMN policy.method IS 'How to handle policy violations (cancel/enqueue)';
COMMENT ON COLUMN policy.threshold IS 'Numeric limit (e.g., max concurrent executions)';

-- ============================================================================
