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

package crypto

import (
	"context"
	"crypto/ed25519"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io"

	wrapping "github.com/hashicorp/go-kms-wrapping/v2"
	"github.com/mr-tron/base58"
	"golang.org/x/crypto/blake2b"
)

// HmacSha256WithPrk will HmacSha256 using the provided prk.  See HmacSha256 for
// options supported.
func HmacSha256WithPrk(ctx context.Context, data, prk []byte, opt ...Option) (string, error) {
	opt = append(opt, WithPrk(prk))
	return HmacSha256(ctx, data, nil, nil, nil, opt...)
}

// HmacSha256 the provided data. Supports WithPrefix, WithEd25519 and WithPrk
// options. WithEd25519 is a "legacy" way to complete this operation and should
// not be used in new operations unless backward compatibility is needed. The
// WithPrefix option will prepend the prefix to the hmac-sha256 value.
func HmacSha256(ctx context.Context, data []byte, cipher wrapping.Wrapper, salt, info []byte, opt ...Option) (string, error) {
	const op = "crypto.HmacSha256"
	opts, err := getOpts(opt...)
	if err != nil {
		return "", fmt.Errorf("%s: unable to get options: %w", op, err)
	}
	if data == nil {
		return "", fmt.Errorf("%s: missing data: %w", op, ErrInvalidParameter)
	}
	if cipher == nil && opts.withPrk == nil {
		return "", fmt.Errorf("%s: you must specify either a wrapper or prk: %w", op, ErrInvalidParameter)
	}
	if cipher != nil && opts.withPrk != nil {
		return "", fmt.Errorf("%s: you cannot specify both a wrapper or prk: %w", op, ErrInvalidParameter)
	}
	if opts.withEd25519 && opts.withPrk != nil {
		return "", fmt.Errorf("%s: you cannot specify both ed25519 and a prk: %w", op, ErrInvalidParameter)
	}
	var key [32]byte
	switch {
	case opts.withPrk != nil:
		key = blake2b.Sum256(opts.withPrk)

	case opts.withEd25519:
		reader, err := NewDerivedReader(ctx, cipher, 32, salt, info)
		if err != nil {
			return "", fmt.Errorf("%s: %w", op, err)
		}
		edKey, _, err := ed25519.GenerateKey(reader)
		if err != nil {
			return "", fmt.Errorf("%s: unable to generate derived key: %w", op, ErrInvalidParameter)
		}
		n := copy(key[:], edKey)
		if n != 32 {
			return "", fmt.Errorf("%s: expected to copy 32 bytes and got: %d", op, n)
		}

	default:
		reader, err := NewDerivedReader(ctx, cipher, 32, salt, info)
		if err != nil {
			return "", fmt.Errorf("%s: %w", op, err)
		}
		readerKey := make([]byte, 32)
		n, err := io.ReadFull(reader, readerKey)
		if err != nil {
			return "", fmt.Errorf("%s: %w", op, err)
		}
		if n != 32 {
			return "", fmt.Errorf("%s: expected to read 32 bytes and got: %d", op, n)
		}
		key = blake2b.Sum256(readerKey)
	}
	mac := hmac.New(sha256.New, key[:])
	_, _ = mac.Write(data)
	hmac := mac.Sum(nil)

	var hmacString string
	switch {
	case opts.withBase64Encoding:
		hmacString = base64.RawURLEncoding.EncodeToString(hmac)
	case opts.withBase58Encoding:
		hmacString = base58.Encode(hmac)
	default:
		hmacString = string(hmac)
	}
	if opts.withPrefix != "" {
		return opts.withPrefix + hmacString, nil
	}
	return hmacString, nil
}
