-- Migration 0042: Identity and Access Control (IAM) Tables
-- AEG-VS-01-02: Data Contract for Identity/Access Management
-- Created: 2026-08-17
-- Status: READY FOR REVIEW

BEGIN;

-- 1. IDENTITY TABLE (PIT: point-in-time identity)
CREATE TABLE IF NOT EXISTS public.identity (
  identity_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Identity attributes
  username VARCHAR(255) NOT NULL UNIQUE,
  email VARCHAR(255) NOT NULL UNIQUE,
  display_name VARCHAR(255),

  -- State machine (UNDEFINED → ACTIVE → REQUIRES_MFA_SETUP → MFA_CONFIGURED → MFA_SUSPENDED → INACTIVE → REVOKED)
  state VARCHAR(50) NOT NULL DEFAULT 'UNDEFINED'
    CHECK (state IN ('UNDEFINED', 'ACTIVE', 'REQUIRES_MFA_SETUP', 'MFA_CONFIGURED', 'MFA_SUSPENDED', 'INACTIVE', 'REVOKED')),

  -- MFA requirement flag
  mfa_required BOOLEAN NOT NULL DEFAULT false,
  mfa_enforced_at TIMESTAMP WITH TIME ZONE,

  -- Lifecycle tracking (immutable append-only)
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,

  -- Audit columns (for correction events)
  published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_end TIMESTAMP WITH TIME ZONE,
  revision_version INT NOT NULL DEFAULT 1,

  -- Idempotency & correlation
  correlation_id UUID UNIQUE,
  source_event_id UUID UNIQUE,
  checksum VARCHAR(64)
);

CREATE INDEX idx_identity_username ON public.identity(username);
CREATE INDEX idx_identity_email ON public.identity(email);
CREATE INDEX idx_identity_state ON public.identity(state);
CREATE INDEX idx_identity_published_at ON public.identity(published_at);

-- 2. ROLE TABLE (Core & Domain-Specific Roles)
CREATE TABLE IF NOT EXISTS public.role (
  role_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Role definition
  role_name VARCHAR(100) NOT NULL UNIQUE,
  description TEXT,

  -- Hierarchy (0 = GUEST, 1 = USER, 2 = OPERATOR, 3 = ADMIN, 4 = SUPER_ADMIN, 100+ = domain-specific)
  hierarchy_level INT NOT NULL DEFAULT 0,

  -- Role type (CORE / DOMAIN_SPECIFIC / TEMPORARY / SERVICE)
  role_type VARCHAR(50) NOT NULL DEFAULT 'CORE'
    CHECK (role_type IN ('CORE', 'DOMAIN_SPECIFIC', 'TEMPORARY', 'SERVICE')),

  -- Expiration (for TEMPORARY roles like quarterly reviewer)
  expires_at TIMESTAMP WITH TIME ZONE,

  -- Lifecycle
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_end TIMESTAMP WITH TIME ZONE,
  revision_version INT NOT NULL DEFAULT 1,

  -- Idempotency
  correlation_id UUID UNIQUE,
  checksum VARCHAR(64)
);

CREATE INDEX idx_role_name ON public.role(role_name);
CREATE INDEX idx_role_hierarchy ON public.role(hierarchy_level);
CREATE INDEX idx_role_type ON public.role(role_type);

-- 3. ROLE_ASSIGNMENT TABLE (With Maker-Checker Workflow)
CREATE TABLE IF NOT EXISTS public.role_assignment (
  role_assignment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Association
  identity_id UUID NOT NULL REFERENCES public.identity(identity_id) ON DELETE CASCADE,
  role_id UUID NOT NULL REFERENCES public.role(role_id) ON DELETE CASCADE,

  -- Maker-Checker workflow
  -- State: PENDING_APPROVAL → APPROVED_BY_1 → APPROVED_BY_2 → ACTIVE → EXPIRED / REVOKED
  assignment_state VARCHAR(50) NOT NULL DEFAULT 'PENDING_APPROVAL'
    CHECK (assignment_state IN ('PENDING_APPROVAL', 'APPROVED_BY_1', 'APPROVED_BY_2', 'ACTIVE', 'EXPIRED', 'REVOKED', 'REJECTED')),

  -- Approval tracking
  approval_count INT DEFAULT 0,
  required_approval_count INT NOT NULL DEFAULT 2,  -- Configurable per role
  approved_by_identity_ids UUID[] DEFAULT '{}',
  approval_reason TEXT,

  -- Effective date (when role becomes ACTIVE)
  effective_at TIMESTAMP WITH TIME ZONE,

  -- Lifecycle
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_end TIMESTAMP WITH TIME ZONE,
  revision_version INT NOT NULL DEFAULT 1,

  -- Idempotency & correlation
  correlation_id UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
  checksum VARCHAR(64),

  -- Constraints: One role per identity (except temporary/special cases)
  UNIQUE(identity_id, role_id) WHERE assignment_state NOT IN ('REVOKED', 'REJECTED')
);

CREATE INDEX idx_role_assignment_identity ON public.role_assignment(identity_id);
CREATE INDEX idx_role_assignment_role ON public.role_assignment(role_id);
CREATE INDEX idx_role_assignment_state ON public.role_assignment(assignment_state);
CREATE INDEX idx_role_assignment_correlation ON public.role_assignment(correlation_id);

-- 4. PERMISSION TABLE (Granular Permissions)
CREATE TABLE IF NOT EXISTS public.permission (
  permission_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Permission definition
  permission_name VARCHAR(100) NOT NULL UNIQUE,
  description TEXT,

  -- Resource and action (e.g., "MODEL:READ", "DATASET:WRITE", "AUDIT_LOG:READ")
  resource VARCHAR(50) NOT NULL,
  action VARCHAR(50) NOT NULL,

  -- Permission category (DATA_ACCESS / WORKFLOW_APPROVAL / ADMIN / AUDIT)
  permission_category VARCHAR(50) NOT NULL
    CHECK (permission_category IN ('DATA_ACCESS', 'WORKFLOW_APPROVAL', 'ADMIN', 'AUDIT')),

  -- Lifecycle
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_end TIMESTAMP WITH TIME ZONE,
  revision_version INT NOT NULL DEFAULT 1,

  -- Idempotency
  correlation_id UUID UNIQUE,
  checksum VARCHAR(64)
);

CREATE UNIQUE INDEX idx_permission_resource_action ON public.permission(resource, action);
CREATE INDEX idx_permission_category ON public.permission(permission_category);

-- 5. ROLE_PERMISSION MAPPING (M:N - Roles to Permissions)
CREATE TABLE IF NOT EXISTS public.role_permission (
  role_permission_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  role_id UUID NOT NULL REFERENCES public.role(role_id) ON DELETE CASCADE,
  permission_id UUID NOT NULL REFERENCES public.permission(permission_id) ON DELETE CASCADE,

  -- Lifecycle
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_end TIMESTAMP WITH TIME ZONE,

  -- Mapping enforced: one permission per role
  UNIQUE(role_id, permission_id)
);

CREATE INDEX idx_role_permission_role ON public.role_permission(role_id);
CREATE INDEX idx_role_permission_permission ON public.role_permission(permission_id);

-- 6. MFA_DEVICE TABLE (Multi-Factor Authentication)
CREATE TABLE IF NOT EXISTS public.mfa_device (
  mfa_device_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  identity_id UUID NOT NULL REFERENCES public.identity(identity_id) ON DELETE CASCADE,

  -- Device type (TOTP / WEBAUTHN / SMS / EMAIL)
  device_type VARCHAR(50) NOT NULL
    CHECK (device_type IN ('TOTP', 'WEBAUTHN', 'SMS', 'EMAIL')),

  -- Device identifier (for recovery/management)
  device_name VARCHAR(255),
  device_identifier VARCHAR(255) UNIQUE,

  -- Secret (encrypted, stored as hash only for recovery codes)
  secret_hash VARCHAR(255),

  -- State (PENDING_VERIFICATION → VERIFIED → REVOKED)
  state VARCHAR(50) NOT NULL DEFAULT 'PENDING_VERIFICATION'
    CHECK (state IN ('PENDING_VERIFICATION', 'VERIFIED', 'REVOKED')),

  -- Last used (for anomaly detection)
  last_used_at TIMESTAMP WITH TIME ZONE,

  -- Lifecycle
  created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  valid_time_end TIMESTAMP WITH TIME ZONE,

  -- Idempotency
  correlation_id UUID UNIQUE,
  checksum VARCHAR(64)
);

CREATE INDEX idx_mfa_device_identity ON public.mfa_device(identity_id);
CREATE INDEX idx_mfa_device_state ON public.mfa_device(state);

COMMIT;
