package client

import (
	"crypto"
	"crypto/rsa"
	"encoding/asn1"
	"fmt"
	"io"
	"math/big"
	"sync"

	"github.com/google/go-tpm-tools/internal"
	"github.com/google/go-tpm/legacy/tpm2"
)

// Global mutex to protect against concurrent TPM access.
var signerMutex sync.Mutex

type tpmSigner struct {
	Key  *Key
	Hash crypto.Hash
}

// Public returns the tpmSigners public key.
func (signer *tpmSigner) Public() crypto.PublicKey {
	return signer.Key.PublicKey()
}

// Sign uses the TPM key to sign the digest.
// The digest must be hashed from the same hash algorithm as the keys scheme.
// The opts hash function must also match the keys scheme (or be nil).
// Concurrent use of Sign is thread safe, but it is not safe to access the TPM
// from other sources while Sign is executing.
// For RSAPSS signatures, you cannot specify custom salt lengths. The salt
// length will be (keyBits/8) - digestSize - 2, unless that is less than the
// digestSize in which case, saltLen will be digestSize. The only normal case
// where saltLen is not digestSize is when using 1024 keyBits with SHA512.
func (signer *tpmSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
	if pssOpts, ok := opts.(*rsa.PSSOptions); ok {
		if signer.Key.pubArea.RSAParameters == nil {
			return nil, fmt.Errorf("invalid options: PSSOptions can only be used with RSA keys")
		}
		if signer.Key.pubArea.RSAParameters.Sign.Alg != tpm2.AlgRSAPSS {
			return nil, fmt.Errorf("invalid options: PSSOptions cannot be used with signing alg: %v", signer.Key.pubArea.RSAParameters.Sign.Alg)
		}
		if pssOpts.SaltLength != rsa.PSSSaltLengthAuto {
			return nil, fmt.Errorf("salt length must be rsa.PSSSaltLengthAuto")
		}
	}
	if opts != nil && opts.HashFunc() != signer.Hash {
		return nil, fmt.Errorf("hash algorithm: got %v, want %v", opts.HashFunc(), signer.Hash)
	}
	if len(digest) != signer.Hash.Size() {
		return nil, fmt.Errorf("digest length: got %d, want %d", len(digest), signer.Hash.Size())
	}

	signerMutex.Lock()
	defer signerMutex.Unlock()

	auth, err := signer.Key.session.Auth()
	if err != nil {
		return nil, err
	}

	sig, err := tpm2.SignWithSession(signer.Key.rw, auth.Session, signer.Key.handle, "", digest, nil, nil)
	if err != nil {
		return nil, err
	}
	return getSignature(sig)
}

// GetSigner returns a crypto.Signer wrapping the loaded TPM Key.
// Concurrent use of one or more Signers is thread safe, but it is not safe to
// access the TPM from other sources while using a Signer.
// The returned Signer lasts the lifetime of the Key, and will no longer work
// once the Key has been closed.
func (k *Key) GetSigner() (crypto.Signer, error) {
	if k.hasAttribute(tpm2.FlagRestricted) {
		return nil, fmt.Errorf("restricted keys are not supported")
	}
	hashAlg, err := internal.GetSigningHashAlg(k.pubArea)
	if err != nil {
		return nil, err
	}
	// For crypto.Signer, Go does the hashing. Make sure the hash is supported.
	hash, err := hashAlg.Hash()
	if err != nil {
		return nil, err
	}
	return &tpmSigner{k, hash}, nil
}

// SignData signs a data buffer with a TPM loaded key. Unlike GetSigner, this
// method works with restricted and unrestricted keys. If this method is called
// on a restriced key, the TPM itself will hash the provided data, failing the
// signing operation if the data begins with TPM_GENERATED_VALUE.
func (k *Key) SignData(data []byte) ([]byte, error) {
	hashAlg, err := internal.GetSigningHashAlg(k.pubArea)
	if err != nil {
		return nil, err
	}

	var digest []byte
	var ticket *tpm2.Ticket
	if k.hasAttribute(tpm2.FlagRestricted) {
		// Restricted keys can only sign data hashed by the TPM. We use the
		// owner hierarchy for the Ticket, but any non-Null hierarchy would do.
		digest, ticket, err = tpm2.Hash(k.rw, hashAlg, data, tpm2.HandleOwner)
		if err != nil {
			return nil, err
		}
	} else {
		// Unrestricted keys can sign any digest, no need for TPM hashing.
		hash, err := hashAlg.Hash()
		if err != nil {
			return nil, err
		}
		hasher := hash.New()
		hasher.Write(data)
		digest = hasher.Sum(nil)
	}

	auth, err := k.session.Auth()
	if err != nil {
		return nil, err
	}
	sig, err := tpm2.SignWithSession(k.rw, auth.Session, k.handle, "", digest, ticket, nil)
	if err != nil {
		return nil, err
	}
	return getSignature(sig)
}

func getSignature(sig *tpm2.Signature) ([]byte, error) {
	switch sig.Alg {
	case tpm2.AlgRSASSA:
		return sig.RSA.Signature, nil
	case tpm2.AlgRSAPSS:
		return sig.RSA.Signature, nil
	case tpm2.AlgECDSA:
		sigStruct := struct{ R, S *big.Int }{sig.ECC.R, sig.ECC.S}
		return asn1.Marshal(sigStruct)
	default:
		return nil, fmt.Errorf("unsupported signing algorithm: %v", sig.Alg)
	}
}
