package connector_oauth

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"sort"
	"strings"
	"time"

	"github.com/pingcap-inc/nexuscrm-connectors/internal/id"
)

const authorizationCleanupTimeout = 2 * time.Second

type Service struct {
	repository     oauthRepository
	cipher         SecretCipher
	providers      ProviderResolver
	policy         AuthorizationPolicy
	now            func() time.Time
	random         io.Reader
	authTTL        time.Duration
	refreshLease   time.Duration
	refreshSkew    time.Duration
	cleanupTimeout time.Duration
	audit          CleanupAuditEmitter
}

func NewServiceWithCleanupAudit(repository *SQLRepository, cipher SecretCipher, providers ProviderResolver, policy AuthorizationPolicy, audit CleanupAuditEmitter) (*Service, error) {
	if audit == nil {
		return nil, ErrDependencyMissing
	}
	service, err := NewService(repository, cipher, providers, policy)
	if err != nil {
		return nil, err
	}
	service.audit = audit
	return service, nil
}

func NewService(repository *SQLRepository, cipher SecretCipher, providers ProviderResolver, policy AuthorizationPolicy) (*Service, error) {
	if repository == nil {
		return nil, ErrDependencyMissing
	}
	return newService(repository, cipher, providers, policy)
}

func newService(repository oauthRepository, cipher SecretCipher, providers ProviderResolver, policy AuthorizationPolicy) (*Service, error) {
	if repository == nil || cipher == nil || providers == nil || policy == nil {
		return nil, ErrDependencyMissing
	}
	return &Service{
		repository: repository, cipher: cipher, providers: providers, policy: policy,
		now: time.Now, random: rand.Reader, authTTL: DefaultAuthorizationTTL,
		refreshLease: DefaultRefreshLeaseTTL, refreshSkew: 30 * time.Second,
		cleanupTimeout: authorizationCleanupTimeout,
	}, nil
}

func (s *Service) BeginAuthorization(ctx context.Context, request BeginAuthorizationRequest) (AuthorizationStart, error) {
	if s == nil || s.repository == nil || s.cipher == nil || s.providers == nil || s.policy == nil {
		return AuthorizationStart{}, ErrDependencyMissing
	}
	request.UserID = strings.TrimSpace(request.UserID)
	request.ConnectorKey = strings.TrimSpace(request.ConnectorKey)
	request.RedirectURI = strings.TrimSpace(request.RedirectURI)
	request.Scopes = normalizeScopes(request.Scopes)
	if request.UserID == "" || request.ConnectorKey == "" || request.RedirectURI == "" || len(request.Scopes) == 0 {
		return AuthorizationStart{}, ErrInvalidInput
	}
	if err := s.policy.ValidateRedirect(request.ConnectorKey, request.RedirectURI); err != nil {
		return AuthorizationStart{}, ErrAuthorizationDenied
	}
	if err := s.policy.ValidateScopes(request.ConnectorKey, request.Scopes); err != nil {
		return AuthorizationStart{}, ErrAuthorizationDenied
	}
	provider, ok := s.providers.Provider(request.ConnectorKey)
	if !ok || provider == nil {
		return AuthorizationStart{}, ErrAuthorizationDenied
	}
	state, err := randomURLString(s.random, 32)
	if err != nil {
		return AuthorizationStart{}, fmt.Errorf("generate authorization state: %w", err)
	}
	verifier, err := randomURLString(s.random, 48)
	if err != nil {
		return AuthorizationStart{}, fmt.Errorf("generate proof key: %w", err)
	}
	now := s.now().UTC()
	record := authorizationRecord{
		ID: id.Generate(), StateDigest: digestString(state), UserID: request.UserID,
		ConnectorKey: request.ConnectorKey, RedirectURI: request.RedirectURI,
		RequestedScopes: strings.Join(request.Scopes, " "), ExpiresAt: now.Add(s.authTTL),
		CreatedDate: now, LastModifiedDate: now,
	}
	reservation, err := s.repository.reserveAuthorization(ctx, record, func(expectedGeneration int64) (string, error) {
		ciphertext, _, err := s.cipher.Seal([]byte(verifier), authorizationSecretContext(record, expectedGeneration))
		return ciphertext, err
	})
	if err != nil {
		return AuthorizationStart{}, err
	}
	if !reservation.localCommitConfirmed || reservation.record == nil {
		return AuthorizationStart{}, ErrOwnershipLost
	}
	if reservation.postCommitCleanupFailed != nil {
		return AuthorizationStart{}, s.releaseAuthorization(*reservation.record, false, reservation.postCommitCleanupFailed)
	}
	reserved := reservation.record
	if reserved.OAuthEffectState == EffectExchangeResultStaged {
		pending, err := s.openAuthorizationResult(*reserved)
		if err != nil {
			if deterministicSecretFailure(err) {
				manualErr := s.repository.markStagedAuthorizationManual(ctx, *reserved, s.now().UTC())
				return AuthorizationStart{}, errors.Join(err, ErrManualActionRequired, manualErr)
			}
			return AuthorizationStart{}, err
		}
		defer clearTokenSet(&pending)
		// The released staged-result format cannot distinguish omitted scope from
		// an explicit empty grant. Preserve unscoped recovery without claiming a
		// grant; only non-empty staged scopes can satisfy the requested set.
		if len(pending.Scopes) > 0 && !scopesContain(pending.Scopes, strings.Fields(reserved.RequestedScopes)) {
			manualErr := s.repository.markStagedAuthorizationManual(ctx, *reserved, s.now().UTC())
			return AuthorizationStart{}, errors.Join(ErrScopeMismatch, ErrManualActionRequired, manualErr)
		}
		identity, err := provider.Identity(ctx, pending.AccessToken)
		if err != nil {
			safeErr := safeProviderError("identity", err)
			if providerFailureRequiresManual(safeErr) {
				_ = s.repository.markStagedAuthorizationManual(ctx, *reserved, s.now().UTC())
				return AuthorizationStart{}, errors.Join(safeErr, ErrManualActionRequired)
			}
			return AuthorizationStart{}, safeErr
		}
		identity = normalizeIdentity(identity)
		if identity.Issuer == "" || identity.Subject == "" || identity.PrincipalAddress == "" {
			_ = s.repository.markStagedAuthorizationManual(ctx, *reserved, s.now().UTC())
			return AuthorizationStart{}, errors.Join(ErrInvalidInput, ErrManualActionRequired)
		}
		principal, err := s.repository.finalizeAuthorization(ctx, *reserved, identity, id.Generate(), now, func(principal ConnectedPrincipal) (tokenMaterial, error) {
			return s.encryptTokens(principal, pending)
		})
		if err != nil {
			if errors.Is(err, ErrPrincipalOwnership) || errors.Is(err, ErrGenerationMismatch) || errors.Is(err, ErrAuthorizationDenied) {
				_ = s.repository.markStagedAuthorizationManual(ctx, *reserved, s.now().UTC())
				return AuthorizationStart{}, errors.Join(err, ErrManualActionRequired)
			}
			return AuthorizationStart{}, err
		}
		return AuthorizationStart{Outcome: AuthorizationOutcomeConnected, Principal: principal,
			ExpectedGeneration: principal.Generation}, nil
	}
	if reserved.OAuthEffectState != EffectNone {
		return AuthorizationStart{}, ErrEffectStateInvalid
	}
	challenge := sha256.Sum256([]byte(verifier))
	authorizationURL, err := provider.AuthorizationURL(AuthorizationURLRequest{
		State: state, CodeChallenge: base64.RawURLEncoding.EncodeToString(challenge[:]),
		ChallengeMethod: "S256", RedirectURI: request.RedirectURI,
		Scopes: append([]string(nil), request.Scopes...),
	})
	if err != nil {
		return AuthorizationStart{}, s.releaseAuthorization(*reserved, false, safeProviderError("authorization", err))
	}
	if strings.TrimSpace(authorizationURL) == "" {
		return AuthorizationStart{}, s.releaseAuthorization(*reserved, false, ErrAuthorizationDenied)
	}
	return AuthorizationStart{URL: authorizationURL, ExpiresAt: reserved.ExpiresAt,
		ExpectedGeneration: reserved.ExpectedGeneration, Outcome: AuthorizationOutcomeRedirect}, nil
}

func (s *Service) CompleteAuthorization(ctx context.Context, request CompleteAuthorizationRequest) (AuthorizationCompletion, error) {
	if s == nil || s.repository == nil || s.cipher == nil || s.providers == nil {
		return AuthorizationCompletion{}, ErrDependencyMissing
	}
	request.UserID = strings.TrimSpace(request.UserID)
	request.State = strings.TrimSpace(request.State)
	request.Code = strings.TrimSpace(request.Code)
	if request.UserID == "" || request.State == "" || request.Code == "" {
		return AuthorizationCompletion{}, ErrInvalidInput
	}
	now := s.now().UTC()
	ownerID, err := randomURLString(s.random, 24)
	if err != nil {
		return AuthorizationCompletion{}, fmt.Errorf("generate exchange owner: %w", err)
	}
	claim, err := s.repository.claimAuthorizationExchange(ctx, digestString(request.State), request.UserID,
		ownerID, now, now.Add(s.refreshLease))
	if err != nil {
		return AuthorizationCompletion{}, err
	}
	if claim.Record.OAuthEffectState == EffectExchangeResultStaged {
		return AuthorizationCompletion{Outcome: AuthorizationCompletionResultStaged}, nil
	}
	provider, ok := s.providers.Provider(claim.Record.ConnectorKey)
	if !ok || provider == nil {
		return AuthorizationCompletion{}, ErrAuthorizationDenied
	}
	verifier, err := s.cipher.Open(claim.Record.CodeVerifierCiphertext, authorizationSecretContext(claim.Record, claim.Record.ExpectedGeneration))
	if err != nil {
		return AuthorizationCompletion{}, err
	}
	defer clear(verifier)
	providerCtx, cancelProvider, err := s.providerEffectContext(ctx, provider, claim.Record.OAuthActionLeaseExpiresAt)
	if err != nil {
		return AuthorizationCompletion{}, err
	}
	defer cancelProvider()
	if err := s.repository.markAuthorizationExchangeDispatched(ctx, *claim, now); err != nil {
		return AuthorizationCompletion{}, err
	}
	tokens, err := provider.Exchange(providerCtx, TokenExchangeRequest{Code: request.Code, CodeVerifier: string(verifier), RedirectURI: claim.Record.RedirectURI})
	completionCtx, cancelCompletion := s.localCompletionContext(ctx)
	defer cancelCompletion()
	if err != nil {
		safeErr := safeProviderError("exchange", err)
		manualErr := s.repository.markAuthorizationManual(completionCtx, *claim, s.now().UTC())
		return AuthorizationCompletion{}, errors.Join(safeErr, ErrManualActionRequired, manualErr)
	}
	defer clearTokenSet(&tokens)
	if err := validateTokenSet(tokens); err != nil {
		manualErr := s.repository.markAuthorizationManual(completionCtx, *claim, s.now().UTC())
		return AuthorizationCompletion{}, errors.Join(err, ErrManualActionRequired, manualErr)
	}
	requestedScopes := strings.Fields(claim.Record.RequestedScopes)
	if tokens.Scopes == nil {
		tokens.Scopes = append([]string(nil), requestedScopes...)
	}
	tokens.Scopes = normalizeScopes(tokens.Scopes)
	if !scopesContain(tokens.Scopes, requestedScopes) {
		manualErr := s.repository.markAuthorizationManual(completionCtx, *claim, s.now().UTC())
		return AuthorizationCompletion{}, errors.Join(ErrScopeMismatch, ErrManualActionRequired, manualErr)
	}
	payload, err := marshalPendingTokenSet(tokens)
	if err != nil {
		manualErr := s.repository.markAuthorizationManual(completionCtx, *claim, s.now().UTC())
		return AuthorizationCompletion{}, errors.Join(err, ErrManualActionRequired, manualErr)
	}
	ciphertext, _, err := s.cipher.Seal(payload, authorizationResultContext(claim.Record, claim.Epoch))
	clear(payload)
	if err != nil {
		manualErr := s.repository.markAuthorizationManual(completionCtx, *claim, s.now().UTC())
		return AuthorizationCompletion{}, errors.Join(err, ErrManualActionRequired, manualErr)
	}
	if err := s.repository.stageAuthorizationResult(completionCtx, *claim, ciphertext, s.now().UTC()); err != nil {
		staged, reconcileErr := s.repository.reconcileAuthorizationStage(completionCtx, *claim, ciphertext, s.now().UTC())
		if reconcileErr == nil && staged {
			return AuthorizationCompletion{Outcome: AuthorizationCompletionResultStaged}, nil
		}
		var manualErr error
		if reconcileErr != nil {
			manualErr = s.repository.markAuthorizationManual(completionCtx, *claim, s.now().UTC())
		}
		return AuthorizationCompletion{}, errors.Join(err, ErrManualActionRequired, reconcileErr, manualErr)
	}
	return AuthorizationCompletion{Outcome: AuthorizationCompletionResultStaged}, nil
}

func (s *Service) Principal(ctx context.Context, userID, connectorKey string) (*ConnectedPrincipal, error) {
	if s == nil || s.repository == nil {
		return nil, ErrDependencyMissing
	}
	userID, connectorKey = strings.TrimSpace(userID), strings.TrimSpace(connectorKey)
	if userID == "" || connectorKey == "" {
		return nil, ErrInvalidInput
	}
	principal, err := s.repository.loadPrincipal(ctx, userID, connectorKey)
	if err != nil {
		return nil, err
	}
	if principal == nil {
		return nil, ErrPrincipalNotFound
	}
	return principal, nil
}

func (s *Service) Status(ctx context.Context, userID, connectorKey string) (EffectStatus, error) {
	if s == nil || s.repository == nil {
		return EffectStatus{}, ErrDependencyMissing
	}
	userID, connectorKey = strings.TrimSpace(userID), strings.TrimSpace(connectorKey)
	if userID == "" || connectorKey == "" {
		return EffectStatus{}, ErrInvalidInput
	}
	return s.repository.loadEffectStatus(ctx, userID, connectorKey, s.now().UTC())
}

func (s *Service) AttestExternalCleanup(ctx context.Context, request CleanupAttestation) error {
	if s == nil || s.repository == nil || s.audit == nil {
		return ErrDependencyMissing
	}
	request.UserID = strings.TrimSpace(request.UserID)
	request.ConnectorKey = strings.TrimSpace(request.ConnectorKey)
	request.Reason = strings.TrimSpace(request.Reason)
	if request.UserID == "" || request.ConnectorKey == "" || request.Reason == "" {
		return ErrInvalidInput
	}
	return s.repository.attestExternalCleanup(ctx, request, s.now().UTC(), s.audit)
}

// ResolveAccessToken linearizes at the committed pessimistic read of the User,
// principal, and token rows. A stale or uncertain generation never returns a
// plaintext token.
func (s *Service) ResolveAccessToken(ctx context.Context, request ResolveAccessTokenRequest) (string, error) {
	return s.resolveAccessToken(ctx, request, nil)
}

// ResolveScopedAccessToken returns a token only when the committed generation
// contains every required scope. The same constraint is enforced if resolving
// the token resumes or starts a refresh.
func (s *Service) ResolveScopedAccessToken(ctx context.Context, request ResolveScopedAccessTokenRequest) (string, error) {
	if s == nil || s.repository == nil || s.cipher == nil || s.providers == nil {
		return "", ErrDependencyMissing
	}
	requiredScopes := normalizeScopes(request.RequiredScopes)
	if len(requiredScopes) == 0 {
		return "", ErrInvalidInput
	}
	return s.resolveAccessToken(ctx, ResolveAccessTokenRequest{
		UserID:             request.UserID,
		ConnectorKey:       request.ConnectorKey,
		ExpectedGeneration: request.ExpectedGeneration,
	}, requiredScopes)
}

func (s *Service) resolveAccessToken(ctx context.Context, request ResolveAccessTokenRequest, requiredScopes []string) (string, error) {
	if s == nil || s.repository == nil || s.cipher == nil || s.providers == nil {
		return "", ErrDependencyMissing
	}
	if err := validateResolveRequest(request); err != nil {
		return "", err
	}
	principal, token, err := s.repository.resolveToken(ctx, strings.TrimSpace(request.UserID), strings.TrimSpace(request.ConnectorKey), request.ExpectedGeneration, s.now().UTC())
	if err != nil {
		return "", err
	}
	if token.OAuthEffectState == EffectRefreshResultStaged {
		return s.finalizePendingRefresh(ctx, *principal, *token, requiredScopes)
	}
	if !scopesContain(strings.Fields(token.GrantedScopes), requiredScopes) {
		return "", ErrScopeMismatch
	}
	if token.OAuthEffectState == EffectRefreshDispatched {
		return s.refresh(ctx, request, requiredScopes)
	}
	if token.ExpiresAt != nil && !token.ExpiresAt.After(s.now().UTC().Add(s.refreshSkew)) {
		return s.refresh(ctx, request, requiredScopes)
	}
	plaintext, err := s.cipher.Open(token.AccessTokenCiphertext, principalSecretContext(*principal, SecretPrincipalAccessToken))
	if err != nil {
		return "", err
	}
	defer clear(plaintext)
	return string(plaintext), nil
}

func (s *Service) Refresh(ctx context.Context, request ResolveAccessTokenRequest) (string, error) {
	return s.refresh(ctx, request, nil)
}

func (s *Service) refresh(ctx context.Context, request ResolveAccessTokenRequest, requiredScopes []string) (string, error) {
	if s == nil || s.repository == nil || s.cipher == nil || s.providers == nil {
		return "", ErrDependencyMissing
	}
	if err := validateResolveRequest(request); err != nil {
		return "", err
	}
	userID := strings.TrimSpace(request.UserID)
	connectorKey := strings.TrimSpace(request.ConnectorKey)
	principal, token, err := s.repository.resolveToken(ctx, userID, connectorKey, request.ExpectedGeneration, s.now().UTC())
	if err != nil {
		return "", err
	}
	if token.OAuthEffectState == EffectRefreshResultStaged {
		return s.finalizePendingRefresh(ctx, *principal, *token, requiredScopes)
	}
	provider, ok := s.providers.Provider(connectorKey)
	if !ok || provider == nil {
		return "", ErrAuthorizationDenied
	}
	ownerID, err := randomURLString(s.random, 24)
	if err != nil {
		return "", fmt.Errorf("generate refresh owner: %w", err)
	}
	now := s.now().UTC()
	claim, err := s.repository.claimRefresh(ctx, userID, connectorKey, request.ExpectedGeneration, ownerID, now, now.Add(s.refreshLease))
	if err != nil {
		return "", err
	}
	if claim.Token.OAuthEffectState == EffectRefreshResultStaged {
		return s.finalizePendingRefresh(ctx, claim.Principal, claim.Token, requiredScopes)
	}
	refreshToken, err := s.cipher.Open(claim.Token.RefreshTokenCiphertext, principalSecretContext(claim.Principal, SecretPrincipalRefreshToken))
	if err != nil {
		if deterministicSecretFailure(err) {
			manualErr := s.repository.markRefreshManual(ctx, *claim, s.now().UTC())
			return "", errors.Join(err, ErrManualActionRequired, manualErr)
		}
		return "", err
	}
	defer clear(refreshToken)
	providerCtx, cancelProvider, err := s.providerEffectContext(ctx, provider, claim.Token.RefreshLeaseExpiresAt)
	if err != nil {
		return "", err
	}
	defer cancelProvider()
	if err := s.repository.markRefreshDispatched(ctx, *claim, s.now().UTC()); err != nil {
		return "", err
	}
	refreshed, providerErr := provider.Refresh(providerCtx, string(refreshToken))
	completionCtx, cancelCompletion := s.localCompletionContext(ctx)
	defer cancelCompletion()
	if providerErr != nil {
		safeErr := safeProviderError("refresh", providerErr)
		manualErr := s.repository.markRefreshManual(completionCtx, *claim, s.now().UTC())
		return "", errors.Join(safeErr, ErrManualActionRequired, manualErr)
	}
	defer clearTokenSet(&refreshed)
	if err := validateTokenSet(refreshed); err != nil {
		manualErr := s.repository.markRefreshManual(completionCtx, *claim, s.now().UTC())
		return "", errors.Join(err, ErrManualActionRequired, manualErr)
	}
	committedScopes := strings.Fields(claim.Token.GrantedScopes)
	if refreshed.Scopes == nil {
		refreshed.Scopes = append([]string(nil), committedScopes...)
	}
	refreshed.Scopes = normalizeScopes(refreshed.Scopes)
	if !scopesContain(refreshed.Scopes, committedScopes) ||
		!scopesContain(refreshed.Scopes, requiredScopes) {
		manualErr := s.repository.markRefreshManual(completionCtx, *claim, s.now().UTC())
		return "", errors.Join(ErrScopeMismatch, ErrManualActionRequired, manualErr)
	}
	payload, err := marshalPendingTokenSet(refreshed)
	if err != nil {
		manualErr := s.repository.markRefreshManual(completionCtx, *claim, s.now().UTC())
		return "", errors.Join(err, ErrManualActionRequired, manualErr)
	}
	ciphertext, _, err := s.cipher.Seal(payload, refreshResultContext(claim.Principal, claim.Token))
	clear(payload)
	if err != nil {
		manualErr := s.repository.markRefreshManual(completionCtx, *claim, s.now().UTC())
		return "", errors.Join(err, ErrManualActionRequired, manualErr)
	}
	if err := s.repository.stageRefreshResult(completionCtx, *claim, ciphertext, s.now().UTC()); err != nil {
		staged, reconcileErr := s.repository.reconcileRefreshStage(completionCtx, *claim, ciphertext, s.now().UTC())
		if reconcileErr == nil && staged {
			return "", ErrEffectResultStaged
		}
		manualErr := s.repository.markRefreshManual(completionCtx, *claim, s.now().UTC())
		return "", errors.Join(err, ErrManualActionRequired, reconcileErr, manualErr)
	}
	return "", ErrEffectResultStaged
}

func (s *Service) Revoke(ctx context.Context, userID, connectorKey string) error {
	return s.revokeAndSetState(ctx, userID, connectorKey, PrincipalReconnectRequired)
}

func (s *Service) Disconnect(ctx context.Context, userID, connectorKey string) error {
	return s.revokeAndSetState(ctx, userID, connectorKey, PrincipalDisconnected)
}

func (s *Service) revokeAndSetState(ctx context.Context, userID, connectorKey, state string) error {
	if s == nil || s.repository == nil || s.cipher == nil || s.providers == nil {
		return ErrDependencyMissing
	}
	userID, connectorKey = strings.TrimSpace(userID), strings.TrimSpace(connectorKey)
	if userID == "" || connectorKey == "" {
		return ErrInvalidInput
	}
	principal, err := s.repository.loadPrincipal(ctx, userID, connectorKey)
	if err != nil {
		return err
	}
	if principal == nil {
		return ErrPrincipalNotFound
	}
	if principal.State == PrincipalConnected {
		resolvedPrincipal, token, resolveErr := s.repository.resolveToken(ctx, userID, connectorKey, principal.Generation, s.now().UTC())
		if resolveErr == nil && token.OAuthEffectState == EffectRefreshResultStaged {
			_, finalizeErr := s.finalizePendingRefresh(ctx, *resolvedPrincipal, *token, nil)
			if finalizeErr != nil {
				return finalizeErr
			}
		} else if errors.Is(resolveErr, ErrRefreshInProgress) {
			return resolveErr
		} else if resolveErr != nil && !errors.Is(resolveErr, ErrReconnectRequired) {
			return resolveErr
		}
	}
	outcome, err := s.repository.disconnect(ctx, userID, connectorKey, principal.Generation, state, s.now().UTC())
	if err != nil {
		return err
	}
	if !outcome.localCommitConfirmed {
		return ErrOwnershipLost
	}
	if outcome.token == nil {
		return outcome.postCommitCleanupFailed
	}
	ownerID, err := randomURLString(s.random, 24)
	if err != nil {
		return joinDisconnectErrors(err, outcome.postCommitCleanupFailed)
	}
	now := s.now().UTC()
	claim, err := s.repository.claimRevoke(ctx, userID, connectorKey, ownerID, now, now.Add(s.refreshLease))
	if err != nil {
		return joinDisconnectErrors(err, outcome.postCommitCleanupFailed)
	}
	if claim == nil {
		return outcome.postCommitCleanupFailed
	}
	if claim.Token.OAuthEffectState == EffectRevokeSucceeded {
		return joinDisconnectErrors(s.repository.deleteRevokedToken(ctx, userID, connectorKey, claim.Principal.ID,
			claim.Token.PrincipalGeneration, claim.Token.TokenVersion), outcome.postCommitCleanupFailed)
	}
	ciphertext := claim.Token.AccessTokenCiphertext
	purpose := SecretPrincipalAccessToken
	if claim.Token.RefreshTokenCiphertext != "" {
		ciphertext = claim.Token.RefreshTokenCiphertext
		purpose = SecretPrincipalRefreshToken
	}
	oldPrincipal := claim.Principal
	oldPrincipal.Generation = claim.Token.PrincipalGeneration
	plaintext, err := s.cipher.Open(ciphertext, principalSecretContext(oldPrincipal, purpose))
	if err != nil {
		if deterministicSecretFailure(err) {
			manualErr := s.repository.markRevokeManual(ctx, *claim, s.now().UTC())
			return joinDisconnectErrors(errors.Join(err, ErrManualActionRequired, manualErr), outcome.postCommitCleanupFailed)
		}
		return joinDisconnectErrors(err, outcome.postCommitCleanupFailed)
	}
	defer clear(plaintext)
	provider, ok := s.providers.Provider(claim.Principal.ConnectorKey)
	if !ok || provider == nil {
		return joinDisconnectErrors(ErrAuthorizationDenied, outcome.postCommitCleanupFailed)
	}
	providerCtx, cancelProvider, err := s.providerEffectContext(ctx, provider, claim.Token.RefreshLeaseExpiresAt)
	if err != nil {
		return joinDisconnectErrors(err, outcome.postCommitCleanupFailed)
	}
	defer cancelProvider()
	if err := s.repository.markRevokeDispatched(ctx, *claim, now); err != nil {
		return joinDisconnectErrors(err, outcome.postCommitCleanupFailed)
	}
	providerErr := provider.Revoke(providerCtx, string(plaintext))
	completionCtx, cancelCompletion := s.localCompletionContext(ctx)
	defer cancelCompletion()
	if providerErr != nil {
		manualErr := s.repository.markRevokeManual(completionCtx, *claim, s.now().UTC())
		return joinDisconnectErrors(errors.Join(safeProviderError("revoke", providerErr), ErrManualActionRequired, manualErr), outcome.postCommitCleanupFailed)
	}
	if err := s.repository.markRevokeSucceeded(completionCtx, *claim, s.now().UTC()); err != nil {
		manualErr := s.repository.markRevokeManual(completionCtx, *claim, s.now().UTC())
		return joinDisconnectErrors(errors.Join(err, ErrManualActionRequired, manualErr), outcome.postCommitCleanupFailed)
	}
	return joinDisconnectErrors(s.repository.deleteRevokedToken(completionCtx, userID, connectorKey, claim.Principal.ID,
		claim.Token.PrincipalGeneration, claim.Token.TokenVersion), outcome.postCommitCleanupFailed)
}

func (s *Service) localCompletionContext(ctx context.Context) (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.WithoutCancel(ctx), s.cleanupTimeout)
}

func (s *Service) providerEffectContext(ctx context.Context, provider OAuthProvider, leaseExpiresAt *time.Time) (context.Context, context.CancelFunc, error) {
	if leaseExpiresAt == nil || s.refreshLease <= 0 || s.cleanupTimeout <= 0 {
		return nil, nil, ErrEffectStateInvalid
	}
	remaining := s.refreshLease - s.cleanupTimeout
	if remaining <= s.cleanupTimeout {
		return nil, nil, ErrEffectStateInvalid
	}
	providerBudget := remaining - s.cleanupTimeout
	terminationBound := provider.EffectTerminationBound()
	if terminationBound <= 0 || terminationBound >= providerBudget {
		return nil, nil, ErrEffectStateInvalid
	}
	deadline := leaseExpiresAt.Add(-terminationBound).Add(-s.cleanupTimeout).Add(-s.cleanupTimeout)
	if !deadline.After(s.now().UTC()) {
		return nil, nil, ErrAuthorizationProgress
	}
	providerCtx, cancel := context.WithDeadline(ctx, deadline)
	return providerCtx, cancel, nil
}

func (s *Service) openAuthorizationResult(record authorizationRecord) (TokenSet, error) {
	payload, err := s.cipher.Open(record.PendingTokenCiphertext, authorizationResultContext(record, record.OAuthActionEpoch))
	if err != nil {
		return TokenSet{}, err
	}
	defer clear(payload)
	return unmarshalPendingTokenSet(payload)
}

func (s *Service) finalizePendingRefresh(
	ctx context.Context,
	principal ConnectedPrincipal,
	token tokenRecord,
	requiredScopes []string,
) (string, error) {
	committedScopes := strings.Fields(token.GrantedScopes)
	if !scopesContain(committedScopes, requiredScopes) {
		return "", ErrScopeMismatch
	}
	payload, err := s.cipher.Open(token.PendingTokenCiphertext, refreshResultContext(principal, token))
	if err != nil {
		if deterministicSecretFailure(err) {
			manualErr := s.repository.markStagedRefreshManual(ctx, principal, token, s.now().UTC())
			return "", errors.Join(err, ErrManualActionRequired, manualErr)
		}
		return "", err
	}
	defer clear(payload)
	tokens, err := unmarshalPendingTokenSet(payload)
	if err != nil {
		if deterministicSecretFailure(err) {
			manualErr := s.repository.markStagedRefreshManual(ctx, principal, token, s.now().UTC())
			return "", errors.Join(err, ErrManualActionRequired, manualErr)
		}
		return "", err
	}
	defer clearTokenSet(&tokens)
	// The released staged-result format cannot distinguish omitted scope from
	// an explicit empty grant. An unscoped legacy consumer may still recover the
	// token, but an empty staged set must never satisfy a scoped resolution.
	if len(tokens.Scopes) == 0 && len(requiredScopes) > 0 {
		return "", ErrScopeMismatch
	}
	if len(tokens.Scopes) > 0 && !scopesContain(tokens.Scopes, committedScopes) {
		manualErr := s.repository.markStagedRefreshManual(ctx, principal, token, s.now().UTC())
		return "", errors.Join(ErrScopeMismatch, ErrManualActionRequired, manualErr)
	}
	if tokens.RefreshToken == "" {
		refreshToken, err := s.cipher.Open(token.RefreshTokenCiphertext, principalSecretContext(principal, SecretPrincipalRefreshToken))
		if err != nil {
			if deterministicSecretFailure(err) {
				manualErr := s.repository.markStagedRefreshManual(ctx, principal, token, s.now().UTC())
				return "", errors.Join(err, ErrManualActionRequired, manualErr)
			}
			return "", err
		}
		tokens.RefreshToken = string(refreshToken)
		clear(refreshToken)
	}
	material, err := s.encryptTokens(principal, tokens)
	if err != nil {
		return "", err
	}
	if err := s.repository.finalizeStagedRefresh(ctx, principal, token, material, s.now().UTC()); err != nil {
		return "", err
	}
	return tokens.AccessToken, nil
}

func joinDisconnectErrors(primary, postCommitCleanup error) error {
	if postCommitCleanup == nil {
		return primary
	}
	if primary == nil {
		return postCommitCleanup
	}
	return errors.Join(primary, postCommitCleanup)
}

func (s *Service) encryptTokens(principal ConnectedPrincipal, tokens TokenSet) (tokenMaterial, error) {
	accessCiphertext, keyID, err := s.cipher.Seal([]byte(tokens.AccessToken), principalSecretContext(principal, SecretPrincipalAccessToken))
	if err != nil {
		return tokenMaterial{}, err
	}
	refreshCiphertext := ""
	if tokens.RefreshToken != "" {
		refreshCiphertext, _, err = s.cipher.Seal([]byte(tokens.RefreshToken), principalSecretContext(principal, SecretPrincipalRefreshToken))
		if err != nil {
			return tokenMaterial{}, err
		}
	}
	return tokenMaterial{AccessCiphertext: accessCiphertext, RefreshCiphertext: refreshCiphertext,
		TokenType: strings.TrimSpace(tokens.TokenType), GrantedScopes: strings.Join(normalizeScopes(tokens.Scopes), " "),
		ExpiresAt: tokens.ExpiresAt, EncryptionKeyID: keyID}, nil
}

func (s *Service) releaseAuthorization(record authorizationRecord, consumed bool, originalErr error) error {
	cleanupCtx, cancel := context.WithTimeout(context.Background(), s.cleanupTimeout)
	defer cancel()
	outcome, err := s.repository.releaseAuthorization(cleanupCtx, record, consumed)
	if err != nil || !outcome.deleteCommitConfirmed {
		return errors.Join(originalErr, ErrAuthorizationCleanupPending)
	}
	return originalErr
}

func providerFailureRequiresManual(err error) bool {
	var providerErr *ProviderError
	if !errors.As(err, &providerErr) {
		return false
	}
	return providerErr.Kind == ProviderErrorAuthorization || providerErr.Kind == ProviderErrorRejected || providerErr.Kind == ProviderErrorUncertain
}

func deterministicSecretFailure(err error) bool {
	return errors.Is(err, ErrCiphertextInvalid) || errors.Is(err, ErrInvalidInput)
}

func clearTokenSet(tokens *TokenSet) {
	if tokens == nil {
		return
	}
	tokens.AccessToken = ""
	tokens.RefreshToken = ""
}

func authorizationSecretContext(record authorizationRecord, expectedGeneration int64) SecretContext {
	return SecretContext{Purpose: SecretAuthorizationVerifier, UserID: record.UserID,
		ConnectorKey: record.ConnectorKey, RecordID: record.ID, Generation: expectedGeneration}
}

func authorizationResultContext(record authorizationRecord, epoch int64) SecretContext {
	return SecretContext{Purpose: SecretAuthorizationResult, UserID: record.UserID,
		ConnectorKey: record.ConnectorKey, RecordID: record.ID, Generation: record.ExpectedGeneration, Version: epoch}
}

func refreshResultContext(principal ConnectedPrincipal, token tokenRecord) SecretContext {
	return SecretContext{Purpose: SecretRefreshResult, UserID: principal.UserID,
		ConnectorKey: principal.ConnectorKey, RecordID: principal.ID,
		Generation: token.PrincipalGeneration, Version: token.TokenVersion}
}

func principalSecretContext(principal ConnectedPrincipal, purpose SecretPurpose) SecretContext {
	return SecretContext{Purpose: purpose, UserID: principal.UserID, ConnectorKey: principal.ConnectorKey,
		RecordID: principal.ID, Generation: principal.Generation}
}

func validateResolveRequest(request ResolveAccessTokenRequest) error {
	if strings.TrimSpace(request.UserID) == "" || strings.TrimSpace(request.ConnectorKey) == "" || request.ExpectedGeneration < 1 {
		return ErrInvalidInput
	}
	return nil
}

func scopesContain(granted, required []string) bool {
	granted = normalizeScopes(granted)
	required = normalizeScopes(required)
	available := make(map[string]struct{}, len(granted))
	for _, scope := range granted {
		available[scope] = struct{}{}
	}
	for _, scope := range required {
		if _, ok := available[scope]; !ok {
			return false
		}
	}
	return true
}

func validateTokenSet(tokens TokenSet) error {
	if strings.TrimSpace(tokens.AccessToken) == "" || strings.TrimSpace(tokens.TokenType) == "" {
		return ErrInvalidInput
	}
	return nil
}

func normalizeIdentity(identity RemoteIdentity) RemoteIdentity {
	identity.Issuer = strings.TrimSpace(identity.Issuer)
	identity.Subject = strings.TrimSpace(identity.Subject)
	identity.PrincipalAddress = strings.TrimSpace(identity.PrincipalAddress)
	identity.DisplayName = strings.TrimSpace(identity.DisplayName)
	return identity
}

func randomURLString(reader io.Reader, byteCount int) (string, error) {
	if reader == nil || byteCount < 16 {
		return "", ErrInvalidInput
	}
	buffer := make([]byte, byteCount)
	if _, err := io.ReadFull(reader, buffer); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(buffer), nil
}

func digestString(value string) string {
	digest := sha256.Sum256([]byte(value))
	return fmt.Sprintf("%x", digest[:])
}

func normalizeScopes(scopes []string) []string {
	seen := make(map[string]struct{}, len(scopes))
	normalized := make([]string, 0, len(scopes))
	for _, scope := range scopes {
		scope = strings.TrimSpace(scope)
		if scope == "" {
			continue
		}
		if _, exists := seen[scope]; exists {
			continue
		}
		seen[scope] = struct{}{}
		normalized = append(normalized, scope)
	}
	sort.Strings(normalized)
	return normalized
}
