// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package asymkey

import (
	"errors"
	"fmt"
	"hash"

	repo_model "code.gitea.io/gitea/models/repo"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/log"

	"github.com/ProtonMail/go-crypto/openpgp/packet"
)

// This file provides functions relating commit verification

// CommitVerification represents a commit validation of signature
type CommitVerification struct {
	Verified       bool
	Warning        bool
	Reason         string
	SigningUser    *user_model.User // if Verified, then SigningUser is non-nil
	CommittingUser *user_model.User // if Verified, then CommittingUser is non-nil
	SigningEmail   string
	SigningKey     *GPGKey // FIXME: need to refactor it to a new name like "SigningGPGKey", it is also used in some templates
	SigningSSHKey  *PublicKey
	TrustStatus    string
}

// SignCommit represents a commit with validation of signature.
type SignCommit struct {
	Verification *CommitVerification
	*user_model.UserCommit
}

const (
	// BadSignature is used as the reason when the signature has a KeyID that is in the db
	// but no key that has that ID verifies the signature. This is a suspicious failure.
	BadSignature = "gpg.error.probable_bad_signature"
	// BadDefaultSignature is used as the reason when the signature has a KeyID that matches the
	// default Key but is not verified by the default key. This is a suspicious failure.
	BadDefaultSignature = "gpg.error.probable_bad_default_signature"
	// NoKeyFound is used as the reason when no key can be found to verify the signature.
	NoKeyFound = "gpg.error.no_gpg_keys_found"
)

func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
	// Check if key can sign
	if !k.CanSign {
		return errors.New("key can not sign")
	}
	// Decode key
	pkey, err := base64DecPubKey(k.Content)
	if err != nil {
		return err
	}
	return pkey.VerifySignature(h, s)
}

func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) {
	// Generating hash of commit
	hash, err := populateHash(sig.Hash, []byte(payload))
	if err != nil { // Skipping as failed to generate hash
		log.Error("PopulateHash: %v", err)
		return nil, err
	}
	// We will ignore errors in verification as they don't need to be propagated up
	err = verifySign(sig, hash, k)
	if err != nil {
		return nil, nil
	}
	return k, nil
}

func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) {
	verified, err := hashAndVerify(sig, payload, k)
	if err != nil || verified != nil {
		return verified, err
	}
	for _, sk := range k.SubsKey {
		verified, err := hashAndVerify(sig, payload, sk)
		if err != nil || verified != nil {
			return verified, err
		}
	}
	return nil, nil
}

func HashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification {
	key, err := hashAndVerifyWithSubKeys(sig, payload, k)
	if err != nil { // Skipping failed to generate hash
		return &CommitVerification{
			CommittingUser: committer,
			Verified:       false,
			Reason:         "gpg.error.generate_hash",
		}
	}

	if key != nil {
		return &CommitVerification{ // Everything is ok
			CommittingUser: committer,
			Verified:       true,
			Reason:         fmt.Sprintf("%s / %s", signer.Name, key.KeyID),
			SigningUser:    signer,
			SigningKey:     key,
			SigningEmail:   email,
		}
	}
	return nil
}

// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
// There are several trust models in Gitea
func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error {
	if !verification.Verified {
		return nil
	}

	// In the Committer trust model a signature is trusted if it matches the committer
	// - it doesn't matter if they're a collaborator, the owner, Gitea or Github
	// NB: This model is commit verification only
	if repoTrustModel == repo_model.CommitterTrustModel {
		// default to "unmatched"
		verification.TrustStatus = "unmatched"

		// We can only verify against users in our database but the default key will match
		// against by email if it is not in the db.
		if (verification.SigningUser.ID != 0 &&
			verification.CommittingUser.ID == verification.SigningUser.ID) ||
			(verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 &&
				verification.SigningUser.Email == verification.CommittingUser.Email) {
			verification.TrustStatus = "trusted"
		}
		return nil
	}

	// Now we drop to the more nuanced trust models...
	verification.TrustStatus = "trusted"

	if verification.SigningUser.ID == 0 {
		// This commit is signed by the default key - but this key is not assigned to a user in the DB.

		// However in the repo_model.CollaboratorCommitterTrustModel we cannot mark this as trusted
		// unless the default key matches the email of a non-user.
		if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 ||
			verification.SigningUser.Email != verification.CommittingUser.Email) {
			verification.TrustStatus = "untrusted"
		}
		return nil
	}

	// Check we actually have a GPG SigningKey
	var err error
	if verification.SigningKey != nil {
		var isMember bool
		if keyMap != nil {
			var has bool
			isMember, has = (*keyMap)[verification.SigningKey.KeyID]
			if !has {
				isMember, err = isOwnerMemberCollaborator(verification.SigningUser)
				(*keyMap)[verification.SigningKey.KeyID] = isMember
			}
		} else {
			isMember, err = isOwnerMemberCollaborator(verification.SigningUser)
		}

		if !isMember {
			verification.TrustStatus = "untrusted"
			if verification.CommittingUser.ID != verification.SigningUser.ID {
				// The committing user and the signing user are not the same
				// This should be marked as questionable unless the signing user is a collaborator/team member etc.
				verification.TrustStatus = "unmatched"
			}
		} else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
			// The committing user and the signing user are not the same and our trustmodel states that they must match
			verification.TrustStatus = "unmatched"
		}
	}

	return err
}
