// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package oidc

import (
	"context"
	"crypto/ed25519"
	"fmt"
	"time"

	"github.com/hashicorp/boundary/internal/auth/oidc/request"
	"github.com/hashicorp/boundary/internal/authtoken"
	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/iam"
	"github.com/hashicorp/boundary/internal/kms"
	"github.com/hashicorp/boundary/internal/libs/crypto"
	wrapping "github.com/hashicorp/go-kms-wrapping/v2"
	aead "github.com/hashicorp/go-kms-wrapping/v2/aead"
	"github.com/mr-tron/base58"
	"google.golang.org/protobuf/proto"
)

const (
	// AttemptExpiration defines the TTL for an authentication attempt
	AttemptExpiration = 5 * 60 * time.Second

	// FinalRedirectEndpoint is the endpoint that the oidc callback redirect
	// client to after the callback is complete.
	FinalRedirectEndpoint = "%s/authentication-complete"

	// AuthenticationErrorsEndpoint is the endpoint that will returned as the final redirect
	// from the callback when there are auth errors
	AuthenticationErrorsEndpoint = "%s/authentication-error"

	// CallbackEndpoint is the endpoint for the oidc callback which will be
	// included in the auth URL returned when an authen attempted is kicked off.
	CallbackEndpoint = "%s/v1/auth-methods/oidc:authenticate:callback"
)

type (
	// OidcRepoFactory is used by "service functions" to create a new oidc repo
	OidcRepoFactory func() (*Repository, error)

	// IamRepoFactory is used by "service functions" to create a new iam repo
	IamRepoFactory func() (*iam.Repository, error)

	// AuthTokenRepoFactory is used by "service functions" to create a new auth token repo
	AuthTokenRepoFactory func() (*authtoken.Repository, error)
)

// validator defines an optional interface that proto messages can implement
// which will allow encryptMessage to validate them before encryption.
type validator interface {
	Validate() error
}

// encryptMessage will encrypt the message.  The encrypted message will be wrapped in a
// request.Wrapper and then encoded into the returned string. This function
// supports encrypting: request.State and request.Token messages.  proto Messages should
// implement the validator interface, so they can be validated before encryption.
func encryptMessage(ctx context.Context, wrapper wrapping.Wrapper, am *AuthMethod, m proto.Message) (encodedEncryptedState string, e error) {
	const op = "oidc.encryptMessage"
	if wrapper == nil {
		return "", errors.New(ctx, errors.InvalidParameter, op, "missing wrapper")
	}
	keyId, err := wrapper.KeyId(ctx)
	if err != nil {
		return "", errors.Wrap(ctx, err, op, errors.WithMsg("error fetching wrapper key id"))
	}
	if keyId == "" {
		return "", errors.New(ctx, errors.InvalidParameter, op, "missing wrapper key id")
	}
	if am == nil || am.AuthMethod == nil {
		return "", errors.New(ctx, errors.InvalidParameter, op, "missing auth method")
	}
	if am.ScopeId == "" {
		return "", errors.New(ctx, errors.InvalidParameter, op, "missing auth method scope id")
	}
	if am.PublicId == "" {
		return "", errors.New(ctx, errors.InvalidParameter, op, "missing auth method public id")
	}
	if m == nil {
		return "", errors.New(ctx, errors.InvalidParameter, op, "missing message to encrypt")
	}
	switch v := m.(type) {
	case *request.State, *request.Token:
	default:
		return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported message type %v for encryption", v))
	}

	v, ok := m.(validator)
	if ok {
		if err := v.Validate(); err != nil {
			return "", errors.Wrap(ctx, err, op)
		}
	}
	marshaled, err := proto.Marshal(m)
	if err != nil {
		return "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to marshal message"), errors.WithCode(errors.Encode))
	}
	blobInfo, err := wrapper.Encrypt(ctx, marshaled, wrapping.WithAad([]byte(fmt.Sprintf("%s%s", am.PublicId, am.ScopeId))))
	if err != nil {
		return "", errors.New(ctx, errors.Encrypt, op, "unable to encrypt message", errors.WithWrap(err))
	}
	marshaledBlob, err := proto.Marshal(blobInfo)
	if err != nil {
		return "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to marshal blob"), errors.WithCode(errors.Encode))
	}
	wrapped := &request.Wrapper{
		AuthMethodId: am.PublicId,
		ScopeId:      am.ScopeId,
		WrapperKeyId: keyId,
		Ct:           marshaledBlob,
	}
	if err := wrapped.Validate(ctx); err != nil {
		return "", errors.Wrap(ctx, err, op)
	}
	marshaledEncryptedSt, err := proto.Marshal(wrapped)
	if err != nil {
		return "", errors.New(ctx, errors.Unknown, op, "unable to marshal encrypted message", errors.WithWrap(err))
	}

	return base58.FastBase58Encoding(marshaledEncryptedSt), nil
}

// decryptMessage will decrypt messages that were previously encrypted with oidc.encryptMessage(...)
// The returned messageBytes can be marshaled via proto.Marshal into the appropriate message.
func decryptMessage(ctx context.Context, wrappingWrapper wrapping.Wrapper, wrappedRequest *request.Wrapper) (messageBytes []byte, e error) {
	const op = "oidc.decryptMessage"
	if wrappedRequest == nil {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapped request")
	}
	if wrappingWrapper == nil {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing wrapping wrapper")
	}
	var blobInfo wrapping.BlobInfo
	if err := proto.Unmarshal(wrappedRequest.Ct, &blobInfo); err != nil {
		return nil, errors.New(ctx, errors.Unknown, op, "unable to marshal blob info", errors.WithWrap(err))
	}

	decryptedMsg, err := wrappingWrapper.Decrypt(ctx, &blobInfo, wrapping.WithAad([]byte(fmt.Sprintf("%s%s", wrappedRequest.AuthMethodId, wrappedRequest.ScopeId))))
	if err != nil {
		return nil, errors.New(ctx, errors.Decrypt, op, "unable to decrypt message", errors.WithWrap(err))
	}
	return decryptedMsg, nil
}

// UnwrapMessage does just that, it unwraps the encoded request.Wrapper proto message
func UnwrapMessage(ctx context.Context, encodedWrappedMsg string) (*request.Wrapper, error) {
	const op = ""
	decoded, err := base58.FastBase58Decoding(encodedWrappedMsg)
	if err != nil {
		return nil, errors.New(ctx, errors.Unknown, op, "unable to decode message", errors.WithWrap(err))
	}
	var wrapper request.Wrapper
	if err := proto.Unmarshal(decoded, &wrapper); err != nil {
		return nil, errors.New(ctx, errors.Unknown, op, "unable to marshal encoded/encrypted message", errors.WithWrap(err))
	}
	return &wrapper, nil
}

// requestWrappingWrapper finds the wrapping wrapper to use when encrypting/decrypting
// both a Request.State and Request.Token.
//
// It first checks the cache of derived wrappers. If it's not found in the cache, it
// generates a key based on the scope's oidc DEK, using the scopeId and authMethod
// as salt and info for derivation, creates a wrapper for the new key, adds that wrapper
// to the cache and the returns the wrapper for the new key.
//
// It supports the WithKeyId(...) option which allows you to specify which oidc DEK
// to use vs the default of just using the latest version of the DEK.
func requestWrappingWrapper(ctx context.Context, k *kms.Kms, scopeId, authMethodId string, opt ...Option) (wrapping.Wrapper, error) {
	const op = "oidc.(Repository).oidcWrapper"
	if k == nil {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing kms")
	}
	if scopeId == "" {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
	}
	if authMethodId == "" {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
	}
	opts := getOpts(opt...)
	// get a specific oidcWrapper using the WithKeyId(...) option
	oidcWrapper, err := k.GetWrapper(ctx, scopeId, kms.KeyPurposeOidc, kms.WithKeyId(opts.withKeyId))
	if err != nil {
		return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oidc wrapper"))
	}

	// What derived key are we looking for?
	keyId, err := oidcWrapper.KeyId(ctx)
	if err != nil {
		return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oidc wrapper key id"))
	}
	keyId = derivedKeyId(derivedKeyPurposeState, keyId, authMethodId)
	derivedWrapper, ok := k.GetDerivedPurposeCache().Load(keyId)
	if ok {
		return derivedWrapper.(*aead.Wrapper), nil
	}

	// okay, I guess we need to derive a new key for this combo of oidcWrapper and authMethod
	reader, err := crypto.NewDerivedReader(ctx, oidcWrapper, 32, []byte(authMethodId), []byte(scopeId))
	if err != nil {
		return nil, errors.Wrap(ctx, err, op)
	}
	privKey, _, err := ed25519.GenerateKey(reader)
	if err != nil {
		return nil, errors.New(ctx, errors.Encrypt, op, "unable to generate key", errors.WithWrap(err))
	}
	wrapper := aead.NewWrapper()
	if _, err := wrapper.SetConfig(ctx, wrapping.WithKeyId(keyId)); err != nil {
		return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error setting config on aead wrapper in auth method %s", authMethodId)))
	}
	if err := wrapper.SetAesGcmKeyBytes(privKey); err != nil {
		return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("error setting key bytes on aead wrapper in auth method %s", authMethodId)))
	}
	// store the derived key in our cache
	k.GetDerivedPurposeCache().Store(keyId, wrapper)

	return wrapper, nil
}

// derivedKeyId returns a key that represents the derived key
func derivedKeyId(purpose derivedKeyPurpose, wrapperKeyId, authMethodId string) string {
	return fmt.Sprintf("%s.%s.%s", purpose.String(), wrapperKeyId, authMethodId)
}

// derivedKeyPurpose represents the purpose of the derived key.
//
// In the future, this could be moved to the kms package
// and exported for others to use.
type derivedKeyPurpose uint

const (
	derivedKeyPurposeUnknown = iota
	derivedKeyPurposeState
)

// String returns a representative string for the key's purpose
func (k derivedKeyPurpose) String() string {
	switch k {
	case derivedKeyPurposeState:
		return "oidc_state"
	default:
		return "oidc_unknown"
	}
}
