package crypto

import (
	"bytes"
	"crypto"
	"fmt"
	"io"
	"math"
	"time"

	"github.com/ProtonMail/go-crypto/openpgp"
	pgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors"
	"github.com/ProtonMail/go-crypto/openpgp/packet"
	"github.com/pkg/errors"

	"github.com/ProtonMail/gopenpgp/v2/constants"
	"github.com/ProtonMail/gopenpgp/v2/internal"
)

// allowedHashes stores the allowed OpenPGP hashes that can be used in a
// message signature hash.
var allowedHashes = []crypto.Hash{
	crypto.SHA224,
	crypto.SHA256,
	crypto.SHA384,
	crypto.SHA512,
	crypto.SHA3_256,
	crypto.SHA3_512,
}

// SignatureVerificationError is returned from Decrypt and VerifyDetached
// functions when signature verification fails.
type SignatureVerificationError struct {
	Status  int
	Message string
	Cause   error
}

// Error is the base method for all errors.
func (e SignatureVerificationError) Error() string {
	if e.Cause != nil {
		return fmt.Sprintf("Signature Verification Error: %v caused by %v", e.Message, e.Cause)
	}
	return fmt.Sprintf("Signature Verification Error: %v", e.Message)
}

// Unwrap returns the cause of failure.
func (e SignatureVerificationError) Unwrap() error {
	return e.Cause
}

// ------------------
// Internal functions
// ------------------

// newSignatureFailed creates a new SignatureVerificationError, type
// SignatureFailed.
func newSignatureBadContext(cause error) SignatureVerificationError {
	return SignatureVerificationError{
		Status:  constants.SIGNATURE_BAD_CONTEXT,
		Message: "Invalid signature context",
		Cause:   cause,
	}
}

func newSignatureFailed(cause error) SignatureVerificationError {
	return SignatureVerificationError{
		Status:  constants.SIGNATURE_FAILED,
		Message: "Invalid signature",
		Cause:   cause,
	}
}

// newSignatureInsecure creates a new SignatureVerificationError, type
// SignatureFailed, with a message describing the signature as insecure.
func newSignatureInsecure() SignatureVerificationError {
	return SignatureVerificationError{
		Status:  constants.SIGNATURE_FAILED,
		Message: "Insecure signature",
	}
}

// newSignatureNotSigned creates a new SignatureVerificationError, type
// SignatureNotSigned.
func newSignatureNotSigned() SignatureVerificationError {
	return SignatureVerificationError{
		Status:  constants.SIGNATURE_NOT_SIGNED,
		Message: "Missing signature",
	}
}

// newSignatureNoVerifier creates a new SignatureVerificationError, type
// SignatureNoVerifier.
func newSignatureNoVerifier() SignatureVerificationError {
	return SignatureVerificationError{
		Status:  constants.SIGNATURE_NO_VERIFIER,
		Message: "No matching signature",
	}
}

// processSignatureExpiration handles signature time verification manually, so
// we can add a margin to the creationTime check.
func processSignatureExpiration(md *openpgp.MessageDetails, verifyTime int64) {
	if !errors.Is(md.SignatureError, pgpErrors.ErrSignatureExpired) {
		return
	}
	if verifyTime == 0 {
		// verifyTime = 0: time check disabled, everything is okay
		md.SignatureError = nil
		return
	}
	created := md.Signature.CreationTime.Unix()
	expires := int64(math.MaxInt64)
	if md.Signature.SigLifetimeSecs != nil {
		expires = int64(*md.Signature.SigLifetimeSecs) + created
	}
	if created-internal.CreationTimeOffset <= verifyTime && verifyTime <= expires {
		md.SignatureError = nil
	}
}

// verifyDetailsSignature verifies signature from message details.
func verifyDetailsSignature(md *openpgp.MessageDetails, verifierKey *KeyRing, verificationContext *VerificationContext) error {
	if !md.IsSigned {
		return newSignatureNotSigned()
	}
	if md.SignedBy == nil ||
		len(verifierKey.entities) == 0 ||
		len(verifierKey.entities.KeysById(md.SignedByKeyId)) == 0 {
		return newSignatureNoVerifier()
	}
	if md.SignatureError != nil {
		return newSignatureFailed(md.SignatureError)
	}
	if md.Signature == nil ||
		md.Signature.Hash < allowedHashes[0] ||
		md.Signature.Hash > allowedHashes[len(allowedHashes)-1] {
		return newSignatureInsecure()
	}
	if verificationContext != nil {
		err := verificationContext.verifyContext(md.Signature)
		if err != nil {
			return newSignatureBadContext(err)
		}
	}

	return nil
}

// SigningContext gives the context that will be
// included in the signature's notation data.
type SigningContext struct {
	Value      string
	IsCritical bool
}

// NewSigningContext creates a new signing context.
// The value is set to the notation data.
// isCritical controls whether the notation is flagged as a critical packet.
func NewSigningContext(value string, isCritical bool) *SigningContext {
	return &SigningContext{Value: value, IsCritical: isCritical}
}

func (context *SigningContext) getNotation() *packet.Notation {
	return &packet.Notation{
		Name:            constants.SignatureContextName,
		Value:           []byte(context.Value),
		IsCritical:      context.IsCritical,
		IsHumanReadable: true,
	}
}

// VerificationContext gives the context that will be
// used to verify the signature.
type VerificationContext struct {
	Value         string
	IsRequired    bool
	RequiredAfter int64
}

// NewVerificationContext creates a new verification context.
// The value is checked against the signature's notation data.
// If isRequired is false, the signature is allowed to have no context set.
// If requiredAfter is != 0, the signature is allowed to have no context set if it
// was created before the unix time set in requiredAfter.
func NewVerificationContext(value string, isRequired bool, requiredAfter int64) *VerificationContext {
	return &VerificationContext{
		Value:         value,
		IsRequired:    isRequired,
		RequiredAfter: requiredAfter,
	}
}

func (context *VerificationContext) isRequiredAtTime(signatureTime time.Time) bool {
	return context.IsRequired &&
		(context.RequiredAfter == 0 || signatureTime.After(time.Unix(context.RequiredAfter, 0)))
}

func findContext(notations []*packet.Notation) (string, error) {
	context := ""
	for _, notation := range notations {
		if notation.Name == constants.SignatureContextName {
			if context != "" {
				return "", errors.New("gopenpgp: signature has multiple context notations")
			}
			if !notation.IsHumanReadable {
				return "", errors.New("gopenpgp: context notation was not set as human-readable")
			}
			context = string(notation.Value)
		}
	}
	return context, nil
}

func (context *VerificationContext) verifyContext(sig *packet.Signature) error {
	signatureContext, err := findContext(sig.Notations)
	if err != nil {
		return err
	}
	if signatureContext != context.Value {
		contextRequired := context.isRequiredAtTime(sig.CreationTime)
		if contextRequired {
			return errors.New("gopenpgp: signature did not have the required context")
		} else if signatureContext != "" {
			return errors.New("gopenpgp: signature had a wrong context")
		}
	}

	return nil
}

// verifySignature verifies if a signature is valid with the entity list.
func verifySignature(
	pubKeyEntries openpgp.EntityList,
	origText io.Reader,
	signature []byte,
	verifyTime int64,
	verificationContext *VerificationContext,
) (*packet.Signature, error) {
	config := &packet.Config{}
	if verifyTime == 0 {
		config.Time = func() time.Time {
			return time.Unix(0, 0)
		}
	} else {
		config.Time = func() time.Time {
			return time.Unix(verifyTime+internal.CreationTimeOffset, 0)
		}
	}

	if verificationContext != nil {
		config.KnownNotations = map[string]bool{constants.SignatureContextName: true}
	}
	signatureReader := bytes.NewReader(signature)

	sig, signer, err := openpgp.VerifyDetachedSignatureAndHash(pubKeyEntries, origText, signatureReader, allowedHashes, config)

	if sig != nil && signer != nil && (errors.Is(err, pgpErrors.ErrSignatureExpired) || errors.Is(err, pgpErrors.ErrKeyExpired)) { //nolint:nestif
		if verifyTime == 0 { // Expiration check disabled
			err = nil
		} else {
			// Maybe the creation time offset pushed it over the edge
			// Retry with the actual verification time
			config.Time = func() time.Time {
				return time.Unix(verifyTime, 0)
			}

			seeker, ok := origText.(io.ReadSeeker)
			if !ok {
				return nil, errors.Wrap(err, "gopenpgp: message reader do not support seeking, cannot retry signature verification")
			}

			_, err = seeker.Seek(0, io.SeekStart)
			if err != nil {
				return nil, newSignatureFailed(errors.Wrap(err, "gopenpgp: could not rewind the data reader."))
			}

			_, err = signatureReader.Seek(0, io.SeekStart)
			if err != nil {
				return nil, newSignatureFailed(err)
			}

			sig, signer, err = openpgp.VerifyDetachedSignatureAndHash(pubKeyEntries, seeker, signatureReader, allowedHashes, config)
		}
	}

	if err != nil {
		return nil, newSignatureFailed(err)
	}

	if sig == nil || signer == nil {
		return nil, newSignatureFailed(errors.New("gopenpgp: no signer or valid signature"))
	}

	if verificationContext != nil {
		err := verificationContext.verifyContext(sig)
		if err != nil {
			return nil, newSignatureBadContext(err)
		}
	}

	return sig, nil
}

func signMessageDetached(
	signKeyRing *KeyRing,
	messageReader io.Reader,
	isBinary bool,
	context *SigningContext,
) (*PGPSignature, error) {
	config := &packet.Config{
		DefaultHash: crypto.SHA512,
		Time:        getTimeGenerator(),
	}

	signEntity, err := signKeyRing.getSigningEntity()
	if err != nil {
		return nil, err
	}

	if context != nil {
		config.SignatureNotations = append(config.SignatureNotations, context.getNotation())
	}

	var outBuf bytes.Buffer
	if isBinary {
		err = openpgp.DetachSign(&outBuf, signEntity, messageReader, config)
	} else {
		err = openpgp.DetachSignText(&outBuf, signEntity, messageReader, config)
	}
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in signing")
	}

	return NewPGPSignature(outBuf.Bytes()), nil
}
