// Package credentialops provides a principal-authenticated, in-process
// management service for credential slots and routes. It sits between the
// wire layer and the credentialstore and enforces three invariants that the
// store alone cannot guarantee:
//
//  1. Authentication — every request is bound to a principal resolved from a
//     presented IOP token digest. The raw token is hashed inside the service and never passed to the store.
//
//  2. Target ownership — callers may only mutate resources that belong to
//     their own principal. Cross-principal references are rejected.
//
//  3. Secret blindness — the service accepts plaintext secrets at the
//     method boundary, hands an opaque encrypted envelope to the store, and
//     never exposes raw secret material in responses, logs, or errors.
//
// The service is transport-neutral. It does not register listeners, declare
// protobuf messages, or depend on any network adapter. The production
// composition may supply a SecretSealer; mutating methods that touch secret
// material fail closed when the sealer is unset.
package credentialops

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/google/uuid"
	"go.uber.org/zap"

	"iop/apps/control-plane/internal/credentialseal"
	"iop/apps/control-plane/internal/credentialstore"
)

// SecretSealer is the injected boundary for at-rest secret encryption.
// Implementations must produce an opaque ciphertext envelope that the
// credentialstore persists; the raw plaintext must never leave the method
// boundary. Mutating methods that require one return ErrSealerUnavailable
// when composition leaves it unset.
//
// The seal context is the lower-layer credentialseal.Context so this service
// depends on the crypto primitive rather than the reverse, avoiding any layer
// inversion.
type SecretSealer interface {
	Seal(ctx context.Context, plaintext []byte, sealCtx credentialseal.Context) (credentialstore.SecretEnvelope, error)
}

// ErrSealerUnavailable is returned by mutating methods when no SecretSealer
// is configured on the service.
var ErrSealerUnavailable = errors.New("credentialops: sealer is not configured")

// ErrInvalidCredentialKind is returned when the caller supplies a
// credential kind that is not one of the recognized canonical values.
var ErrInvalidCredentialKind = errors.New("credentialops: invalid credential kind")

// ErrUnauthorized is returned when the presented IOP token does not resolve
// to an active principal or when the caller targets a resource that does
// not belong to the resolved principal.
var ErrUnauthorized = errors.New("credentialops: unauthorized")

// ErrNotFound is returned when a requested slot or route is not found.
var ErrNotFound = errors.New("credentialops: not found")

// ErrStaleRevision is returned when a CAS operation fails because the
// caller's revision does not match the current row revision.
var ErrStaleRevision = errors.New("credentialops: stale revision")

// ErrSecretHandlingFailed is returned when secret sealing, unsealing, or envelope validation fails.
// It ensures sensitive sealer error strings or key metadata never leak across the boundary.
var ErrSecretHandlingFailed = errors.New("credentialops: secret handling failed")

// IssuedToken pairs the token DTO with the one-time raw token string returned upon token creation.
type IssuedToken struct {
	Token    TokenRecord
	RawToken string
}

// SlotRecord is the secret-blind DTO returned by slot queries. It exposes
// only metadata; the plaintext IOP token, provider secret, and opaque
// encrypted envelope are never present.
type SlotRecord struct {
	ID             string
	Vendor         string
	CredentialKind string
	Alias          string
	Status         string
	Revision       int64
	CreatedAt      time.Time
	UpdatedAt      time.Time
	RevokedAt      *time.Time
}

// RouteRecord is the DTO returned by route queries. It is a thin projection
// of the credentialstore.RouteRecord with no secret material.
type RouteRecord struct {
	ID               string
	SlotID           string
	Alias            string
	ProfileID        string
	UpstreamModel    string
	ResourceSelector string
	Status           string
	Revision         int64
	CreatedAt        time.Time
	UpdatedAt        time.Time
	RevokedAt        *time.Time
}

// TokenRecord is the digest-free DTO returned by token operations. It exposes
// metadata and opaque token reference; the raw token and digest are never present.
type TokenRecord struct {
	ID        string
	TokenRef  string
	Status    string
	Revision  int64
	CreatedAt time.Time
	UpdatedAt time.Time
	RevokedAt *time.Time
}

// CreateSlotInput holds parameters for creating a new credential slot.
type CreateSlotInput struct {
	Vendor         string
	CredentialKind string
	Alias          string
	ProviderSecret []byte
}

// RotateSlotInput holds parameters for rotating a slot's secret envelope.
type RotateSlotInput struct {
	SlotID         string
	Revision       int64
	ProviderSecret []byte
}

// CreateRouteInput holds parameters for creating a new route binding.
type CreateRouteInput struct {
	SlotID           string
	Alias            string
	ProfileID        string
	UpstreamModel    string
	ResourceSelector string
}

// UpdateRouteInput holds parameters for updating an existing route binding.
type UpdateRouteInput struct {
	RouteID          string
	CurrentRevision  int64
	SlotID           string
	Alias            string
	ProfileID        string
	UpstreamModel    string
	ResourceSelector string
}

// Service is the principal-scoped credential management service. It is
// transport-neutral and does not register listeners or protobuf messages.
type Service struct {
	store        *credentialstore.Store
	logger       *zap.Logger
	sealer       SecretSealer
	randomReader io.Reader
}

// NewService constructs a Service bound to the given store. The sealer is
// optional; mutating methods that require it return ErrSealerUnavailable
// when none is configured. The randomReader is used for internal
// randomness and may be nil (the crypto/rand default is used).
func NewService(store *credentialstore.Store, logger *zap.Logger, sealer SecretSealer, randomReader io.Reader) *Service {
	return &Service{
		store:        store,
		logger:       logger,
		sealer:       sealer,
		randomReader: randomReader,
	}
}

// authenticate resolves the presented IOP token to a principal. The raw
// token bytes are hashed in process; only the SHA-256 digest is compared
// against the store. The returned principal ID and token record are the
// only values passed to downstream store calls.
func (s *Service) authenticate(ctx context.Context, rawIOPToken []byte) (string, *credentialstore.TokenRecord, error) {
	if len(rawIOPToken) == 0 {
		return "", nil, ErrUnauthorized
	}
	digest := sha256Hex(rawIOPToken)
	p, t, err := s.store.LookupTokenByDigest(ctx, digest)
	if err != nil {
		if errors.Is(err, credentialstore.ErrTokenNotFound) {
			return "", nil, ErrUnauthorized
		}
		return "", nil, fmt.Errorf("credentialops: authenticate: %w", err)
	}
	return p.ID, t, nil
}

// requireOwner verifies that the requested resource belongs to the
// authenticated principal. It returns ErrNotFound when the ownership
// check fails.
func (s *Service) requireOwner(ctx context.Context, principalID string, slotID string) error {
	_, err := s.store.GetSlot(ctx, principalID, slotID)
	if err != nil {
		if errors.Is(err, credentialstore.ErrSlotNotFound) {
			return ErrNotFound
		}
		return fmt.Errorf("credentialops: require owner: %w", err)
	}
	return nil
}

// requireOwnerRoute verifies that the requested route belongs to the
// authenticated principal.
func (s *Service) requireOwnerRoute(ctx context.Context, principalID string, routeID string) error {
	_, err := s.store.GetRoute(ctx, principalID, routeID)
	if err != nil {
		if errors.Is(err, credentialstore.ErrRouteNotFound) {
			return ErrNotFound
		}
		return fmt.Errorf("credentialops: require owner route: %w", err)
	}
	return nil
}

// CreateSlot seals the caller-supplied provider secret and creates a slot for the authenticated principal.
func (s *Service) CreateSlot(ctx context.Context, rawIOPToken []byte, input CreateSlotInput) (SlotRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return SlotRecord{}, err
	}
	if s.sealer == nil {
		return SlotRecord{}, ErrSealerUnavailable
	}

	owned := append([]byte(nil), input.ProviderSecret...)
	defer clear(owned)

	// Allocate the stable slot id before sealing so the create-time AAD binds to
	// the exact slot that will persist the ciphertext.
	slotID := uuid.NewString()

	kind, err := canonicalCredentialKind(input.CredentialKind)
	if err != nil {
		return SlotRecord{}, err
	}

	sealCtx := credentialseal.Context{
		PrincipalID: principalID,
		SlotID:      slotID,
		Kind:        kind,
	}

	sealed, err := s.sealer.Seal(ctx, owned, sealCtx)
	if err != nil {
		return SlotRecord{}, ErrSecretHandlingFailed
	}

	created, err := s.store.CreateSlot(ctx, credentialstore.CreateSlotInput{
		SlotID:         slotID,
		PrincipalID:    principalID,
		Vendor:         input.Vendor,
		CredentialKind: kind,
		Alias:          input.Alias,
		Envelope:       sealed,
	})
	if err != nil {
		return SlotRecord{}, mapStoreError(err)
	}
	return slotRecordFromSlot(created), nil
}

// RotateSlot rotates the secret envelope on a slot. The caller presents
// the raw IOP token, input params, and provider secret.
func (s *Service) RotateSlot(ctx context.Context, rawIOPToken []byte, input RotateSlotInput) (SlotRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return SlotRecord{}, err
	}
	if err := s.requireOwner(ctx, principalID, input.SlotID); err != nil {
		return SlotRecord{}, err
	}
	if s.sealer == nil {
		return SlotRecord{}, ErrSealerUnavailable
	}

	slot, err := s.store.GetSlot(ctx, principalID, input.SlotID)
	if err != nil {
		return SlotRecord{}, mapStoreError(err)
	}

	sealCtx := credentialseal.Context{
		PrincipalID: principalID,
		SlotID:      input.SlotID,
		Kind:        slot.CredentialKind,
	}

	owned := append([]byte(nil), input.ProviderSecret...)
	defer clear(owned)

	sealed, err := s.sealer.Seal(ctx, owned, sealCtx)
	if err != nil {
		return SlotRecord{}, ErrSecretHandlingFailed
	}

	rotated, err := s.store.RotateSlotSecret(ctx, credentialstore.RotateSlotSecretInput{
		PrincipalID:     principalID,
		SlotID:          input.SlotID,
		CurrentRevision: input.Revision,
		Envelope:        sealed,
	})
	if err != nil {
		return SlotRecord{}, mapStoreError(err)
	}
	return slotRecordFromSlot(rotated), nil
}

// DisableSlot transitions an active slot to disabled using CAS on revision.
func (s *Service) DisableSlot(ctx context.Context, rawIOPToken []byte, slotID string, revision int64) (SlotRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return SlotRecord{}, err
	}
	if err := s.requireOwner(ctx, principalID, slotID); err != nil {
		return SlotRecord{}, err
	}

	disabled, err := s.store.DisableSlot(ctx, principalID, slotID, revision)
	if err != nil {
		return SlotRecord{}, mapStoreError(err)
	}
	return slotRecordFromSlot(disabled), nil
}

// EnableSlot transitions a disabled slot back to active using CAS on revision.
func (s *Service) EnableSlot(ctx context.Context, rawIOPToken []byte, slotID string, revision int64) (SlotRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return SlotRecord{}, err
	}
	if err := s.requireOwner(ctx, principalID, slotID); err != nil {
		return SlotRecord{}, err
	}

	enabled, err := s.store.EnableSlot(ctx, principalID, slotID, revision)
	if err != nil {
		return SlotRecord{}, mapStoreError(err)
	}
	return slotRecordFromSlot(enabled), nil
}

// RevokeSlot permanently revokes a slot using CAS on revision.
func (s *Service) RevokeSlot(ctx context.Context, rawIOPToken []byte, slotID string, revision int64) (SlotRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return SlotRecord{}, err
	}
	if err := s.requireOwner(ctx, principalID, slotID); err != nil {
		return SlotRecord{}, err
	}

	revoked, err := s.store.RevokeSlot(ctx, principalID, slotID, revision)
	if err != nil {
		return SlotRecord{}, mapStoreError(err)
	}
	return slotRecordFromSlot(revoked), nil
}

// GetSlot returns a secret-blind DTO for a single slot.
func (s *Service) GetSlot(ctx context.Context, rawIOPToken []byte, slotID string) (SlotRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return SlotRecord{}, err
	}
	if err := s.requireOwner(ctx, principalID, slotID); err != nil {
		return SlotRecord{}, err
	}

	slot, err := s.store.GetSlot(ctx, principalID, slotID)
	if err != nil {
		return SlotRecord{}, mapStoreError(err)
	}
	return slotRecordFromSlot(slot), nil
}

// ListSlots returns all secret-blind slot DTOs for the authenticated principal.
func (s *Service) ListSlots(ctx context.Context, rawIOPToken []byte) ([]SlotRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return nil, err
	}

	slots, err := s.store.ListSlots(ctx, principalID)
	if err != nil {
		return nil, fmt.Errorf("credentialops: list slots: %w", err)
	}
	out := make([]SlotRecord, len(slots))
	for i := range slots {
		out[i] = slotRecordFromSlot(&slots[i])
	}
	return out, nil
}

// CreateRoute binds a new route to an owned slot for the authenticated principal.
func (s *Service) CreateRoute(ctx context.Context, rawIOPToken []byte, input CreateRouteInput) (RouteRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return RouteRecord{}, err
	}
	if err := s.requireOwner(ctx, principalID, input.SlotID); err != nil {
		return RouteRecord{}, err
	}

	route, err := s.store.CreateRoute(ctx, credentialstore.CreateRouteInput{
		PrincipalID:      principalID,
		SlotID:           input.SlotID,
		Alias:            input.Alias,
		ProfileID:        input.ProfileID,
		UpstreamModel:    input.UpstreamModel,
		ResourceSelector: input.ResourceSelector,
	})
	if err != nil {
		return RouteRecord{}, mapStoreError(err)
	}
	return routeRecordFromRoute(route), nil
}

// UpdateRoute updates an existing route binding under CAS on revision.
func (s *Service) UpdateRoute(ctx context.Context, rawIOPToken []byte, input UpdateRouteInput) (RouteRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return RouteRecord{}, err
	}
	if err := s.requireOwnerRoute(ctx, principalID, input.RouteID); err != nil {
		return RouteRecord{}, err
	}
	if err := s.requireOwner(ctx, principalID, input.SlotID); err != nil {
		return RouteRecord{}, err
	}

	updated, err := s.store.UpdateRoute(ctx, credentialstore.UpdateRouteInput{
		PrincipalID:      principalID,
		RouteID:          input.RouteID,
		CurrentRevision:  input.CurrentRevision,
		SlotID:           input.SlotID,
		Alias:            input.Alias,
		ProfileID:        input.ProfileID,
		UpstreamModel:    input.UpstreamModel,
		ResourceSelector: input.ResourceSelector,
	})
	if err != nil {
		return RouteRecord{}, mapStoreError(err)
	}
	return routeRecordFromRoute(updated), nil
}

// GetRoute returns a DTO for a single route.
func (s *Service) GetRoute(ctx context.Context, rawIOPToken []byte, routeID string) (RouteRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return RouteRecord{}, err
	}
	if err := s.requireOwnerRoute(ctx, principalID, routeID); err != nil {
		return RouteRecord{}, err
	}

	route, err := s.store.GetRoute(ctx, principalID, routeID)
	if err != nil {
		return RouteRecord{}, mapStoreError(err)
	}
	return routeRecordFromRoute(route), nil
}

// ListRoutes returns all route DTOs for the authenticated principal.
func (s *Service) ListRoutes(ctx context.Context, rawIOPToken []byte) ([]RouteRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return nil, err
	}

	routes, err := s.store.ListRoutes(ctx, principalID)
	if err != nil {
		return nil, fmt.Errorf("credentialops: list routes: %w", err)
	}
	out := make([]RouteRecord, len(routes))
	for i := range routes {
		out[i] = routeRecordFromRoute(&routes[i])
	}
	return out, nil
}

// DisableRoute transitions an active route to disabled using CAS on revision.
func (s *Service) DisableRoute(ctx context.Context, rawIOPToken []byte, routeID string, revision int64) (RouteRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return RouteRecord{}, err
	}
	if err := s.requireOwnerRoute(ctx, principalID, routeID); err != nil {
		return RouteRecord{}, err
	}

	disabled, err := s.store.DisableRoute(ctx, principalID, routeID, revision)
	if err != nil {
		return RouteRecord{}, mapStoreError(err)
	}
	return routeRecordFromRoute(disabled), nil
}

// EnableRoute transitions a disabled route back to active using CAS on revision.
func (s *Service) EnableRoute(ctx context.Context, rawIOPToken []byte, routeID string, revision int64) (RouteRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return RouteRecord{}, err
	}
	if err := s.requireOwnerRoute(ctx, principalID, routeID); err != nil {
		return RouteRecord{}, err
	}

	enabled, err := s.store.EnableRoute(ctx, principalID, routeID, revision)
	if err != nil {
		return RouteRecord{}, mapStoreError(err)
	}
	return routeRecordFromRoute(enabled), nil
}

// RevokeRoute permanently revokes a route using CAS on revision.
func (s *Service) RevokeRoute(ctx context.Context, rawIOPToken []byte, routeID string, revision int64) (RouteRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return RouteRecord{}, err
	}
	if err := s.requireOwnerRoute(ctx, principalID, routeID); err != nil {
		return RouteRecord{}, err
	}

	revoked, err := s.store.RevokeRoute(ctx, principalID, routeID, revision)
	if err != nil {
		return RouteRecord{}, mapStoreError(err)
	}
	return routeRecordFromRoute(revoked), nil
}

// CreateToken issues a new active IOP token for the authenticated principal.
// The raw token string is returned exactly once in IssuedToken and is never persisted or retrievable.
func (s *Service) CreateToken(ctx context.Context, rawIOPToken []byte) (IssuedToken, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return IssuedToken{}, err
	}

	issued, err := s.store.CreateToken(ctx, principalID)
	if err != nil {
		return IssuedToken{}, mapStoreError(err)
	}

	return IssuedToken{
		Token:    tokenRecordFromToken(&issued.Token),
		RawToken: issued.RawToken,
	}, nil
}

// ListTokens returns all digest-free token records for the authenticated principal.
func (s *Service) ListTokens(ctx context.Context, rawIOPToken []byte) ([]TokenRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return nil, err
	}

	tokens, err := s.store.ListTokens(ctx, principalID)
	if err != nil {
		return nil, mapStoreError(err)
	}

	out := make([]TokenRecord, len(tokens))
	for i := range tokens {
		out[i] = tokenRecordFromToken(&tokens[i])
	}
	return out, nil
}

// DisableToken transitions an active token to disabled using CAS on revision.
func (s *Service) DisableToken(ctx context.Context, rawIOPToken []byte, tokenRef string, revision int64) (TokenRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return TokenRecord{}, err
	}

	disabled, err := s.store.DisableToken(ctx, principalID, tokenRef, revision)
	if err != nil {
		return TokenRecord{}, mapStoreError(err)
	}
	return tokenRecordFromToken(disabled), nil
}

// RevokeToken permanently revokes a token using CAS on revision.
func (s *Service) RevokeToken(ctx context.Context, rawIOPToken []byte, tokenRef string, revision int64) (TokenRecord, error) {
	principalID, _, err := s.authenticate(ctx, rawIOPToken)
	if err != nil {
		return TokenRecord{}, err
	}

	revoked, err := s.store.RevokeToken(ctx, principalID, tokenRef, revision)
	if err != nil {
		return TokenRecord{}, mapStoreError(err)
	}
	return tokenRecordFromToken(revoked), nil
}

// slotRecordFromSlot converts a credentialstore.CredentialSlotRecord to a
// secret-blind SlotRecord DTO.
func slotRecordFromSlot(s *credentialstore.CredentialSlotRecord) SlotRecord {
	var r SlotRecord
	r.ID = s.ID
	r.Vendor = s.Vendor
	r.CredentialKind = s.CredentialKind
	r.Alias = s.Alias
	r.Status = s.Status
	r.Revision = s.Revision
	r.CreatedAt = s.CreatedAt
	r.UpdatedAt = s.UpdatedAt
	r.RevokedAt = s.RevokedAt
	return r
}

// routeRecordFromRoute converts a credentialstore.RouteRecord to a DTO.
func routeRecordFromRoute(r *credentialstore.RouteRecord) RouteRecord {
	return RouteRecord{
		ID:               r.ID,
		SlotID:           r.SlotID,
		Alias:            r.Alias,
		ProfileID:        r.ProfileID,
		UpstreamModel:    r.UpstreamModel,
		ResourceSelector: r.ResourceSelector,
		Status:           r.Status,
		Revision:         r.Revision,
		CreatedAt:        r.CreatedAt,
		UpdatedAt:        r.UpdatedAt,
		RevokedAt:        r.RevokedAt,
	}
}

// tokenRecordFromToken converts a credentialstore.TokenRecord to a DTO.
func tokenRecordFromToken(t *credentialstore.TokenRecord) TokenRecord {
	return TokenRecord{
		ID:        t.ID,
		TokenRef:  t.TokenRef,
		Status:    t.Status,
		Revision:  t.Revision,
		CreatedAt: t.CreatedAt,
		UpdatedAt: t.UpdatedAt,
		RevokedAt: t.RevokedAt,
	}
}

// mapStoreError translates credentialstore errors into credentialops
// typed errors so callers do not depend on the store package.
func mapStoreError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, credentialstore.ErrRevisionMismatch) {
		return ErrStaleRevision
	}
	if errors.Is(err, credentialstore.ErrSlotNotFound) || errors.Is(err, credentialstore.ErrRouteNotFound) {
		return ErrNotFound
	}
	if errors.Is(err, credentialstore.ErrCrossPrincipalSlot) {
		return ErrUnauthorized
	}
	if errors.Is(err, credentialstore.ErrTokenNotFound) || errors.Is(err, credentialstore.ErrPrincipalNotFound) {
		return ErrNotFound
	}
	if errors.Is(err, credentialstore.ErrInvalidSecretEnvelope) ||
		errors.Is(err, credentialstore.ErrEnvelopeKeyUnavailable) ||
		errors.Is(err, credentialstore.ErrUnknownEnvelopeKey) {
		return ErrSecretHandlingFailed
	}
	if errors.Is(err, credentialstore.ErrTokenRevoked) || errors.Is(err, credentialstore.ErrTokenNotActive) {
		return fmt.Errorf("%w: %v", ErrUnauthorized, err)
	}
	if errors.Is(err, credentialstore.ErrSlotRevoked) || errors.Is(err, credentialstore.ErrRouteRevoked) {
		return fmt.Errorf("%w: %v", ErrUnauthorized, err)
	}
	if errors.Is(err, credentialstore.ErrSlotNotActive) || errors.Is(err, credentialstore.ErrRouteNotActive) {
		return fmt.Errorf("%w: %v", ErrUnauthorized, err)
	}
	return err
}

// sha256Hex returns the lowercase hex SHA-256 digest of raw.
func sha256Hex(raw []byte) string {
	sum := sha256.Sum256(raw)
	return hex.EncodeToString(sum[:])
}

// canonicalCredentialKind normalizes and validates the caller-supplied
// credential kind. It reuses the credentialstore constants so the value
// written into the seal context and persisted in the slot row are identical,
// preventing AAD divergence after restart. Unknown kinds are rejected before
// any sealer or store call.
func canonicalCredentialKind(raw string) (string, error) {
	norm := strings.ToLower(strings.TrimSpace(raw))
	switch norm {
	case credentialstore.CredentialKindBearer, credentialstore.CredentialKindAPIKey:
		return norm, nil
	default:
		return "", fmt.Errorf("%w: %q", ErrInvalidCredentialKind, raw)
	}
}
