package main

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/vmihailenco/msgpack/v5"
	"golang.org/x/crypto/nacl/box"
	"golang.org/x/crypto/nacl/sign"
)

const (
	delegatedProtocolVer              = int64(1)
	delegatedClaimDomain              = "kl_mobile_sso_request_v1"
	delegatedClaimFieldCount          = 11
	delegatedRandomBytes              = 32
	delegatedPublicKeyBytes           = 32
	delegatedKeyIDBytes               = 16
	delegatedReqLifetime              = 10 * time.Minute
	delegatedRetentionLifetime        = 12 * time.Minute
	delegatedClockTolerance           = time.Minute
	delegatedAssertionLifetime        = 2 * time.Minute
	delegatedMaxAuthReqBytes          = 4 * 1024
	delegatedMaxReturnURLBytes        = 2 * 1024
	delegatedMaxBackendAssertionBytes = 3 * 1024
	delegatedMaxClientResultBytes     = 12 * 1024
	delegatedMaxSuccessWrapperBytes   = 16 * 1024
	delegatedMaxEncodedResultChars    = 21_846
	delegatedMaxCallbackURLBytes      = 24 * 1024
)

var (
	ErrDelegatedAuthentication = errors.New("delegated authentication failed")
	delegatedSchemePattern     = regexp.MustCompile(`^[a-z][a-z0-9+.-]*$`)
)

type DelegatedStartOptions struct {
	AppTag          string
	ReturnURL       string
	Capabilities    []string
	ClientSignPK    []byte
	ClientEncPK     []byte
	DeviceName      *string
	RequestedScopes []string
}

type DelegatedPending struct {
	AppTag             string
	ChallengeHash      [sha256.Size]byte
	FinishSecretHash   [sha256.Size]byte
	RequestDigest      [sha256.Size]byte
	Capabilities       []string
	ClientSignPK       []byte
	ClientEncPK        []byte
	ChallengeExpiresAt int64
	RetentionExpiresAt int64
}

type DelegatedStartResult struct {
	DeepLink           string
	FinishSecret       string
	RequestDigest      string
	ExpiresAt          int64
	RetentionExpiresAt int64
	Pending            DelegatedPending
}

type DelegatedCallbackResult struct {
	Cancelled        bool
	BackendAssertion []byte
	ClientResult     []byte
}

type DelegatedIdentity struct {
	SafeID       string
	AppID        string
	Nickname     string
	Capabilities []string
	IssuedAt     int64
	ExpiresAt    int64
}

type DelegatedVerifiedFinish struct {
	Identity        DelegatedIdentity
	AssertionDigest [sha256.Size]byte
	IdempotencyKey  string
}

// DelegatedCompletionStore must atomically move pending to completed by IdempotencyKey.
// Exact retries return one session; changed or expired inputs must be rejected.
type DelegatedCompletionStore interface {
	CompleteDelegatedSSO(context.Context, DelegatedCompletionRequest) (DelegatedSessionResult, error)
}

type DelegatedCompletionRequest struct {
	IdempotencyKey     string
	FinishSecretHash   [sha256.Size]byte
	AssertionDigest    [sha256.Size]byte
	AssertionExpiresAt int64
	RetentionExpiresAt int64
	Identity           DelegatedIdentity
}

type DelegatedSessionResult struct {
	SessionID string
	SafeID    string
	Nickname  string
}

func StartDelegatedSSO(options DelegatedStartOptions, signingKey ed25519.PrivateKey) (*DelegatedStartResult, error) {
	return startDelegatedSSO(options, signingKey, time.Now(), rand.Reader)
}

func startDelegatedSSO(
	options DelegatedStartOptions,
	signingKey ed25519.PrivateKey,
	now time.Time,
	random io.Reader,
) (*DelegatedStartResult, error) {
	if len(signingKey) != ed25519.PrivateKeySize || random == nil {
		return nil, fmt.Errorf("delegated SSO requires a valid Ed25519 private key and random source")
	}
	if err := validateDelegatedStartOptions(options); err != nil {
		return nil, err
	}
	challenge := make([]byte, delegatedRandomBytes)
	finishSecret := make([]byte, delegatedRandomBytes)
	if _, err := io.ReadFull(random, challenge); err != nil {
		return nil, fmt.Errorf("generate delegated challenge: %w", err)
	}
	if _, err := io.ReadFull(random, finishSecret); err != nil {
		return nil, fmt.Errorf("generate delegated finish secret: %w", err)
	}
	if subtle.ConstantTimeCompare(challenge, finishSecret) == 1 {
		return nil, errors.New("delegated challenge and finish secret must be independent")
	}

	startWallTime := now.Unix()
	challengeExpiresAt := startWallTime + int64(delegatedReqLifetime/time.Second)
	retentionExpiresAt := startWallTime + int64(delegatedRetentionLifetime/time.Second)
	claim := []any{
		delegatedClaimDomain,
		options.AppTag,
		challenge,
		startWallTime,
		challengeExpiresAt,
		options.ReturnURL,
		options.Capabilities,
		optionalBytes(options.ClientSignPK),
		optionalBytes(options.ClientEncPK),
		optionalString(options.DeviceName),
		optionalStrings(options.RequestedScopes),
	}
	claimBytes, err := encodeCanonical(claim)
	if err != nil {
		return nil, fmt.Errorf("encode delegated claim: %w", err)
	}
	publicKey := signingKey.Public().(ed25519.PublicKey)
	keyDigest := sha256.Sum256(publicKey)
	keyID := base64.RawURLEncoding.EncodeToString(keyDigest[:delegatedKeyIDBytes])
	signature := ed25519.Sign(signingKey, claimBytes)
	signedClaim := append(append([]byte(nil), signature...), claimBytes...)
	authReq, err := encodeDelegatedAuthReq(options.AppTag, keyID, signedClaim)
	if err != nil {
		return nil, fmt.Errorf("encode delegated auth request: %w", err)
	}
	if len(authReq) > delegatedMaxAuthReqBytes {
		return nil, fmt.Errorf("delegated auth request exceeds %d bytes", delegatedMaxAuthReqBytes)
	}
	requestDigest := sha256.Sum256(authReq)
	challengeHash := sha256.Sum256(challenge)
	finishSecretHash := sha256.Sum256(finishSecret)
	return &DelegatedStartResult{
		DeepLink:           "keylockr://sso-request?request=" + base64.RawURLEncoding.EncodeToString(authReq),
		FinishSecret:       base64.RawURLEncoding.EncodeToString(finishSecret),
		RequestDigest:      base64.RawURLEncoding.EncodeToString(requestDigest[:]),
		ExpiresAt:          challengeExpiresAt,
		RetentionExpiresAt: retentionExpiresAt,
		Pending: DelegatedPending{
			AppTag: options.AppTag, ChallengeHash: challengeHash,
			FinishSecretHash: finishSecretHash, RequestDigest: requestDigest,
			Capabilities:       append([]string(nil), options.Capabilities...),
			ClientSignPK:       append([]byte(nil), options.ClientSignPK...),
			ClientEncPK:        append([]byte(nil), options.ClientEncPK...),
			ChallengeExpiresAt: challengeExpiresAt, RetentionExpiresAt: retentionExpiresAt,
		},
	}, nil
}

func ParseDelegatedCallback(callback string, expectedDigest [sha256.Size]byte) (*DelegatedCallbackResult, error) {
	if callback == "cancel" {
		return &DelegatedCallbackResult{Cancelled: true}, nil
	}
	if len(callback) == 0 || len(callback) > delegatedMaxEncodedResultChars {
		return nil, ErrDelegatedAuthentication
	}
	raw, err := base64.RawURLEncoding.DecodeString(callback)
	if err != nil || len(raw) == 0 || len(raw) > delegatedMaxSuccessWrapperBytes ||
		base64.RawURLEncoding.EncodeToString(raw) != callback {
		return nil, ErrDelegatedAuthentication
	}
	fields, err := decodeStrictMap(raw, "v", "request_digest", "backend_assertion", "client_result")
	if err != nil || len(fields) < 3 || len(fields) > 4 || decodeInt64(fields["v"]) != delegatedProtocolVer {
		return nil, ErrDelegatedAuthentication
	}
	digest, err := decodeBytes(fields["request_digest"])
	if err != nil || len(digest) != sha256.Size || subtle.ConstantTimeCompare(digest, expectedDigest[:]) != 1 {
		return nil, ErrDelegatedAuthentication
	}
	assertion, err := decodeBytes(fields["backend_assertion"])
	if err != nil || len(assertion) == 0 || len(assertion) > delegatedMaxBackendAssertionBytes {
		return nil, ErrDelegatedAuthentication
	}
	var clientResult []byte
	if rawClient, found := fields["client_result"]; found {
		clientResult, err = decodeBytes(rawClient)
		if err != nil || len(clientResult) == 0 || len(clientResult) > delegatedMaxClientResultBytes {
			return nil, ErrDelegatedAuthentication
		}
	}
	return &DelegatedCallbackResult{BackendAssertion: assertion, ClientResult: clientResult}, nil
}

func ParseDelegatedCallbackURL(callbackURL string, expectedDigest [sha256.Size]byte) (*DelegatedCallbackResult, error) {
	if len(callbackURL) == 0 || len([]byte(callbackURL)) > delegatedMaxCallbackURLBytes {
		return nil, ErrDelegatedAuthentication
	}
	parsed, err := url.Parse(callbackURL)
	if err != nil || parsed.Fragment != "" {
		return nil, ErrDelegatedAuthentication
	}
	query, err := url.ParseQuery(parsed.RawQuery)
	if err != nil {
		return nil, ErrDelegatedAuthentication
	}
	values, ok := query["keylockr_result"]
	if !ok || len(values) != 1 || len(query) != 1 {
		return nil, ErrDelegatedAuthentication
	}
	return ParseDelegatedCallback(values[0], expectedDigest)
}

func VerifyDelegatedFinish(
	pending DelegatedPending,
	finishSecret string,
	backendAssertion []byte,
	klSigningPublicKey []byte,
	klEncPK []byte,
	svcEncSK []byte,
	now time.Time,
) (*DelegatedVerifiedFinish, error) {
	verified, err := verifyDelegatedAssertion(
		pending, backendAssertion, klSigningPublicKey, klEncPK,
		svcEncSK, now,
	)
	if err != nil {
		return nil, ErrDelegatedAuthentication
	}
	secret, err := base64.RawURLEncoding.DecodeString(finishSecret)
	secretHash := sha256.Sum256(secret)
	if err != nil || len(secret) != delegatedRandomBytes ||
		base64.RawURLEncoding.EncodeToString(secret) != finishSecret ||
		subtle.ConstantTimeCompare(secretHash[:], pending.FinishSecretHash[:]) != 1 {
		return nil, ErrDelegatedAuthentication
	}
	return verified, nil
}

func CompleteDelegatedSSO(
	ctx context.Context,
	store DelegatedCompletionStore,
	pending DelegatedPending,
	finishSecret string,
	backendAssertion []byte,
	klSigningPublicKey []byte,
	klEncPK []byte,
	svcEncSK []byte,
	now time.Time,
) (DelegatedSessionResult, error) {
	if store == nil {
		return DelegatedSessionResult{}, ErrDelegatedAuthentication
	}
	verified, err := VerifyDelegatedFinish(
		pending, finishSecret, backendAssertion, klSigningPublicKey,
		klEncPK, svcEncSK, now,
	)
	if err != nil {
		return DelegatedSessionResult{}, ErrDelegatedAuthentication
	}
	result, err := store.CompleteDelegatedSSO(ctx, DelegatedCompletionRequest{
		IdempotencyKey: verified.IdempotencyKey, FinishSecretHash: pending.FinishSecretHash,
		AssertionDigest: verified.AssertionDigest, AssertionExpiresAt: verified.Identity.ExpiresAt,
		RetentionExpiresAt: pending.RetentionExpiresAt, Identity: verified.Identity,
	})
	if err != nil || result.SessionID == "" || result.SafeID != verified.Identity.SafeID ||
		result.Nickname != verified.Identity.Nickname {
		return DelegatedSessionResult{}, ErrDelegatedAuthentication
	}
	return result, nil
}

func verifyDelegatedAssertion(
	pending DelegatedPending,
	assertion []byte,
	klSigningPublicKey []byte,
	klEncPK []byte,
	svcEncSK []byte,
	now time.Time,
) (*DelegatedVerifiedFinish, error) {
	if len(assertion) == 0 || len(assertion) > delegatedMaxBackendAssertionBytes ||
		len(klSigningPublicKey) != delegatedPublicKeyBytes ||
		len(klEncPK) != delegatedPublicKeyBytes ||
		len(svcEncSK) != delegatedPublicKeyBytes ||
		now.Unix() >= pending.RetentionExpiresAt {
		return nil, ErrDelegatedAuthentication
	}
	root, err := decodeStrictMap(assertion, "kps", "seal")
	if err != nil || len(root) != 2 {
		return nil, errInvalidDelegated()
	}
	kpsRaw, sealRaw := root["kps"], root["seal"]
	seal, err := decodeBytes(sealRaw)
	if err != nil || len(seal) != sign.Overhead+sha256.Size {
		return nil, errInvalidDelegated()
	}
	kpsDigest := sha256.Sum256(kpsRaw)
	var signingKey [delegatedPublicKeyBytes]byte
	copy(signingKey[:], klSigningPublicKey)
	openedDigest, ok := sign.Open(nil, seal, &signingKey)
	if !ok || subtle.ConstantTimeCompare(openedDigest, kpsDigest[:]) != 1 {
		return nil, errInvalidDelegated()
	}
	kps, err := decodeStrictMap(kpsRaw, "box", "n")
	if err != nil || len(kps) != 2 {
		return nil, errInvalidDelegated()
	}
	boxed, err := decodeBytes(kps["box"])
	if err != nil || len(boxed) <= box.Overhead {
		return nil, errInvalidDelegated()
	}
	nonceBytes, err := decodeBytes(kps["n"])
	if err != nil || len(nonceBytes) != 24 {
		return nil, errInvalidDelegated()
	}
	var nonce [24]byte
	var klEncKey, svcEncKey [delegatedPublicKeyBytes]byte
	copy(nonce[:], nonceBytes)
	copy(klEncKey[:], klEncPK)
	copy(svcEncKey[:], svcEncSK)
	plain, ok := box.Open(nil, boxed, &nonce, &klEncKey, &svcEncKey)
	if !ok {
		return nil, errInvalidDelegated()
	}
	payload, err := decodeStrictMap(plain, "header", "body")
	if err != nil || len(payload) != 2 {
		return nil, errInvalidDelegated()
	}
	header, err := decodeStrictMap(payload["header"], "ts", "from")
	if err != nil || len(header) != 2 || decodeString(header["from"]) != "sso_login_assertion" {
		return nil, errInvalidDelegated()
	}
	body, err := decodeStrictMap(
		payload["body"], "_res", "status", "version", "aud", "challenge", "safe_id",
		"app_id", "capabilities", "user_nickname", "iat", "exp", "request_digest",
	)
	if err != nil || len(body) != 12 || decodeString(body["_res"]) != "ok" ||
		decodeString(body["status"]) != "done" || decodeInt64(body["version"]) != delegatedProtocolVer ||
		decodeString(body["aud"]) != pending.AppTag {
		return nil, errInvalidDelegated()
	}
	challenge, err := decodeBytes(body["challenge"])
	if err != nil || len(challenge) != delegatedRandomBytes {
		return nil, errInvalidDelegated()
	}
	challengeHash := sha256.Sum256(challenge)
	requestDigest, err := decodeBytes(body["request_digest"])
	if err != nil || len(requestDigest) != sha256.Size ||
		subtle.ConstantTimeCompare(challengeHash[:], pending.ChallengeHash[:]) != 1 ||
		subtle.ConstantTimeCompare(requestDigest, pending.RequestDigest[:]) != 1 {
		return nil, errInvalidDelegated()
	}
	capabilities, err := decodeStrings(body["capabilities"])
	if err != nil || !equalStrings(capabilities, pending.Capabilities) {
		return nil, errInvalidDelegated()
	}
	iat, exp := decodeInt64(body["iat"]), decodeInt64(body["exp"])
	nowUnix := now.Unix()
	if iat <= 0 || exp <= iat || exp-iat > int64(delegatedAssertionLifetime/time.Second) ||
		decodeInt64(header["ts"]) != iat ||
		iat > pending.ChallengeExpiresAt+int64(delegatedClockTolerance/time.Second) ||
		iat > nowUnix+int64(delegatedClockTolerance/time.Second) ||
		nowUnix >= exp+int64(delegatedClockTolerance/time.Second) {
		return nil, errInvalidDelegated()
	}
	identity := DelegatedIdentity{
		SafeID: decodeString(body["safe_id"]), AppID: decodeString(body["app_id"]),
		Nickname: decodeString(body["user_nickname"]), Capabilities: capabilities,
		IssuedAt: iat, ExpiresAt: exp,
	}
	if identity.SafeID == "" || identity.AppID == "" || identity.Nickname == "" {
		return nil, errInvalidDelegated()
	}
	assertionDigest := sha256.Sum256(assertion)
	return &DelegatedVerifiedFinish{
		Identity: identity, AssertionDigest: assertionDigest,
		IdempotencyKey: base64.RawURLEncoding.EncodeToString(pending.ChallengeHash[:]),
	}, nil
}

func validateDelegatedStartOptions(options DelegatedStartOptions) error {
	if !utf8.ValidString(options.AppTag) || strings.TrimSpace(options.AppTag) == "" ||
		options.AppTag != strings.TrimSpace(options.AppTag) ||
		len(options.AppTag) > 128 {
		return errors.New("delegated app_tag is invalid")
	}
	if _, ok := canonicalDelegatedReturnURL(options.ReturnURL); !ok {
		return errors.New("delegated return_url is invalid")
	}
	want := []string{"sso"}
	seenCapabilities := make(map[string]bool, len(options.Capabilities))
	for _, capability := range options.Capabilities {
		if seenCapabilities[capability] {
			return errors.New("delegated capabilities must be unique and ordered")
		}
		seenCapabilities[capability] = true
		switch capability {
		case "sso":
		case "app_data":
			want = append(want, "app_data")
		case "ap":
			want = append(want, "ap")
		default:
			return errors.New("delegated capability is invalid")
		}
	}
	if !equalStrings(want, options.Capabilities) {
		return errors.New("delegated capabilities must be unique and ordered")
	}
	if (len(options.ClientSignPK) == 0) != (len(options.ClientEncPK) == 0) ||
		(len(options.ClientSignPK) != 0 && (len(options.ClientSignPK) != delegatedPublicKeyBytes ||
			len(options.ClientEncPK) != delegatedPublicKeyBytes)) {
		return errors.New("delegated client keys must be a complete 32-byte pair")
	}
	requiresDeviceName := containsString(options.Capabilities, "app_data") ||
		containsString(options.Capabilities, "ap")
	if requiresDeviceName && options.DeviceName == nil {
		return errors.New("delegated device name is required for AppData or AP")
	}
	if options.DeviceName != nil && (!utf8.ValidString(*options.DeviceName) ||
		strings.TrimSpace(*options.DeviceName) == "" || len([]rune(*options.DeviceName)) > 255) {
		return errors.New("delegated device name is empty")
	}
	if options.RequestedScopes != nil {
		if !containsString(options.Capabilities, "ap") || len(options.RequestedScopes) == 0 {
			return errors.New("delegated scopes require AP capability")
		}
		seenScopes := make(map[string]bool, len(options.RequestedScopes))
		for _, scope := range options.RequestedScopes {
			if !utf8.ValidString(scope) || strings.TrimSpace(scope) == "" ||
				scope != strings.TrimSpace(scope) || seenScopes[scope] {
				return errors.New("delegated scope is invalid")
			}
			seenScopes[scope] = true
		}
	}
	return nil
}

func canonicalDelegatedReturnURL(raw string) (string, bool) {
	if raw == "" || !utf8.ValidString(raw) || len([]byte(raw)) > delegatedMaxReturnURLBytes ||
		strings.TrimSpace(raw) != raw || strings.ContainsAny(raw, "*?#\\\r\n\t") {
		return "", false
	}
	parsed, err := url.Parse(raw)
	if err != nil || !parsed.IsAbs() || parsed.Opaque != "" || parsed.User != nil || parsed.RawQuery != "" ||
		parsed.ForceQuery || parsed.Fragment != "" || !delegatedSchemePattern.MatchString(parsed.Scheme) {
		return "", false
	}
	// BEGIN GENERATED SSO RETURN URL POLICY
	// return-url-blocked: about,blob,content,data,facetime,facetime-audio,file,intent,itms*,javascript,keylockr,mailto,market,sms,sso,tel,telprompt
	blocked := map[string]bool{
		"about":          true,
		"blob":           true,
		"content":        true,
		"data":           true,
		"facetime":       true,
		"facetime-audio": true,
		"file":           true,
		"intent":         true,
		"javascript":     true,
		"keylockr":       true,
		"mailto":         true,
		"market":         true,
		"sms":            true,
		"sso":            true,
		"tel":            true,
		"telprompt":      true,
	}
	scheme := strings.ToLower(parsed.Scheme)
	if blocked[scheme] || strings.HasPrefix(scheme, "itms") {
		return "", false
	}
	// END GENERATED SSO RETURN URL POLICY
	hostname := parsed.Hostname()
	if parsed.Host != "" {
		if hostname == "" || hostname != strings.ToLower(hostname) {
			return "", false
		}
		port := parsed.Port()
		if (parsed.Scheme == "https" && port == "443") || (parsed.Scheme == "http" && port == "80") {
			return "", false
		}
		if port != "" {
			portNum, err := strconv.Atoi(port)
			if err != nil || portNum < 1 || portNum > 65535 {
				return "", false
			}
		}
	}
	if (parsed.Scheme == "https" || parsed.Scheme == "http") && hostname == "" {
		return "", false
	}
	if parsed.Host == "" && parsed.Path == "" {
		return "", false
	}
	for _, segment := range strings.Split(parsed.EscapedPath(), "/") {
		if segment == "." || segment == ".." || strings.EqualFold(segment, "%2e") || strings.EqualFold(segment, "%2e%2e") {
			return "", false
		}
	}
	return raw, parsed.String() == raw
}

func encodeCanonical(value any) ([]byte, error) {
	var buffer bytes.Buffer
	encoder := msgpack.NewEncoder(&buffer)
	encoder.SetSortMapKeys(true)
	if err := encoder.Encode(value); err != nil {
		return nil, err
	}
	return buffer.Bytes(), nil
}

func encodeDelegatedAuthReq(appTag, keyID string, signedClaim []byte) ([]byte, error) {
	var buffer bytes.Buffer
	encoder := msgpack.NewEncoder(&buffer)
	if err := encoder.EncodeMapLen(4); err != nil {
		return nil, err
	}
	fields := []struct {
		name  string
		value any
	}{
		{"v", delegatedProtocolVer},
		{"app_tag", appTag},
		{"kid", keyID},
		{"signed_claim", signedClaim},
	}
	for _, field := range fields {
		if err := encoder.EncodeString(field.name); err != nil {
			return nil, err
		}
		switch value := field.value.(type) {
		case int64:
			if err := encoder.EncodeInt64(value); err != nil {
				return nil, err
			}
		case string:
			if err := encoder.EncodeString(value); err != nil {
				return nil, err
			}
		case []byte:
			if err := encoder.EncodeBytes(value); err != nil {
				return nil, err
			}
		default:
			return nil, errors.New("unsupported delegated auth request field")
		}
	}
	return buffer.Bytes(), nil
}

func decodeStrictMap(raw []byte, allowed ...string) (map[string]msgpack.RawMessage, error) {
	reader := bytes.NewReader(raw)
	decoder := msgpack.NewDecoder(reader)
	fieldCount, err := decoder.DecodeMapLen()
	if err != nil || fieldCount < 0 || fieldCount > len(allowed) {
		return nil, errInvalidDelegated()
	}
	allowedSet := make(map[string]bool, len(allowed))
	for _, field := range allowed {
		allowedSet[field] = true
	}
	fields := make(map[string]msgpack.RawMessage, fieldCount)
	for index := 0; index < fieldCount; index++ {
		field, err := decoder.DecodeString()
		if err != nil || !allowedSet[field] {
			return nil, errInvalidDelegated()
		}
		if _, duplicate := fields[field]; duplicate {
			return nil, errInvalidDelegated()
		}
		value, err := decoder.DecodeRaw()
		if err != nil {
			return nil, errInvalidDelegated()
		}
		fields[field] = value
	}
	if reader.Len() != 0 {
		return nil, errInvalidDelegated()
	}
	return fields, nil
}

func decodeBytes(raw msgpack.RawMessage) ([]byte, error) {
	var value []byte
	if err := msgpack.Unmarshal(raw, &value); err != nil || value == nil {
		return nil, errInvalidDelegated()
	}
	return value, nil
}

func decodeString(raw msgpack.RawMessage) string {
	var value string
	if err := msgpack.Unmarshal(raw, &value); err != nil {
		return ""
	}
	return value
}

func decodeInt64(raw msgpack.RawMessage) int64 {
	var value int64
	if err := msgpack.Unmarshal(raw, &value); err != nil {
		return 0
	}
	return value
}

func decodeStrings(raw msgpack.RawMessage) ([]string, error) {
	var value []string
	if err := msgpack.Unmarshal(raw, &value); err != nil || value == nil {
		return nil, errInvalidDelegated()
	}
	return value, nil
}

func optionalBytes(value []byte) any {
	if len(value) == 0 {
		return nil
	}
	return value
}

func optionalString(value *string) any {
	if value == nil {
		return nil
	}
	return *value
}

func optionalStrings(value []string) any {
	if value == nil {
		return nil
	}
	return value
}

func errInvalidDelegated() error {
	return ErrDelegatedAuthentication
}
