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

package oidc

import (
	"context"
	"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/kms"
	"google.golang.org/protobuf/proto"
)

// TokenRequest is an oidc domain service function for processing a token
// request from a Boundary client.  Token requests are the result of a Boundary
// client polling the tokenUrl they received via StartAuth.  On success, it
// returns Boundary token.
//
// * Decrypt the tokenRequestId.  If encryption fails, it returns an error.
//
// * Use the authtoken.(Repository).IssueAuthToken to issue the request id's
// token and mark it as issued in the repo.  If the token is already issue, an
// error is returned.
func TokenRequest(ctx context.Context, kms *kms.Kms, atRepoFn AuthTokenRepoFactory, authMethodId, tokenRequestId string) (*authtoken.AuthToken, error) {
	const op = "oidc.TokenRequest"
	if kms == nil {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing kms")
	}
	if atRepoFn == nil {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token repo function")
	}
	if authMethodId == "" {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
	}
	if tokenRequestId == "" {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing token request id")
	}

	reqTkWrapper, err := UnwrapMessage(ctx, tokenRequestId)
	if err != nil {
		return nil, errors.Wrap(ctx, err, op)
	}
	if reqTkWrapper.ScopeId == "" {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "request token id wrapper missing scope id")
	}
	if reqTkWrapper.AuthMethodId == "" {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "request token id wrapper missing auth method id")
	}
	if reqTkWrapper.AuthMethodId != authMethodId {
		return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s auth method id does not match request wrapper auth method id: %s", authMethodId, reqTkWrapper.AuthMethodId))
	}

	// tokenRequestId is a proto request.Wrapper, which contains a cipher text field,
	// so we need the derived wrapper that was used to encrypt it.
	requestWrapper, err := requestWrappingWrapper(ctx, kms, reqTkWrapper.ScopeId, reqTkWrapper.AuthMethodId)
	if err != nil {
		return nil, errors.Wrap(ctx, err, op)
	}
	reqTkBytes, err := decryptMessage(ctx, requestWrapper, reqTkWrapper)
	if err != nil {
		return nil, errors.Wrap(ctx, err, op)
	}
	var reqTk request.Token
	if err := proto.Unmarshal(reqTkBytes, &reqTk); err != nil {
		return nil, errors.New(ctx, errors.Unknown, op, "unable to unmarshal request token", errors.WithWrap(err))
	}

	if reqTk.ExpirationTime == nil {
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing request token id expiration time")
	}

	// before proceeding, make sure the request hasn't timed out
	if time.Now().After(reqTk.ExpirationTime.Timestamp.AsTime()) {
		return nil, errors.New(ctx, errors.AuthAttemptExpired, op, "request token id has expired")
	}

	tokenRepo, err := atRepoFn()
	if err != nil {
		return nil, errors.Wrap(ctx, err, op)
	}
	authTk, err := tokenRepo.IssueAuthToken(ctx, reqTk.RequestId)
	if err != nil {
		if errors.Match(errors.T(errors.RecordNotFound), err) {
			// We don't have it -- at least not yet. So don't mark it as an
			// error, but nothing is returned.
			return nil, nil
		}
		return nil, errors.Wrap(ctx, err, op)
	}
	if authTk.Token == "" {
		return nil, errors.New(ctx, errors.Internal, op, "issued token is missing")
	}
	return authTk, nil
}
