package pkcs

import (
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"crypto/x509/pkix"
	"encoding/asn1"
	"errors"
	"fmt"
	"hash"
	"io"
	"strconv"

	"github.com/emmansun/gmsm/sm3"
)

var (
	oidPBES2  = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}
	oidSMPBES = asn1.ObjectIdentifier{1, 2, 156, 10197, 6, 4, 1, 5, 2}
)

// Hash identifies a cryptographic hash function that is implemented in another
// package.
type Hash uint

const (
	SHA1 Hash = 1 + iota
	SHA224
	SHA256
	SHA384
	SHA512
	SHA512_224
	SHA512_256
	SM3
)

// New returns a new hash.Hash calculating the given hash function. New panics
// if the hash function is not linked into the binary.
func (h Hash) New() hash.Hash {
	switch h {
	case SM3:
		return sm3.New()
	case SHA1:
		return sha1.New()
	case SHA224:
		return sha256.New224()
	case SHA256:
		return sha256.New()
	case SHA384:
		return sha512.New384()
	case SHA512:
		return sha512.New()
	case SHA512_224:
		return sha512.New512_224()
	case SHA512_256:
		return sha512.New512_256()

	}
	panic("pbes: requested hash function #" + strconv.Itoa(int(h)) + " is unavailable")
}

var (
	ErrPBEDecryption = errors.New("pbes: decryption error, please verify the password and try again")
)

// PBES2Params contains algorithm identifiers and related parameters for PBKDF2 key derivation function.
//
//	PBES2-params ::= SEQUENCE {
//		keyDerivationFunc AlgorithmIdentifier {{PBES2-KDFs}},
//		encryptionScheme AlgorithmIdentifier {{PBES2-Encs}}
//	}
type PBES2Params struct {
	KeyDerivationFunc pkix.AlgorithmIdentifier
	EncryptionScheme  pkix.AlgorithmIdentifier
}

// PBES2Opts contains options for encrypting a key using PBES2.
type PBES2Opts struct {
	Cipher
	KDFOpts
	pbesOID asn1.ObjectIdentifier
}

// DefaultOpts are the default options for encrypting a key if none are given.
// The defaults can be changed by the library user.
var DefaultOpts = &PBES2Opts{
	Cipher: AES256CBC,
	KDFOpts: PBKDF2Opts{
		SaltSize:       16,
		IterationCount: 2048,
		HMACHash:       SHA256,
		pbkdfOID:       oidPKCS5PBKDF2,
	},
	pbesOID: oidPBES2,
}

// NewPBESEncrypter returns a new PBESEncrypter with the given cipher and KDF options.
func NewPBESEncrypter(cipher Cipher, kdfOpts KDFOpts) PBESEncrypter {
	return &PBES2Opts{
		Cipher:  cipher,
		KDFOpts: kdfOpts,
		pbesOID: oidPBES2,
	}
}

// NewSMPBESEncrypterWithKDF returns a new SMPBESEncrypter (ShangMi PBES Encrypter) with the given KDF options.
func NewSMPBESEncrypterWithKDF(kdfOpts KDFOpts) PBESEncrypter {
	return &PBES2Opts{
		Cipher:  SM4CBC,
		KDFOpts: kdfOpts,
		pbesOID: oidSMPBES,
	}
}

// NewSMPBESEncrypter returns a new SMPBESEncrypter (ShangMi PBES Encrypter) with the given salt size and iteration count.
func NewSMPBESEncrypter(saltSize, iterationCount int) PBESEncrypter {
	return NewSMPBESEncrypterWithKDF(NewSMPBKDF2Opts(saltSize, iterationCount))
}

// KDFOpts contains options for a key derivation function.
// An implementation of this interface must be specified when encrypting a PKCS#8 key.
type KDFOpts interface {
	// DeriveKey derives a key of size bytes from the given password and salt.
	// It returns the key and the ASN.1-encodable parameters used.
	DeriveKey(password, salt []byte, size int) (key []byte, params KDFParameters, err error)
	// GetSaltSize returns the salt size specified.
	GetSaltSize() int
	// OID returns the OID of the KDF specified.
	OID() asn1.ObjectIdentifier
}

type PBESEncrypter interface {
	Encrypt(rand io.Reader, password, plaintext []byte) (*pkix.AlgorithmIdentifier, []byte, error)
}

// KDFParameters contains parameters (salt, etc.) for a key deriviation function.
// It must be a ASN.1-decodable structure.
// An implementation of this interface is created when decoding an encrypted PKCS#8 key.
type KDFParameters interface {
	// DeriveKey derives a key of size bytes from the given password.
	// It uses the salt from the decoded parameters.
	DeriveKey(oidKDF asn1.ObjectIdentifier, password []byte, size int) (key []byte, err error)
	// KeyLength returns the length of the derived key from the params.
	KeyLength() int
}

var kdfs = make(map[string]func() KDFParameters)

// RegisterKDF registers a function that returns a new instance of the given KDF
// parameters. This allows the library to support client-provided KDFs.
func RegisterKDF(oid asn1.ObjectIdentifier, params func() KDFParameters) {
	kdfs[oid.String()] = params
}

func (pbes2Params *PBES2Params) parseKeyDerivationFunc() (KDFParameters, error) {
	oid := pbes2Params.KeyDerivationFunc.Algorithm.String()
	newParams, ok := kdfs[oid]
	if !ok {
		return nil, fmt.Errorf("pbes: unsupported KDF (OID: %s)", oid)
	}
	params := newParams()
	_, err := asn1.Unmarshal(pbes2Params.KeyDerivationFunc.Parameters.FullBytes, params)
	if err != nil {
		return nil, errors.New("pbes: invalid KDF parameters")
	}
	return params, nil
}

// Decrypt decrypts the given ciphertext using the given password and the options specified.
func (pbes2Params *PBES2Params) Decrypt(password, ciphertext []byte) ([]byte, KDFParameters, error) {
	cipher, err := GetCipher(pbes2Params.EncryptionScheme)
	if err != nil {
		return nil, nil, err
	}

	kdfParams, err := pbes2Params.parseKeyDerivationFunc()
	if err != nil {
		return nil, nil, err
	}

	keySize := cipher.KeySize()
	symkey, err := kdfParams.DeriveKey(pbes2Params.KeyDerivationFunc.Algorithm, password, keySize)
	if err != nil {
		return nil, nil, err
	}

	plaintext, err := cipher.Decrypt(symkey, &pbes2Params.EncryptionScheme.Parameters, ciphertext)
	if err != nil {
		return nil, nil, ErrPBEDecryption
	}
	return plaintext, kdfParams, nil
}

func deriveKey(kdfOpts KDFOpts, rand io.Reader, password []byte, size int) ([]byte, *pkix.AlgorithmIdentifier, error) {
	// Generate a random salt
	salt := make([]byte, kdfOpts.GetSaltSize())
	if _, err := rand.Read(salt); err != nil {
		return nil, nil, err
	}
	key, kdfParams, err := kdfOpts.DeriveKey(password, salt, size)
	if err != nil {
		return nil, nil, err
	}
	marshalledParams, err := asn1.Marshal(kdfParams)
	if err != nil {
		return nil, nil, err
	}
	keyDerivationFunc := pkix.AlgorithmIdentifier{
		Algorithm:  kdfOpts.OID(),
		Parameters: asn1.RawValue{FullBytes: marshalledParams},
	}
	return key, &keyDerivationFunc, nil
}

// Encrypt encrypts the given plaintext using the given password and the options specified.
func (opts *PBES2Opts) Encrypt(rand io.Reader, password, plaintext []byte) (*pkix.AlgorithmIdentifier, []byte, error) {
	encAlg := opts.Cipher
	key, keyDerivationFunc, err := deriveKey(opts.KDFOpts, rand, password, encAlg.KeySize())
	if err != nil {
		return nil, nil, err
	}
	// Encrypt the plaintext
	encryptionScheme, ciphertext, err := encAlg.Encrypt(rand, key, plaintext)
	if err != nil {
		return nil, nil, err
	}

	encryptionAlgorithmParams := PBES2Params{
		EncryptionScheme:  *encryptionScheme,
		KeyDerivationFunc: *keyDerivationFunc,
	}
	marshalledEncryptionAlgorithmParams, err := asn1.Marshal(encryptionAlgorithmParams)
	if err != nil {
		return nil, nil, err
	}
	encryptionAlgorithm := pkix.AlgorithmIdentifier{
		Algorithm:  opts.pbesOID,
		Parameters: asn1.RawValue{FullBytes: marshalledEncryptionAlgorithmParams},
	}

	// fallback to default
	if len(encryptionAlgorithm.Algorithm) == 0 {
		encryptionAlgorithm.Algorithm = oidPBES2
	}

	return &encryptionAlgorithm, ciphertext, nil
}

func IsPBES2(algorithm pkix.AlgorithmIdentifier) bool {
	return oidPBES2.Equal(algorithm.Algorithm)
}

func IsSMPBES(algorithm pkix.AlgorithmIdentifier) bool {
	return oidSMPBES.Equal(algorithm.Algorithm)
}
