// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package gitinterface

import (
	"bytes"
	"context"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"strings"

	"github.com/ProtonMail/go-crypto/openpgp"
	"github.com/gittuf/gittuf/internal/signerverifier/common"
	"github.com/gittuf/gittuf/internal/signerverifier/sigstore"
	sslibsvssh "github.com/gittuf/gittuf/internal/signerverifier/ssh"
	"github.com/hiddeco/sshsig"
	"github.com/secure-systems-lab/go-securesystemslib/signerverifier"
	"github.com/sigstore/cosign/v2/pkg/cosign"
	gitsignVerifier "github.com/sigstore/gitsign/pkg/git"
	gitsignRekor "github.com/sigstore/gitsign/pkg/rekor"
	"github.com/sigstore/sigstore/pkg/fulcioroots"
	"golang.org/x/crypto/ssh"
)

const (
	rekorPublicGoodInstance           = "https://rekor.sigstore.dev"
	namespaceSSHSignature      string = "git"
	gpgPrivateKeyPEMHeader     string = "PGP PRIVATE KEY"
	opensshPrivateKeyPEMHeader string = "OPENSSH PRIVATE KEY"
	rsaPrivateKeyPEMHeader     string = "RSA PRIVATE KEY"
	genericPrivateKeyPEMHeader string = "PRIVATE KEY"
	signingFormatGPG           string = "gpg"
	signingFormatSSH           string = "ssh"
)

var (
	ErrNotCommitOrTag             = errors.New("invalid object type, expected commit or tag for signature verification")
	ErrSigningKeyNotSpecified     = errors.New("signing key not specified in git config")
	ErrUnknownSigningMethod       = errors.New("unknown signing method (not one of gpg, ssh, x509)")
	ErrIncorrectVerificationKey   = errors.New("incorrect key provided to verify signature")
	ErrVerifyingSigstoreSignature = errors.New("unable to verify Sigstore signature")
	ErrVerifyingSSHSignature      = errors.New("unable to verify SSH signature")
	ErrInvalidSignature           = errors.New("unable to parse signature / signature has unexpected header")
)

// CanSign inspects the Git configuration to determine if commit / tag signing
// is possible.
func (r *Repository) CanSign() error {
	config, err := r.GetGitConfig()
	if err != nil {
		return err
	}

	// Format is one of GPG, SSH, X509
	format := getSigningMethod(config)

	// If format is GPG or X509, the signing key parameter is optional
	// However, for SSH, the signing key must be set
	if format == signingFormatSSH {
		keyInfo := getSigningKeyInfo(config)
		if keyInfo == "" {
			return ErrSigningKeyNotSpecified
		}
	}

	return nil
}

// VerifySignature verifies the cryptographic signature associated with the
// specified object. The `objectID` must point to a Git commit or tag object.
func (r *Repository) VerifySignature(ctx context.Context, objectID Hash, key *signerverifier.SSLibKey) error {
	if err := r.ensureIsCommit(objectID); err == nil {
		return r.verifyCommitSignature(ctx, objectID, key)
	}

	if err := r.ensureIsTag(objectID); err == nil {
		return r.verifyTagSignature(ctx, objectID, key)
	}

	return ErrNotCommitOrTag
}

func signGitObjectUsingKey(contents, pemKeyBytes []byte) (string, error) {
	block, _ := pem.Decode(pemKeyBytes)
	if block == nil {
		// openpgp implements its own armor-decode method, pem.Decode considers
		// the input invalid. We haven't tested if this is universal, so in case
		// pem.Decode does succeed on a GPG key, we catch it below.
		return signGitObjectUsingGPGKey(contents, pemKeyBytes)
	}

	switch block.Type {
	case gpgPrivateKeyPEMHeader:
		return signGitObjectUsingGPGKey(contents, pemKeyBytes)
	case opensshPrivateKeyPEMHeader, rsaPrivateKeyPEMHeader, genericPrivateKeyPEMHeader:
		return signGitObjectUsingSSHKey(contents, pemKeyBytes)
	}

	return "", ErrUnknownSigningMethod
}

func signGitObjectUsingGPGKey(contents, pemKeyBytes []byte) (string, error) {
	reader := bytes.NewReader(contents)

	keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(pemKeyBytes))
	if err != nil {
		return "", err
	}

	sig := new(strings.Builder)
	if err := openpgp.ArmoredDetachSign(sig, keyring[0], reader, nil); err != nil {
		return "", err
	}

	return sig.String(), nil
}

func signGitObjectUsingSSHKey(contents, pemKeyBytes []byte) (string, error) {
	signer, err := ssh.ParsePrivateKey(pemKeyBytes)
	if err != nil {
		return "", err
	}

	sshSig, err := sshsig.Sign(bytes.NewReader(contents), signer, sshsig.HashSHA512, namespaceSSHSignature)
	if err != nil {
		return "", err
	}

	sigBytes := sshsig.Armor(sshSig)

	return string(sigBytes), nil
}

// verifyGitsignSignature handles the Sigstore-specific workflow involved in
// verifying commit or tag signatures issued by gitsign.
func verifyGitsignSignature(ctx context.Context, repo *Repository, key *signerverifier.SSLibKey, data, signature []byte) error {
	checkOpts := &cosign.CheckOpts{
		Identities: []cosign.Identity{{
			Issuer:  key.KeyVal.Issuer,
			Subject: key.KeyVal.Identity,
		}},
	}

	var verifier *gitsignVerifier.CertVerifier
	sigstoreRootFilePath := os.Getenv(sigstore.EnvSigstoreRootFile)
	if sigstoreRootFilePath == "" {
		root, err := fulcioroots.Get()
		if err != nil {
			return errors.Join(ErrVerifyingSigstoreSignature, err)
		}
		intermediate, err := fulcioroots.GetIntermediates()
		if err != nil {
			return errors.Join(ErrVerifyingSigstoreSignature, err)
		}

		checkOpts.RootCerts = root
		checkOpts.IntermediateCerts = intermediate

		verifier, err = gitsignVerifier.NewCertVerifier(
			gitsignVerifier.WithRootPool(root),
			gitsignVerifier.WithIntermediatePool(intermediate),
		)
		if err != nil {
			return errors.Join(ErrVerifyingSigstoreSignature, err)
		}
	} else {
		slog.Debug("Using environment variables to establish trust for Sigstore instance...")
		rootCerts, err := common.LoadCertsFromPath(sigstoreRootFilePath)
		if err != nil {
			return errors.Join(ErrVerifyingSigstoreSignature, err)
		}
		root := x509.NewCertPool()
		for _, cert := range rootCerts {
			root.AddCert(cert)
		}

		checkOpts.RootCerts = root

		verifier, err = gitsignVerifier.NewCertVerifier(
			gitsignVerifier.WithRootPool(root),
		)
		if err != nil {
			return errors.Join(ErrVerifyingSigstoreSignature, err)
		}
	}

	verifiedCert, err := verifier.Verify(ctx, data, signature, true)
	if err != nil {
		return ErrIncorrectVerificationKey
	}

	rekorURL := rekorPublicGoodInstance
	// Check git config to see if rekor server must be overridden
	config, err := repo.GetGitConfig()
	if err != nil {
		return errors.Join(ErrVerifyingSigstoreSignature, err)
	}
	if configValue, has := config[sigstore.GitConfigRekor]; has {
		slog.Debug(fmt.Sprintf("Using '%s' as Rekor instance...", configValue))
		rekorURL = configValue
	}

	// gitsignRekor.NewWithOptions invokes cosign.GetRekorPubs which looks at
	// the env var, so we don't have to do anything here
	rekor, err := gitsignRekor.NewWithOptions(ctx, rekorURL)
	if err != nil {
		return errors.Join(ErrVerifyingSigstoreSignature, err)
	}

	checkOpts.RekorClient = rekor.Rekor
	checkOpts.RekorPubKeys = rekor.PublicKeys()

	// cosign.GetCTLogPubs already looks at the env var, so we don't have to do
	// anything here
	ctPub, err := cosign.GetCTLogPubs(ctx)
	if err != nil {
		return errors.Join(ErrVerifyingSigstoreSignature, err)
	}

	checkOpts.CTLogPubKeys = ctPub

	if _, err := cosign.ValidateAndUnpackCert(verifiedCert, checkOpts); err != nil {
		return errors.Join(ErrIncorrectVerificationKey, err)
	}

	return nil
}

// verifySSHKeySignature verifies Git signatures issued by SSH keys.
func verifySSHKeySignature(ctx context.Context, key *signerverifier.SSLibKey, data, signature []byte) error {
	verifier, err := sslibsvssh.NewVerifierFromKey(key)
	if err != nil {
		return errors.Join(ErrVerifyingSSHSignature, err)
	}

	if err := verifier.Verify(ctx, data, signature); err != nil {
		return errors.Join(ErrVerifyingSSHSignature, err)
	}

	return nil
}

func getSigningMethod(gitConfig map[string]string) string {
	format, ok := gitConfig["gpg.format"]
	if !ok {
		return signingFormatGPG // default to gpg
	}
	return format
}

func getSigningKeyInfo(gitConfig map[string]string) string {
	keyInfo, ok := gitConfig["user.signingkey"]
	if !ok {
		return ""
	}
	return keyInfo
}
