package store

import (
	"context"
	"encoding/json"
	"errors"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
	"github.com/jackc/pgx/v5"
)

var ErrIdentityManagementRequestNotFound = errors.New("identity management request not found")

type IdentityManagementRequest struct {
	Operation   string
	Key         string
	RequestHash string
	Response    []byte
	CreatedAt   time.Time
}

const identityRevisionColumns = `
id::text,state,schema_version,tenant_mode,auth_center_url,COALESCE(issuer,''),COALESCE(tenant_id,''),COALESCE(application_id,''),
COALESCE(audience,''),COALESCE(browser_client_id,''),COALESCE(machine_client_id,''),scopes,capabilities,role_prefix,
local_tenant_key,public_base_url,web_base_url,jit_enabled,legacy_jwt_enabled,token_introspection,session_revocation,
COALESCE(security_event_issuer,''),COALESCE(security_event_configuration_url,''),COALESCE(security_event_audience,''),
COALESCE(tenant_context_endpoint,''),COALESCE(tenant_context_audience,''),COALESCE(tenant_context_scope,''),
COALESCE(machine_credential_ref,''),COALESCE(session_encryption_key_ref,''),session_idle_seconds,session_absolute_seconds,
session_refresh_seconds,version,COALESCE(last_error_category,''),COALESCE(last_trace_id,''),COALESCE(last_audit_id,''),
validated_at,activated_at,superseded_at,created_at,updated_at`

// identityConfigurationLifecycleLockID serializes the database-side boundary
// between onboarding reservations and Active Revision mutations. A transaction
// must hold this lock before deciding that no Active configuration or foreign
// pairing reservation exists.
const identityConfigurationLifecycleLockID int64 = 0x4541494944454e54

// identityDisabledSecretCleanupDelay is intentionally longer than the
// IdentityRuntimeManager's 30-second old-Runtime retirement window. Starting
// cleanup one minute after the database handoff leaves an additional 30
// seconds for commit, Runtime publication, timer scheduling, and worker jitter.
const identityDisabledSecretCleanupDelay = time.Minute

func lockIdentityConfigurationLifecycle(ctx context.Context, tx pgx.Tx) error {
	_, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock($1)`, identityConfigurationLifecycleLockID)
	return err
}

// beginIdentityConfigurationLifecycleTx deliberately uses READ COMMITTED.
// PostgreSQL fixes the transaction snapshot when a SERIALIZABLE transaction
// executes the advisory-lock SELECT, before that statement finishes waiting.
// A later state check can therefore miss the preceding lock holder's commit.
// READ COMMITTED gives every statement after lock acquisition a fresh snapshot,
// while the transaction-scoped advisory lock serializes all lifecycle decisions.
func (s *Store) beginIdentityConfigurationLifecycleTx(ctx context.Context) (pgx.Tx, error) {
	tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.ReadCommitted})
	if err != nil {
		return nil, err
	}
	if err := lockIdentityConfigurationLifecycle(ctx, tx); err != nil {
		rollbackTransaction(tx)
		return nil, err
	}
	return tx, nil
}

func queueDisabledIdentitySecretCleanupTx(ctx context.Context, tx pgx.Tx, reference string, notBefore time.Time) error {
	tag, err := tx.Exec(ctx, `INSERT INTO gateway_identity_secret_cleanup_queue(secret_ref,not_before)
VALUES($1,$2) ON CONFLICT(secret_ref) DO UPDATE
SET not_before=GREATEST(gateway_identity_secret_cleanup_queue.not_before,EXCLUDED.not_before),updated_at=now()
WHERE gateway_identity_secret_cleanup_queue.status='pending'`, reference, notBefore)
	if err != nil {
		return err
	}
	if tag.RowsAffected() != 1 {
		return ErrIdentitySecretCleanupConflict
	}
	return nil
}

func (s *Store) CreateIdentityConfigurationRevision(ctx context.Context, revision identity.Revision) (identity.Revision, error) {
	scopes, _ := json.Marshal(revision.Scopes)
	capabilities, _ := json.Marshal(revision.Capabilities)
	return scanIdentityRevision(s.pool.QueryRow(ctx, `
INSERT INTO gateway_identity_configuration_revisions (
  id,state,schema_version,tenant_mode,auth_center_url,role_prefix,local_tenant_key,public_base_url,web_base_url,
  jit_enabled,legacy_jwt_enabled,scopes,capabilities,session_idle_seconds,session_absolute_seconds,session_refresh_seconds
) VALUES ($1::uuid,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::jsonb,$13::jsonb,$14,$15,$16)
RETURNING `+identityRevisionColumns,
		revision.ID, revision.State, revision.SchemaVersion, revision.TenantMode, revision.AuthCenterURL, revision.RolePrefix,
		revision.LocalTenantKey, revision.PublicBaseURL, revision.WebBaseURL, revision.JITEnabled, revision.LegacyJWTEnabled,
		string(scopes), string(capabilities), revision.SessionIdleSeconds, revision.SessionAbsoluteSeconds, revision.SessionRefreshSeconds,
	))
}

func (s *Store) IdentityConfigurationRevision(ctx context.Context, id string) (identity.Revision, error) {
	revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `SELECT `+identityRevisionColumns+`
FROM gateway_identity_configuration_revisions WHERE id=$1::uuid`, id))
	return revision, normalizeIdentityRevisionError(err)
}

func (s *Store) ActiveIdentityConfigurationRevision(ctx context.Context) (identity.Revision, error) {
	revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `SELECT `+identityRevisionColumns+`
FROM gateway_identity_configuration_revisions WHERE state='active'`))
	return revision, normalizeIdentityRevisionError(err)
}

func (s *Store) LatestInactiveIdentityConfigurationRevision(ctx context.Context) (identity.Revision, error) {
	revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `SELECT `+identityRevisionColumns+`
FROM gateway_identity_configuration_revisions
WHERE id=(
  SELECT revision.id
  FROM gateway_identity_configuration_revisions revision
  JOIN gateway_identity_onboarding_exchanges exchange ON exchange.revision_id=revision.id
  WHERE revision.state IN ('draft','validated','failed')
  ORDER BY NOT (exchange.status='cancelled' AND exchange.cleanup_status='completed') DESC,
    revision.created_at DESC,exchange.created_at DESC
  LIMIT 1
)`))
	return revision, normalizeIdentityRevisionError(err)
}

func (s *Store) LatestSupersededIdentityConfigurationRevision(ctx context.Context) (identity.Revision, error) {
	revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `SELECT `+identityRevisionColumns+`
FROM gateway_identity_configuration_revisions WHERE state='superseded' ORDER BY superseded_at DESC NULLS LAST LIMIT 1`))
	return revision, normalizeIdentityRevisionError(err)
}

func (s *Store) UpdateIdentityRevisionPolicy(ctx context.Context, id string, expectedVersion int64, policy identity.RevisionPolicy) (identity.Revision, error) {
	if err := policy.Validate(); err != nil {
		return identity.Revision{}, err
	}
	revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `
UPDATE gateway_identity_configuration_revisions SET local_tenant_key=$3,role_prefix=$4,jit_enabled=$5,
legacy_jwt_enabled=$6,session_idle_seconds=$7,session_absolute_seconds=$8,session_refresh_seconds=$9,
version=version+1,updated_at=now()
WHERE id=$1::uuid AND version=$2 AND state='draft'
RETURNING `+identityRevisionColumns, id, expectedVersion, policy.LocalTenantKey, policy.RolePrefix, policy.JITEnabled,
		policy.LegacyJWTEnabled, policy.SessionIdleSeconds, policy.SessionAbsoluteSeconds, policy.SessionRefreshSeconds))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	return revision, err
}

func (s *Store) IdentityManagementRequest(ctx context.Context, operation, key string) (IdentityManagementRequest, error) {
	var request IdentityManagementRequest
	err := s.pool.QueryRow(ctx, `SELECT operation,idempotency_key,request_hash,response,created_at
FROM gateway_identity_management_requests WHERE operation=$1 AND idempotency_key=$2`, operation, key).Scan(
		&request.Operation, &request.Key, &request.RequestHash, &request.Response, &request.CreatedAt,
	)
	if errors.Is(err, pgx.ErrNoRows) {
		return IdentityManagementRequest{}, ErrIdentityManagementRequestNotFound
	}
	return request, err
}

func (s *Store) RecordIdentityManagementRequest(ctx context.Context, request IdentityManagementRequest) error {
	if !json.Valid(request.Response) {
		return errors.New("identity management response is invalid")
	}
	_, err := s.pool.Exec(ctx, `INSERT INTO gateway_identity_management_requests(operation,idempotency_key,request_hash,response)
VALUES($1,$2,$3,$4::jsonb) ON CONFLICT(operation,idempotency_key) DO NOTHING`,
		request.Operation, request.Key, request.RequestHash, request.Response)
	return err
}

func (s *Store) ApplyIdentityManifest(ctx context.Context, id string, expectedVersion int64, applied identity.ManifestApplication) (identity.Revision, error) {
	tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return identity.Revision{}, err
	}
	defer rollbackTransaction(tx)
	current, err := scanIdentityRevision(tx.QueryRow(ctx, `SELECT `+identityRevisionColumns+`
FROM gateway_identity_configuration_revisions WHERE id=$1::uuid FOR UPDATE`, id))
	if err != nil {
		return identity.Revision{}, normalizeIdentityRevisionError(err)
	}
	if current.Version != expectedVersion {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	updated, err := identity.ApplyManifest(current, applied)
	if err != nil {
		return identity.Revision{}, err
	}
	newReferences := make([]string, 0, 2)
	for _, reference := range []string{updated.MachineCredentialRef, updated.SessionEncryptionKeyRef} {
		if reference != "" {
			newReferences = append(newReferences, reference)
		}
	}
	if len(newReferences) > 0 {
		if err := adoptPendingIdentitySecrets(ctx, tx, newReferences...); err != nil {
			return identity.Revision{}, err
		}
	}
	scopes, _ := json.Marshal(updated.Scopes)
	capabilities, _ := json.Marshal(updated.Capabilities)
	revision, err := scanIdentityRevision(tx.QueryRow(ctx, `
UPDATE gateway_identity_configuration_revisions SET
  schema_version=$3,tenant_mode=$4,issuer=$5,tenant_id=NULLIF($6,''),application_id=$7,audience=NULLIF($8,''),
  browser_client_id=NULLIF($9,''),machine_client_id=NULLIF($10,''),local_tenant_key=$11,
  scopes=$12::jsonb,capabilities=$13::jsonb,token_introspection=$14,session_revocation=$15,
  security_event_issuer=NULLIF($16,''),security_event_configuration_url=NULLIF($17,''),security_event_audience=NULLIF($18,''),
  tenant_context_endpoint=NULLIF($19,''),tenant_context_audience=NULLIF($20,''),tenant_context_scope=NULLIF($21,''),
  machine_credential_ref=NULLIF($22,''),session_encryption_key_ref=NULLIF($23,''),last_trace_id=NULLIF($24,''),
  last_audit_id=NULLIF($25,''),last_error_category=NULL,version=version+1,updated_at=now()
WHERE id=$1::uuid AND version=$2 AND state='draft'
RETURNING `+identityRevisionColumns,
		id, expectedVersion, updated.SchemaVersion, updated.TenantMode, updated.Issuer, updated.TenantID, updated.ApplicationID, updated.Audience,
		updated.BrowserClientID, updated.MachineClientID, updated.LocalTenantKey, string(scopes), string(capabilities), updated.TokenIntrospection,
		updated.SessionRevocation, updated.SecurityEventIssuer, updated.SecurityEventConfigURL, updated.SecurityEventAudience,
		updated.TenantContextEndpoint, updated.TenantContextAudience, updated.TenantContextScope,
		updated.MachineCredentialRef, updated.SessionEncryptionKeyRef, updated.LastTraceID, updated.LastAuditID,
	))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	if err != nil {
		return identity.Revision{}, err
	}
	activeReferences := map[string]struct{}{}
	for _, reference := range newReferences {
		activeReferences[reference] = struct{}{}
	}
	for _, reference := range []string{current.MachineCredentialRef, current.SessionEncryptionKeyRef} {
		if reference == "" {
			continue
		}
		if _, stillActive := activeReferences[reference]; stillActive {
			continue
		}
		if err := queueIdentitySecretCleanupTx(ctx, tx, reference, time.Now().UTC()); err != nil {
			return identity.Revision{}, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return identity.Revision{}, err
	}
	return revision, nil
}

func (s *Store) MarkIdentityRevisionValidated(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
	revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `
UPDATE gateway_identity_configuration_revisions SET state='validated',validated_at=now(),last_error_category=NULL,
  last_trace_id=NULLIF($3,''),last_audit_id=NULLIF($4,''),version=version+1,updated_at=now()
WHERE id=$1::uuid AND version=$2 AND state='draft'
RETURNING `+identityRevisionColumns, id, expectedVersion, traceID, auditID))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	return revision, err
}

func (s *Store) RevalidateActiveIdentityRevision(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
	revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `
UPDATE gateway_identity_configuration_revisions SET validated_at=now(),last_error_category=NULL,
last_trace_id=NULLIF($3,''),last_audit_id=NULLIF($4,''),version=version+1,updated_at=now()
WHERE id=$1::uuid AND version=$2 AND state='active'
RETURNING `+identityRevisionColumns, id, expectedVersion, traceID, auditID))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	return revision, err
}

func (s *Store) MarkIdentityRevisionFailed(ctx context.Context, id string, expectedVersion int64, category, traceID, auditID string) (identity.Revision, error) {
	revision, err := scanIdentityRevision(s.pool.QueryRow(ctx, `
UPDATE gateway_identity_configuration_revisions SET state='failed',last_error_category=NULLIF($3,''),
  last_trace_id=NULLIF($4,''),last_audit_id=NULLIF($5,''),version=version+1,updated_at=now()
WHERE id=$1::uuid AND version=$2 AND state IN ('draft','validated')
RETURNING `+identityRevisionColumns, id, expectedVersion, category, traceID, auditID))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	return revision, err
}

func (s *Store) ActivateIdentityRevision(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, bool, error) {
	tx, err := s.beginIdentityConfigurationLifecycleTx(ctx)
	if err != nil {
		return identity.Revision{}, false, err
	}
	defer rollbackTransaction(tx)
	var foreignPairingReservation bool
	if err := tx.QueryRow(ctx, `SELECT EXISTS(
  SELECT 1 FROM gateway_identity_pairing_start_reservation
  WHERE (state='starting' AND expires_at > now())
     OR (state='paired' AND revision_id IS DISTINCT FROM $1::uuid)
)`, id).Scan(&foreignPairingReservation); err != nil {
		return identity.Revision{}, false, err
	}
	if foreignPairingReservation {
		return identity.Revision{}, false, identity.ErrPairingInProgress
	}
	if ok, err := hasBreakGlassManager(ctx, tx); err != nil {
		return identity.Revision{}, false, err
	} else if !ok {
		return identity.Revision{}, false, identity.ErrBreakGlassRequired
	}
	var tenantExists bool
	if err := tx.QueryRow(ctx, `SELECT revision.tenant_mode='multi_tenant' OR EXISTS(
  SELECT 1 FROM gateway_tenants tenant
  WHERE tenant.tenant_key=revision.local_tenant_key AND tenant.status='active'
)
FROM gateway_identity_configuration_revisions revision
WHERE revision.id=$1::uuid`, id).Scan(&tenantExists); err != nil {
		return identity.Revision{}, false, err
	}
	if !tenantExists {
		return identity.Revision{}, false, identity.ErrLocalTenantInvalid
	}
	var previousID, previousIssuer, previousTenant, previousAudience, previousClient, previousSessionRef string
	if err := tx.QueryRow(ctx, `SELECT id::text,COALESCE(issuer,''),COALESCE(tenant_id,''),COALESCE(audience,''),
COALESCE(browser_client_id,''),COALESCE(session_encryption_key_ref,'') FROM gateway_identity_configuration_revisions
WHERE state='active' FOR UPDATE`).Scan(
		&previousID, &previousIssuer, &previousTenant, &previousAudience, &previousClient, &previousSessionRef,
	); err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, false, err
	}
	if previousID != "" && previousID != id {
		return identity.Revision{}, false, identity.ErrActiveConfigurationHandoffRequired
	}
	var nextIssuer, nextTenant, nextAudience, nextClient, nextSessionRef string
	if err := tx.QueryRow(ctx, `SELECT COALESCE(issuer,''),COALESCE(tenant_id,''),COALESCE(audience,''),
COALESCE(browser_client_id,''),COALESCE(session_encryption_key_ref,'') FROM gateway_identity_configuration_revisions
WHERE id=$1::uuid AND version=$2 AND state='validated' FOR UPDATE`, id, expectedVersion).Scan(
		&nextIssuer, &nextTenant, &nextAudience, &nextClient, &nextSessionRef,
	); errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, false, identity.ErrRevisionConflict
	} else if err != nil {
		return identity.Revision{}, false, err
	}
	if previousID != "" {
		if _, err := tx.Exec(ctx, `UPDATE gateway_identity_configuration_revisions SET state='superseded',superseded_at=now(),
version=version+1,updated_at=now() WHERE id=$1::uuid AND state='active'`, previousID); err != nil {
			return identity.Revision{}, false, err
		}
	}
	revision, err := scanIdentityRevision(tx.QueryRow(ctx, `UPDATE gateway_identity_configuration_revisions SET state='active',
activated_at=now(),superseded_at=NULL,last_trace_id=NULLIF($3,''),last_audit_id=NULLIF($4,''),version=version+1,updated_at=now()
WHERE id=$1::uuid AND version=$2 AND state='validated' RETURNING `+identityRevisionColumns, id, expectedVersion, traceID, auditID))
	if err != nil {
		return identity.Revision{}, false, err
	}
	identityChanged := previousID != "" && (previousIssuer != nextIssuer || previousTenant != nextTenant || previousAudience != nextAudience || previousClient != nextClient || previousSessionRef != nextSessionRef)
	if identityChanged {
		if _, err := tx.Exec(ctx, `DELETE FROM gateway_oidc_sessions`); err != nil {
			return identity.Revision{}, false, err
		}
	}
	if _, err := tx.Exec(ctx, `DELETE FROM gateway_identity_pairing_start_reservation WHERE revision_id=$1::uuid`, id); err != nil {
		return identity.Revision{}, false, err
	}
	if err := tx.Commit(ctx); err != nil {
		return identity.Revision{}, false, err
	}
	return revision, identityChanged, nil
}

func (s *Store) DisableActiveIdentityRevision(ctx context.Context, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
	tx, err := s.beginIdentityConfigurationLifecycleTx(ctx)
	if err != nil {
		return identity.Revision{}, err
	}
	defer rollbackTransaction(tx)
	if ok, err := hasBreakGlassManager(ctx, tx); err != nil {
		return identity.Revision{}, err
	} else if !ok {
		return identity.Revision{}, identity.ErrBreakGlassRequired
	}
	var activeID, machineReference, sessionReference string
	if err := tx.QueryRow(ctx, `SELECT id::text,COALESCE(machine_credential_ref,''),COALESCE(session_encryption_key_ref,'')
FROM gateway_identity_configuration_revisions
WHERE state='active' AND version=$1 FOR UPDATE`, expectedVersion).Scan(
		&activeID, &machineReference, &sessionReference,
	); errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, identity.ErrRevisionConflict
	} else if err != nil {
		return identity.Revision{}, err
	}
	cleanupNotBefore := time.Now().UTC().Add(identityDisabledSecretCleanupDelay)
	references := make(map[string]struct{}, 2)
	for _, reference := range []string{machineReference, sessionReference} {
		if reference != "" {
			references[reference] = struct{}{}
		}
	}
	for reference := range references {
		if err := queueDisabledIdentitySecretCleanupTx(ctx, tx, reference, cleanupNotBefore); err != nil {
			return identity.Revision{}, err
		}
	}
	revision, err := scanIdentityRevision(tx.QueryRow(ctx, `UPDATE gateway_identity_configuration_revisions SET state='superseded',
superseded_at=now(),machine_credential_ref=NULL,session_encryption_key_ref=NULL,
last_trace_id=NULLIF($3,''),last_audit_id=NULLIF($4,''),version=version+1,updated_at=now()
WHERE id=$1::uuid AND state='active' AND version=$2
RETURNING `+identityRevisionColumns, activeID, expectedVersion, traceID, auditID))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	if err != nil {
		return identity.Revision{}, err
	}
	if _, err := tx.Exec(ctx, `DELETE FROM gateway_oidc_sessions`); err != nil {
		return identity.Revision{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return identity.Revision{}, err
	}
	return revision, nil
}

func (s *Store) HasBreakGlassManager(ctx context.Context) (bool, error) {
	return hasBreakGlassManager(ctx, s.pool)
}

func (s *Store) HasActiveTenantKey(ctx context.Context, tenantKey string) (bool, error) {
	var exists bool
	err := s.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway_tenants WHERE tenant_key=$1 AND status='active')`, tenantKey).Scan(&exists)
	return exists, err
}

func hasBreakGlassManager(ctx context.Context, query interface {
	QueryRow(context.Context, string, ...any) pgx.Row
}) (bool, error) {
	var exists bool
	err := query.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway_users WHERE source='gateway' AND status='active'
AND deleted_at IS NULL AND password_hash IS NOT NULL AND password_hash <> '' AND (roles ? 'manager' OR roles ? 'admin'))`).Scan(&exists)
	return exists, err
}

func scanIdentityRevision(row scanner) (identity.Revision, error) {
	var revision identity.Revision
	var state string
	var scopes, capabilities []byte
	if err := row.Scan(
		&revision.ID, &state, &revision.SchemaVersion, &revision.TenantMode, &revision.AuthCenterURL, &revision.Issuer, &revision.TenantID,
		&revision.ApplicationID, &revision.Audience, &revision.BrowserClientID, &revision.MachineClientID, &scopes,
		&capabilities, &revision.RolePrefix, &revision.LocalTenantKey, &revision.PublicBaseURL, &revision.WebBaseURL,
		&revision.JITEnabled, &revision.LegacyJWTEnabled, &revision.TokenIntrospection, &revision.SessionRevocation,
		&revision.SecurityEventIssuer, &revision.SecurityEventConfigURL, &revision.SecurityEventAudience,
		&revision.TenantContextEndpoint, &revision.TenantContextAudience, &revision.TenantContextScope,
		&revision.MachineCredentialRef, &revision.SessionEncryptionKeyRef, &revision.SessionIdleSeconds,
		&revision.SessionAbsoluteSeconds, &revision.SessionRefreshSeconds, &revision.Version, &revision.LastErrorCategory,
		&revision.LastTraceID, &revision.LastAuditID, &revision.ValidatedAt, &revision.ActivatedAt, &revision.SupersededAt,
		&revision.CreatedAt, &revision.UpdatedAt,
	); err != nil {
		return identity.Revision{}, err
	}
	revision.State = identity.RevisionState(state)
	_ = json.Unmarshal(scopes, &revision.Scopes)
	_ = json.Unmarshal(capabilities, &revision.Capabilities)
	if revision.Scopes == nil {
		revision.Scopes = []string{}
	}
	if revision.Capabilities == nil {
		revision.Capabilities = []string{}
	}
	return revision, nil
}

func normalizeIdentityRevisionError(err error) error {
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.ErrRevisionNotFound
	}
	return err
}
