package sign

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha1" // nolint:gosec
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"os"
)

var (
	errNoPemBlock   = errors.New("no PEM block found")
	errDigestNotSH1 = errors.New("digest is not a SHA1 hash")
	errNoPassphrase = errors.New("key is encrypted but no passphrase was provided")
	errNoRSAKey     = errors.New("key is not an RSA key")
)

const (
	PKCS1PrivkeyPreamble = "RSA PRIVATE KEY"
	PKCS8PrivkeyPreamble = "PRIVATE KEY"
)

// RSASignSHA1Digest signs the provided SHA1 message digest. The key file
// must be in the PEM format and can either be encrypted or not.
func RSASignSHA1Digest(sha1Digest []byte, keyFile, passphrase string) ([]byte, error) {
	if len(sha1Digest) != sha1.Size {
		return nil, errDigestNotSH1
	}

	keyFileContent, err := os.ReadFile(keyFile)
	if err != nil {
		return nil, fmt.Errorf("reading key file: %w", err)
	}

	block, _ := pem.Decode(keyFileContent)
	if block == nil {
		return nil, errNoPemBlock
	}

	blockData := block.Bytes
	if x509.IsEncryptedPEMBlock(block) { //nolint:staticcheck
		if passphrase == "" {
			return nil, errNoPassphrase
		}

		var decryptedBlockData []byte

		decryptedBlockData, err = x509.DecryptPEMBlock(block, []byte(passphrase)) //nolint:staticcheck
		if err != nil {
			return nil, fmt.Errorf("decrypt private key PEM block: %w", err)
		}

		blockData = decryptedBlockData
	}

	var priv crypto.Signer

	switch block.Type {
	case PKCS1PrivkeyPreamble:
		priv, err = x509.ParsePKCS1PrivateKey(blockData)
		if err != nil {
			return nil, fmt.Errorf("parse PKCS#1 private key: %w", err)
		}
	case PKCS8PrivkeyPreamble:
		privAny, err := x509.ParsePKCS8PrivateKey(blockData)
		if err != nil {
			return nil, fmt.Errorf("parse PKCS#8 private key: %w", err)
		}
		privTmp, ok := privAny.(crypto.Signer)
		if !ok {
			return nil, fmt.Errorf("cannot sign with given private key")
		}
		priv = privTmp
	default:
		return nil, fmt.Errorf(`key type "%v" is not supported`, block.Type)
	}

	signature, err := priv.Sign(rand.Reader, sha1Digest, crypto.SHA1)
	if err != nil {
		return nil, fmt.Errorf("signing: %w", err)
	}

	return signature, nil
}

func rsaSign(message io.Reader, keyFile, passphrase string) ([]byte, error) {
	sha1Hash := sha1.New() // nolint:gosec
	_, err := io.Copy(sha1Hash, message)
	if err != nil {
		return nil, fmt.Errorf("create SHA1 message digest: %w", err)
	}

	return RSASignSHA1Digest(sha1Hash.Sum(nil), keyFile, passphrase)
}

// RSAVerifySHA1Digest is exported for use in tests and verifies a signature over the
// provided SHA1 hash of a message. The key file must be in the PEM format.
func RSAVerifySHA1Digest(sha1Digest, signature []byte, publicKeyFile string) error {
	if len(sha1Digest) != sha1.Size {
		return errDigestNotSH1
	}

	keyFileContent, err := os.ReadFile(publicKeyFile)
	if err != nil {
		return fmt.Errorf("reading key file: %w", err)
	}

	block, _ := pem.Decode(keyFileContent)
	if block == nil {
		return errNoPemBlock
	}

	pub, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return fmt.Errorf("parse PKIX public key: %w", err)
	}

	rsaPub, ok := pub.(*rsa.PublicKey)
	if !ok {
		return errNoRSAKey
	}

	err = rsa.VerifyPKCS1v15(rsaPub, crypto.SHA1, sha1Digest, signature)
	if err != nil {
		return fmt.Errorf("verify PKCS1v15 signature: %w", err)
	}

	return nil
}

func rsaVerify(message io.Reader, signature []byte, publicKeyFile string) error {
	sha1Hash := sha1.New() // nolint:gosec
	_, err := io.Copy(sha1Hash, message)
	if err != nil {
		return fmt.Errorf("create SHA1 message digest: %w", err)
	}

	return RSAVerifySHA1Digest(sha1Hash.Sum(nil), signature, publicKeyFile)
}
