// Package blobcrypto performs whole-blob crypto operations.
package blobcrypto

import (
	"crypto/aes"
	"encoding/hex"
	"strings"

	"github.com/pkg/errors"

	"github.com/kopia/kopia/internal/gather"
	"github.com/kopia/kopia/repo/blob"
	"github.com/kopia/kopia/repo/encryption"
	"github.com/kopia/kopia/repo/hashing"
)

// Crypter encapsulates hashing and encryption.
type Crypter interface {
	HashFunc() hashing.HashFunc
	Encryptor() encryption.Encryptor
}

// getIndexBlobIV gets the initialization vector from the provided blob ID by taking
// 32 characters immediately preceding the first dash ('-') and decoding them using base16.
func getIndexBlobIV(s blob.ID) ([]byte, error) {
	if p := strings.Index(string(s), "-"); p >= 0 { //nolint:gocritic
		s = s[0:p]
	}

	if len(s) < 2*aes.BlockSize {
		return nil, errors.Errorf("blob id too short: %v", s)
	}

	v, err := hex.DecodeString(string(s[len(s)-(aes.BlockSize*2):])) //nolint:mnd
	if err != nil {
		return nil, errors.Errorf("invalid blob ID: %v", s)
	}

	return v, nil
}

// Encrypt encrypts the given data using crypter-defined key and returns a name that should
// be used to save the blob in the repository.
func Encrypt(c Crypter, payload gather.Bytes, prefix, suffix blob.ID, output *gather.WriteBuffer) (blob.ID, error) {
	var hashOutput [hashing.MaxHashSize]byte

	hash := c.HashFunc()(hashOutput[:0], payload)
	blobID := prefix + blob.ID(hex.EncodeToString(hash))

	if suffix != "" {
		blobID += "-" + suffix
	}

	iv, err := getIndexBlobIV(blobID)
	if err != nil {
		return "", err
	}

	output.Reset()

	if err := c.Encryptor().Encrypt(payload, iv, output); err != nil {
		return "", errors.Wrapf(err, "error encrypting BLOB %v", blobID)
	}

	return blobID, nil
}

// Decrypt decrypts the provided data using provided blobID to derive initialization vector.
func Decrypt(c Crypter, payload gather.Bytes, blobID blob.ID, output *gather.WriteBuffer) error {
	iv, err := getIndexBlobIV(blobID)
	if err != nil {
		return errors.Wrap(err, "unable to get index blob IV")
	}

	output.Reset()

	// Decrypt will verify the payload.
	if err := c.Encryptor().Decrypt(payload, iv, output); err != nil {
		return errors.Wrapf(err, "error decrypting BLOB %v", blobID)
	}

	return nil
}
