package tuf

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"path"
	"sort"
	"strings"
	"time"

	"github.com/DataDog/go-tuf/data"
	"github.com/DataDog/go-tuf/internal/roles"
	"github.com/DataDog/go-tuf/internal/sets"
	"github.com/DataDog/go-tuf/internal/signer"
	"github.com/DataDog/go-tuf/pkg/keys"
	"github.com/DataDog/go-tuf/pkg/targets"
	"github.com/DataDog/go-tuf/sign"
	"github.com/DataDog/go-tuf/util"
	"github.com/DataDog/go-tuf/verify"
	"github.com/secure-systems-lab/go-securesystemslib/cjson"
)

const (
	// The maximum number of delegations to visit while traversing the delegations graph.
	defaultMaxDelegations = 32
)

// topLevelMetadata determines the order signatures are verified when committing.
var topLevelMetadata = []string{
	"root.json",
	"targets.json",
	"snapshot.json",
	"timestamp.json",
}

// TargetsWalkFunc is a function of a target path name and a target payload used to
// execute some function on each staged target file. For example, it may normalize path
// names and generate target file metadata with additional custom metadata.
type TargetsWalkFunc func(path string, target io.Reader) error

type Repo struct {
	local          LocalStore
	hashAlgorithms []string
	meta           map[string]json.RawMessage
	prefix         string
	indent         string
	logger         *log.Logger
}

type RepoOpts func(r *Repo)

func WithLogger(logger *log.Logger) RepoOpts {
	return func(r *Repo) {
		r.logger = logger
	}
}

func WithHashAlgorithms(hashAlgorithms ...string) RepoOpts {
	return func(r *Repo) {
		r.hashAlgorithms = hashAlgorithms
	}
}

func WithPrefix(prefix string) RepoOpts {
	return func(r *Repo) {
		r.prefix = prefix
	}
}

func WithIndex(indent string) RepoOpts {
	return func(r *Repo) {
		r.indent = indent
	}
}

func NewRepo(local LocalStore, hashAlgorithms ...string) (*Repo, error) {
	return NewRepoIndent(local, "", "", hashAlgorithms...)
}

func NewRepoIndent(local LocalStore, prefix string, indent string,
	hashAlgorithms ...string) (*Repo, error) {
	r := &Repo{
		local:          local,
		hashAlgorithms: hashAlgorithms,
		prefix:         prefix,
		indent:         indent,
		logger:         log.New(io.Discard, "", 0),
	}

	var err error
	r.meta, err = local.GetMeta()
	if err != nil {
		return nil, err
	}
	return r, nil
}

func NewRepoWithOpts(local LocalStore, opts ...RepoOpts) (*Repo, error) {
	r, err := NewRepo(local)
	if err != nil {
		return nil, err
	}
	for _, opt := range opts {
		opt(r)
	}
	return r, nil
}

func (r *Repo) Init(consistentSnapshot bool) error {
	t, err := r.topLevelTargets()
	if err != nil {
		return err
	}
	if len(t.Targets) > 0 {
		return ErrInitNotAllowed
	}
	root := data.NewRoot()
	root.ConsistentSnapshot = consistentSnapshot
	// Set root version to 1 for a new root.
	root.Version = 1
	if err = r.setMeta("root.json", root); err != nil {
		return err
	}

	t.Version = 1
	if err = r.setMeta("targets.json", t); err != nil {
		return err
	}

	r.logger.Println("Repository initialized")
	return nil
}

func (r *Repo) topLevelKeysDB() (*verify.DB, error) {
	db := verify.NewDB()
	root, err := r.root()
	if err != nil {
		return nil, err
	}
	for id, k := range root.Keys {
		if err := db.AddKey(id, k); err != nil {
			return nil, err
		}
	}
	for name, role := range root.Roles {
		if err := db.AddRole(name, role); err != nil {
			return nil, err
		}
	}
	return db, nil
}

func (r *Repo) root() (*data.Root, error) {
	rootJSON, ok := r.meta["root.json"]
	if !ok {
		return data.NewRoot(), nil
	}
	s := &data.Signed{}
	if err := json.Unmarshal(rootJSON, s); err != nil {
		return nil, err
	}
	root := &data.Root{}
	if err := json.Unmarshal(s.Signed, root); err != nil {
		return nil, err
	}
	return root, nil
}

func (r *Repo) snapshot() (*data.Snapshot, error) {
	snapshotJSON, ok := r.meta["snapshot.json"]
	if !ok {
		return data.NewSnapshot(), nil
	}
	s := &data.Signed{}
	if err := json.Unmarshal(snapshotJSON, s); err != nil {
		return nil, err
	}
	snapshot := &data.Snapshot{}
	if err := json.Unmarshal(s.Signed, snapshot); err != nil {
		return nil, err
	}
	return snapshot, nil
}

func (r *Repo) RootVersion() (int64, error) {
	root, err := r.root()
	if err != nil {
		return -1, err
	}
	return root.Version, nil
}

func (r *Repo) GetThreshold(keyRole string) (int, error) {
	if roles.IsDelegatedTargetsRole(keyRole) {
		// The signature threshold for a delegated targets role
		// depends on the incoming delegation edge.
		return -1, ErrInvalidRole{keyRole, "only thresholds for top-level roles supported"}
	}
	root, err := r.root()
	if err != nil {
		return -1, err
	}
	role, ok := root.Roles[keyRole]
	if !ok {
		return -1, ErrInvalidRole{keyRole, "role missing from root metadata"}
	}

	return role.Threshold, nil
}

func (r *Repo) SetThreshold(keyRole string, t int) error {
	if roles.IsDelegatedTargetsRole(keyRole) {
		// The signature threshold for a delegated targets role
		// depends on the incoming delegation edge.
		return ErrInvalidRole{keyRole, "only thresholds for top-level roles supported"}
	}
	root, err := r.root()
	if err != nil {
		return err
	}
	role, ok := root.Roles[keyRole]
	if !ok {
		return ErrInvalidRole{keyRole, "role missing from root metadata"}
	}
	if role.Threshold == t {
		// Change was a no-op.
		return nil
	}
	role.Threshold = t
	if !r.local.FileIsStaged("root.json") {
		root.Version++
	}
	return r.setMeta("root.json", root)
}

func (r *Repo) Targets() (data.TargetFiles, error) {
	targets, err := r.topLevelTargets()
	if err != nil {
		return nil, err
	}
	return targets.Targets, nil
}

func (r *Repo) SetTargetsVersion(v int64) error {
	t, err := r.topLevelTargets()
	if err != nil {
		return err
	}
	t.Version = v
	return r.setMeta("targets.json", t)
}

func (r *Repo) TargetsVersion() (int64, error) {
	t, err := r.topLevelTargets()
	if err != nil {
		return -1, err
	}
	return t.Version, nil
}

func (r *Repo) SetTimestampVersion(v int64) error {
	ts, err := r.timestamp()
	if err != nil {
		return err
	}
	ts.Version = v
	return r.setMeta("timestamp.json", ts)
}

func (r *Repo) TimestampVersion() (int64, error) {
	ts, err := r.timestamp()
	if err != nil {
		return -1, err
	}
	return ts.Version, nil
}

func (r *Repo) SetSnapshotVersion(v int64) error {
	s, err := r.snapshot()
	if err != nil {
		return err
	}

	s.Version = v
	return r.setMeta("snapshot.json", s)
}

func (r *Repo) SnapshotVersion() (int64, error) {
	s, err := r.snapshot()
	if err != nil {
		return -1, err
	}
	return s.Version, nil
}

func (r *Repo) topLevelTargets() (*data.Targets, error) {
	return r.targets("targets")
}

func (r *Repo) targets(metaName string) (*data.Targets, error) {
	targetsJSON, ok := r.meta[metaName+".json"]
	if !ok {
		return data.NewTargets(), nil
	}
	s := &data.Signed{}
	if err := json.Unmarshal(targetsJSON, s); err != nil {
		return nil, fmt.Errorf("error unmarshalling for targets %q: %w", metaName, err)
	}
	targets := &data.Targets{}
	if err := json.Unmarshal(s.Signed, targets); err != nil {
		return nil, fmt.Errorf("error unmarshalling signed data for targets %q: %w", metaName, err)
	}
	return targets, nil
}

func (r *Repo) timestamp() (*data.Timestamp, error) {
	timestampJSON, ok := r.meta["timestamp.json"]
	if !ok {
		return data.NewTimestamp(), nil
	}
	s := &data.Signed{}
	if err := json.Unmarshal(timestampJSON, s); err != nil {
		return nil, err
	}
	timestamp := &data.Timestamp{}
	if err := json.Unmarshal(s.Signed, timestamp); err != nil {
		return nil, err
	}
	return timestamp, nil
}

func (r *Repo) ChangePassphrase(keyRole string) error {
	if p, ok := r.local.(PassphraseChanger); ok {
		return p.ChangePassphrase(keyRole)
	}

	return ErrChangePassphraseNotSupported
}

func (r *Repo) GenKey(role string) ([]string, error) {
	// Not compatible with delegated targets roles, since delegated targets keys
	// are associated with a delegation (edge), not a role (node).

	return r.GenKeyWithExpires(role, data.DefaultExpires(role))
}

func (r *Repo) GenKeyWithExpires(keyRole string, expires time.Time) (keyids []string, err error) {
	return r.GenKeyWithSchemeAndExpires(keyRole, expires, data.KeySchemeEd25519)
}

func (r *Repo) GenKeyWithSchemeAndExpires(role string, expires time.Time, keyScheme data.KeyScheme) ([]string, error) {
	var signer keys.Signer
	var err error
	switch keyScheme {
	case data.KeySchemeEd25519:
		signer, err = keys.GenerateEd25519Key()
	case data.KeySchemeECDSA_SHA2_P256:
		signer, err = keys.GenerateEcdsaKey()
	case data.KeySchemeRSASSA_PSS_SHA256:
		signer, err = keys.GenerateRsaKey()
	default:
		return nil, errors.New("unknown key type")
	}
	if err != nil {
		return nil, err
	}

	// Not compatible with delegated targets roles, since delegated targets keys
	// are associated with a delegation (edge), not a role (node).

	if err = r.AddPrivateKeyWithExpires(role, signer, expires); err != nil {
		return nil, err
	}
	return signer.PublicData().IDs(), nil
}

func (r *Repo) AddPrivateKey(role string, signer keys.Signer) error {
	// Not compatible with delegated targets roles, since delegated targets keys
	// are associated with a delegation (edge), not a role (node).

	return r.AddPrivateKeyWithExpires(role, signer, data.DefaultExpires(role))
}

func (r *Repo) AddPrivateKeyWithExpires(keyRole string, signer keys.Signer, expires time.Time) error {
	// Not compatible with delegated targets roles, since delegated targets keys
	// are associated with a delegation (edge), not a role (node).

	if roles.IsDelegatedTargetsRole(keyRole) {
		return ErrInvalidRole{keyRole, "only support adding keys for top-level roles"}
	}

	if !validExpires(expires) {
		return ErrInvalidExpires{expires}
	}

	// Must add signer before adding verification key, so
	// root.json can be signed when a new root key is added.
	if err := r.local.SaveSigner(keyRole, signer); err != nil {
		return err
	}

	if err := r.AddVerificationKeyWithExpiration(keyRole, signer.PublicData(), expires); err != nil {
		return err
	}

	return nil
}

func (r *Repo) AddVerificationKey(keyRole string, pk *data.PublicKey) error {
	// Not compatible with delegated targets roles, since delegated targets keys
	// are associated with a delegation (edge), not a role (node).

	return r.AddVerificationKeyWithExpiration(keyRole, pk, data.DefaultExpires(keyRole))
}

func (r *Repo) AddVerificationKeyWithExpiration(keyRole string, pk *data.PublicKey, expires time.Time) error {
	// Not compatible with delegated targets roles, since delegated targets keys
	// are associated with a delegation (edge), not a role (node).

	if roles.IsDelegatedTargetsRole(keyRole) {
		return ErrInvalidRole{
			Role:   keyRole,
			Reason: "only top-level targets roles are supported",
		}
	}

	if !validExpires(expires) {
		return ErrInvalidExpires{expires}
	}

	root, err := r.root()
	if err != nil {
		return err
	}

	role, ok := root.Roles[keyRole]
	if !ok {
		role = &data.Role{KeyIDs: []string{}, Threshold: 1}
		root.Roles[keyRole] = role
	}
	changed := false
	if role.AddKeyIDs(pk.IDs()) {
		changed = true
	}

	if root.AddKey(pk) {
		changed = true
	}

	if !changed {
		return nil
	}

	root.Expires = expires.Round(time.Second)
	if !r.local.FileIsStaged("root.json") {
		root.Version++
	}

	return r.setMeta("root.json", root)
}

func validExpires(expires time.Time) bool {
	return time.Until(expires) > 0
}

func (r *Repo) RootKeys() ([]*data.PublicKey, error) {
	root, err := r.root()
	if err != nil {
		return nil, err
	}
	role, ok := root.Roles["root"]
	if !ok {
		return nil, nil
	}

	// We might have multiple key ids that correspond to the same key, so
	// make sure we only return unique keys.
	seen := make(map[string]struct{})
	rootKeys := []*data.PublicKey{}
	for _, id := range role.KeyIDs {
		key, ok := root.Keys[id]
		if !ok {
			return nil, fmt.Errorf("tuf: invalid root metadata")
		}
		found := false
		if _, ok := seen[id]; ok {
			found = true
			break
		}
		if !found {
			for _, id := range key.IDs() {
				seen[id] = struct{}{}
			}
			rootKeys = append(rootKeys, key)
		}
	}
	return rootKeys, nil
}

func (r *Repo) RevokeKey(role, id string) error {
	// Not compatible with delegated targets roles, since delegated targets keys
	// are associated with a delegation (edge), not a role (node).

	return r.RevokeKeyWithExpires(role, id, data.DefaultExpires("root"))
}

func (r *Repo) RevokeKeyWithExpires(keyRole, id string, expires time.Time) error {
	// Not compatible with delegated targets roles, since delegated targets keys
	// are associated with a delegation (edge), not a role (node).

	if roles.IsDelegatedTargetsRole(keyRole) {
		return ErrInvalidRole{keyRole, "only revocations for top-level roles supported"}
	}

	if !validExpires(expires) {
		return ErrInvalidExpires{expires}
	}

	root, err := r.root()
	if err != nil {
		return err
	}

	key, ok := root.Keys[id]
	if !ok {
		return ErrKeyNotFound{keyRole, id}
	}

	role, ok := root.Roles[keyRole]
	if !ok {
		return ErrKeyNotFound{keyRole, id}
	}

	// Create a list of filtered key IDs that do not contain the revoked key IDs.
	filteredKeyIDs := make([]string, 0, len(role.KeyIDs))

	// There may be multiple keyids that correspond to this key, so
	// filter all of them out.
	for _, keyID := range role.KeyIDs {
		if !key.ContainsID(keyID) {
			filteredKeyIDs = append(filteredKeyIDs, keyID)
		}
	}
	if len(filteredKeyIDs) == len(role.KeyIDs) {
		return ErrKeyNotFound{keyRole, id}
	}
	role.KeyIDs = filteredKeyIDs
	root.Roles[keyRole] = role

	// Only delete the key from root.Keys if the key is no longer in use by
	// any other role.
	key_in_use := false
	for _, role := range root.Roles {
		for _, keyID := range role.KeyIDs {
			if key.ContainsID(keyID) {
				key_in_use = true
			}
		}
	}
	if !key_in_use {
		for _, keyID := range key.IDs() {
			delete(root.Keys, keyID)
		}
	}
	root.Expires = expires.Round(time.Second)
	if !r.local.FileIsStaged("root.json") {
		root.Version++
	}

	err = r.setMeta("root.json", root)
	if err == nil {
		r.logger.Println("Revoked", keyRole, "key with ID", id, "in root metadata")
	}
	return err
}

// AddDelegatedRole is equivalent to AddDelegatedRoleWithExpires, but
// with a default expiration time.
func (r *Repo) AddDelegatedRole(delegator string, delegatedRole data.DelegatedRole, keys []*data.PublicKey) error {
	return r.AddDelegatedRoleWithExpires(delegator, delegatedRole, keys, data.DefaultExpires("targets"))
}

// AddDelegatedRoleWithExpires adds a delegation from the delegator to the
// role specified in the role argument. Key IDs referenced in role.KeyIDs
// should have corresponding Key entries in the keys argument. New metadata is
// written with the given expiration time.
func (r *Repo) AddDelegatedRoleWithExpires(delegator string, delegatedRole data.DelegatedRole, keys []*data.PublicKey, expires time.Time) error {
	expires = expires.Round(time.Second)

	t, err := r.targets(delegator)
	if err != nil {
		return fmt.Errorf("error getting delegator (%q) metadata: %w", delegator, err)
	}

	if t.Delegations == nil {
		t.Delegations = &data.Delegations{}
		t.Delegations.Keys = make(map[string]*data.PublicKey)
	}

	for _, keyID := range delegatedRole.KeyIDs {
		for _, key := range keys {
			if key.ContainsID(keyID) {
				t.Delegations.Keys[keyID] = key
				break
			}
		}
	}

	for _, r := range t.Delegations.Roles {
		if r.Name == delegatedRole.Name {
			return fmt.Errorf("role: %s is already delegated to by %s", delegatedRole.Name, r.Name)
		}
	}
	t.Delegations.Roles = append(t.Delegations.Roles, delegatedRole)
	t.Expires = expires

	delegatorFile := delegator + ".json"
	if !r.local.FileIsStaged(delegatorFile) {
		t.Version++
	}

	err = r.setMeta(delegatorFile, t)
	if err != nil {
		return fmt.Errorf("error setting metadata for %q: %w", delegatorFile, err)
	}

	delegatee := delegatedRole.Name
	dt, err := r.targets(delegatee)
	if err != nil {
		return fmt.Errorf("error getting delegatee (%q) metadata: %w", delegatee, err)
	}
	dt.Expires = expires

	delegateeFile := delegatee + ".json"
	if !r.local.FileIsStaged(delegateeFile) {
		dt.Version++
	}

	err = r.setMeta(delegateeFile, dt)
	if err != nil {
		return fmt.Errorf("error setting metadata for %q: %w", delegateeFile, err)
	}

	return nil
}

// AddDelegatedRolesForPathHashBins is equivalent to
// AddDelegatedRolesForPathHashBinsWithExpires, but with a default
// expiration time.
func (r *Repo) AddDelegatedRolesForPathHashBins(delegator string, bins *targets.HashBins, keys []*data.PublicKey, threshold int) error {
	return r.AddDelegatedRolesForPathHashBinsWithExpires(delegator, bins, keys, threshold, data.DefaultExpires("targets"))
}

// AddDelegatedRolesForPathHashBinsWithExpires adds delegations to the
// delegator role for the given hash bins configuration. New metadata is
// written with the given expiration time.
func (r *Repo) AddDelegatedRolesForPathHashBinsWithExpires(delegator string, bins *targets.HashBins, keys []*data.PublicKey, threshold int, expires time.Time) error {
	keyIDs := []string{}
	for _, key := range keys {
		keyIDs = append(keyIDs, key.IDs()...)
	}

	n := bins.NumBins()
	for i := uint64(0); i < n; i += 1 {
		bin := bins.GetBin(i)
		name := bin.RoleName()
		err := r.AddDelegatedRoleWithExpires(delegator, data.DelegatedRole{
			Name:             name,
			KeyIDs:           sets.DeduplicateStrings(keyIDs),
			PathHashPrefixes: bin.HashPrefixes(),
			Threshold:        threshold,
		}, keys, expires)
		if err != nil {
			return fmt.Errorf("error adding delegation from %v to %v: %w", delegator, name, err)
		}
	}

	return nil
}

// ResetTargetsDelegation is equivalent to ResetTargetsDelegationsWithExpires
// with a default expiry time.
func (r *Repo) ResetTargetsDelegations(delegator string) error {
	return r.ResetTargetsDelegationsWithExpires(delegator, data.DefaultExpires("targets"))
}

// ResetTargetsDelegationsWithExpires removes all targets delegations from the
// given delegator role. New metadata is written with the given expiration
// time.
func (r *Repo) ResetTargetsDelegationsWithExpires(delegator string, expires time.Time) error {
	t, err := r.targets(delegator)
	if err != nil {
		return fmt.Errorf("error getting delegator (%q) metadata: %w", delegator, err)
	}

	t.Delegations = nil

	t.Expires = expires.Round(time.Second)

	delegatorFile := delegator + ".json"
	if !r.local.FileIsStaged(delegatorFile) {
		t.Version++
	}

	err = r.setMeta(delegatorFile, t)
	if err != nil {
		return fmt.Errorf("error setting metadata for %q: %w", delegatorFile, err)
	}

	return nil
}

func (r *Repo) jsonMarshal(v interface{}) ([]byte, error) {
	if r.prefix == "" && r.indent == "" {
		return json.Marshal(v)
	}
	return json.MarshalIndent(v, r.prefix, r.indent)
}

func (r *Repo) dbsForRole(role string) ([]*verify.DB, error) {
	dbs := []*verify.DB{}

	if roles.IsTopLevelRole(role) {
		db, err := r.topLevelKeysDB()
		if err != nil {
			return nil, err
		}
		dbs = append(dbs, db)
	} else {
		ddbs, err := r.delegatorDBs(role)
		if err != nil {
			return nil, err
		}

		dbs = append(dbs, ddbs...)
	}

	return dbs, nil
}

func (r *Repo) signersForRole(role string) ([]keys.Signer, error) {
	dbs, err := r.dbsForRole(role)
	if err != nil {
		return nil, err
	}

	signers := []keys.Signer{}
	for _, db := range dbs {
		ss, err := r.getSignersInDB(role, db)
		if err != nil {
			return nil, err
		}

		signers = append(signers, ss...)
	}

	return signers, nil
}

func (r *Repo) setMeta(roleFilename string, meta interface{}) error {
	role := strings.TrimSuffix(roleFilename, ".json")

	signers, err := r.signersForRole(role)
	if err != nil {
		return err
	}

	s, err := sign.Marshal(meta, signers...)
	if err != nil {
		return err
	}
	b, err := r.jsonMarshal(s)
	if err != nil {
		return err
	}
	r.meta[roleFilename] = b
	return r.local.SetMeta(roleFilename, b)
}

// CanonicalizeAndSign canonicalizes the signed portion of signed, then signs it using the key(s) associated with role.
//
// It appends the signature to signed.
//
// It returns the total number of keys used for signing, 0 (along with
// ErrNoKeys) if no keys were found, or -1 (along with an error) in error cases.
func (r *Repo) CanonicalizeAndSign(role string, signed *data.Signed) (int, error) {
	keys, err := r.signersForRole(role)
	if err != nil {
		return -1, err
	}
	if len(keys) == 0 {
		return 0, ErrNoKeys{role}
	}
	for _, k := range keys {
		if err = sign.Sign(signed, k); err != nil {
			return -1, err
		}
	}
	return len(keys), nil
}

// SignPayload canonicalizes the signed portion of payload, then signs it using the key(s) associated with role.
//
// It returns the total number of keys used for signing, 0 (along with
// ErrNoKeys) if no keys were found, or -1 (along with an error) in error cases.
//
// DEPRECATED: please use CanonicalizeAndSign instead.
func (r *Repo) SignPayload(role string, payload *data.Signed) (int, error) {
	return r.CanonicalizeAndSign(role, payload)
}

// SignRaw signs the given (pre-canonicalized) payload using the key(s) associated with role.
//
// It returns the new data.Signatures.
func (r *Repo) SignRaw(role string, payload []byte) ([]data.Signature, error) {
	keys, err := r.signersForRole(role)
	if err != nil {
		return nil, err
	}
	if len(keys) == 0 {
		return nil, ErrNoKeys{role}
	}

	allSigs := make([]data.Signature, 0, len(keys))
	for _, k := range keys {
		sigs, err := sign.MakeSignatures(payload, k)
		if err != nil {
			return nil, err
		}
		allSigs = append(allSigs, sigs...)
	}
	return allSigs, nil
}

func (r *Repo) Sign(roleFilename string) error {
	signed, err := r.SignedMeta(roleFilename)
	if err != nil {
		return err
	}

	role := strings.TrimSuffix(roleFilename, ".json")
	numKeys, err := r.SignPayload(role, signed)
	if errors.Is(err, ErrNoKeys{role}) {
		return ErrNoKeys{roleFilename}
	} else if err != nil {
		return err
	}

	b, err := r.jsonMarshal(signed)
	if err != nil {
		return err
	}
	r.meta[roleFilename] = b
	err = r.local.SetMeta(roleFilename, b)
	if err == nil {
		r.logger.Println("Signed", roleFilename, "with", numKeys, "key(s)")
	}
	return err
}

// AddOrUpdateSignature allows users to add or update a signature generated with an external tool.
// The name must be a valid metadata file name, like root.json.
func (r *Repo) AddOrUpdateSignature(roleFilename string, signature data.Signature) error {
	role := strings.TrimSuffix(roleFilename, ".json")

	// Check key ID is in valid for the role.
	dbs, err := r.dbsForRole(role)
	if err != nil {
		return err
	}

	if len(dbs) == 0 {
		return ErrInvalidRole{role, "no trusted keys for role"}
	}

	s, err := r.SignedMeta(roleFilename)
	if err != nil {
		return err
	}

	keyInDB := false
	validSig := false
	for _, db := range dbs {
		roleData := db.GetRole(role)
		if roleData == nil {
			return ErrInvalidRole{role, "role is not in verifier DB"}
		}
		if roleData.ValidKey(signature.KeyID) {
			keyInDB = true

			verifier, err := db.GetVerifier(signature.KeyID)
			if err != nil {
				continue
			}

			// Now check if this validly signed the metadata.
			if err := verify.VerifySignature(s.Signed, signature.Signature,
				verifier); err == nil {
				validSig = true
				break
			}
		}
	}
	if !keyInDB {
		// This key was not delegated for the role in any delegatee.
		return verify.ErrInvalidKey
	}
	if !validSig {
		// The signature was invalid.
		return verify.ErrInvalid
	}

	// Add or update signature.
	signatures := make([]data.Signature, 0, len(s.Signatures)+1)
	for _, sig := range s.Signatures {
		if sig.KeyID != signature.KeyID {
			signatures = append(signatures, sig)
		}
	}
	signatures = append(signatures, signature)
	s.Signatures = signatures

	b, err := r.jsonMarshal(s)
	if err != nil {
		return err
	}
	r.meta[roleFilename] = b

	return r.local.SetMeta(roleFilename, b)
}

// getSignersInDB returns available signing interfaces, sorted by key ID.
//
// Only keys contained in the keys db are returned (i.e. local keys which have
// been revoked are omitted), except for the root role in which case all local
// keys are returned (revoked root keys still need to sign new root metadata so
// clients can verify the new root.json and update their keys db accordingly).
func (r *Repo) getSignersInDB(roleName string, db *verify.DB) ([]keys.Signer, error) {
	signers, err := r.local.GetSigners(roleName)
	if err != nil {
		return nil, err
	}

	if roleName == "root" {
		sorted := make([]keys.Signer, len(signers))
		copy(sorted, signers)
		sort.Sort(signer.ByIDs(sorted))
		return sorted, nil
	}

	role := db.GetRole(roleName)
	if role == nil {
		return nil, nil
	}
	if len(role.KeyIDs) == 0 {
		return nil, nil
	}

	signersInDB := []keys.Signer{}
	for _, s := range signers {
		for _, id := range s.PublicData().IDs() {
			if _, ok := role.KeyIDs[id]; ok {
				signersInDB = append(signersInDB, s)
			}
		}
	}

	sort.Sort(signer.ByIDs(signersInDB))

	return signersInDB, nil
}

// Used to retrieve the signable portion of the metadata when using an external signing tool.
func (r *Repo) SignedMeta(roleFilename string) (*data.Signed, error) {
	b, ok := r.meta[roleFilename]
	if !ok {
		return nil, ErrMissingMetadata{roleFilename}
	}
	s := &data.Signed{}
	if err := json.Unmarshal(b, s); err != nil {
		return nil, err
	}
	return s, nil
}

// delegatorDBs returns a list of key DBs for all incoming delegations.
func (r *Repo) delegatorDBs(delegateeRole string) ([]*verify.DB, error) {
	delegatorDBs := []*verify.DB{}
	for metaName := range r.meta {
		if roles.IsTopLevelManifest(metaName) && metaName != "targets.json" {
			continue
		}
		roleName := strings.TrimSuffix(metaName, ".json")

		t, err := r.targets(roleName)
		if err != nil {
			return nil, err
		}

		if t.Delegations == nil {
			continue
		}

		delegatesToRole := false
		for _, d := range t.Delegations.Roles {
			if d.Name == delegateeRole {
				delegatesToRole = true
				break
			}
		}
		if !delegatesToRole {
			continue
		}

		db, err := verify.NewDBFromDelegations(t.Delegations)
		if err != nil {
			return nil, err
		}

		delegatorDBs = append(delegatorDBs, db)
	}

	return delegatorDBs, nil
}

// targetDelegationForPath finds the targets metadata for the role that should
// sign the given path. The final delegation that led to the returned target
// metadata is also returned.
//
// Since there may be multiple targets roles that are able to sign a specific
// path, we must choose which roles's metadata to return. If preferredRole is
// specified (non-empty string) and eligible to sign the given path by way of
// some delegation chain, targets metadata for that role is returned. If
// preferredRole is not specified (""), we return targets metadata for the
// final role visited in the depth-first delegation traversal.
func (r *Repo) targetDelegationForPath(path string, preferredRole string) (*data.Targets, *targets.Delegation, error) {
	topLevelKeysDB, err := r.topLevelKeysDB()
	if err != nil {
		return nil, nil, err
	}

	iterator, err := targets.NewDelegationsIterator(path, topLevelKeysDB)
	if err != nil {
		return nil, nil, err
	}
	d, ok := iterator.Next()
	if !ok {
		return nil, nil, ErrNoDelegatedTarget{Path: path}
	}

	for i := 0; i < defaultMaxDelegations; i++ {
		targetsMeta, err := r.targets(d.Delegatee.Name)
		if err != nil {
			return nil, nil, err
		}

		if preferredRole != "" && d.Delegatee.Name == preferredRole {
			// The preferredRole is eligible to sign for the given path, and we've
			// found its metadata. Return it.
			return targetsMeta, &d, nil
		}

		if targetsMeta.Delegations != nil && len(targetsMeta.Delegations.Roles) > 0 {
			db, err := verify.NewDBFromDelegations(targetsMeta.Delegations)
			if err != nil {
				return nil, nil, err
			}

			// Add delegations to the iterator that are eligible to sign for the
			// given path (there may be none).
			iterator.Add(targetsMeta.Delegations.Roles, d.Delegatee.Name, db)
		}

		next, ok := iterator.Next()
		if !ok { // No more roles to traverse.
			if preferredRole == "" {
				// No preferredRole was given, so return metadata for the final role in the traversal.
				return targetsMeta, &d, nil
			} else {
				// There are no more roles to traverse, so preferredRole is either an
				// invalid role, or not eligible to sign the given path.
				return nil, nil, ErrNoDelegatedTarget{Path: path}
			}
		}

		d = next
	}

	return nil, nil, ErrNoDelegatedTarget{Path: path}
}

func (r *Repo) AddTarget(path string, custom json.RawMessage) error {
	return r.AddTargets([]string{path}, custom)
}

func (r *Repo) AddTargetToPreferredRole(path string, custom json.RawMessage, preferredRole string) error {
	return r.AddTargetsToPreferredRole([]string{path}, custom, preferredRole)
}

func (r *Repo) AddTargets(paths []string, custom json.RawMessage) error {
	return r.AddTargetsToPreferredRole(paths, custom, "")
}

func (r *Repo) AddTargetsToPreferredRole(paths []string, custom json.RawMessage, preferredRole string) error {
	return r.AddTargetsWithExpiresToPreferredRole(paths, custom, data.DefaultExpires("targets"), preferredRole)
}

func (r *Repo) AddTargetsWithDigest(digest string, digestAlg string, length int64, path string, custom json.RawMessage) error {
	// TODO: Rename this to AddTargetWithDigest
	// https://github.com/DataDog/go-tuf/issues/242

	expires := data.DefaultExpires("targets")
	path = util.NormalizeTarget(path)

	targetsMeta, delegation, err := r.targetDelegationForPath(path, "")
	if err != nil {
		return err
	}
	// This is the targets role that needs to sign the target file.
	targetsRoleName := delegation.Delegatee.Name

	meta := data.TargetFileMeta{FileMeta: data.FileMeta{Length: length, Hashes: make(data.Hashes, 1)}}
	meta.Hashes[digestAlg], err = hex.DecodeString(digest)
	if err != nil {
		return err
	}

	// If custom is provided, set custom, otherwise maintain existing custom
	// metadata
	if len(custom) > 0 {
		meta.Custom = &custom
	} else if t, ok := targetsMeta.Targets[path]; ok {
		meta.Custom = t.Custom
	}

	// What does G2 mean? Copying and pasting this comment from elsewhere in this file.
	// G2 -> we no longer desire any readers to ever observe non-prefix targets.
	delete(targetsMeta.Targets, "/"+path)
	targetsMeta.Targets[path] = meta

	targetsMeta.Expires = expires.Round(time.Second)

	manifestName := targetsRoleName + ".json"
	if !r.local.FileIsStaged(manifestName) {
		targetsMeta.Version++
	}

	err = r.setMeta(manifestName, targetsMeta)
	if err != nil {
		return fmt.Errorf("error setting metadata for %q: %w", manifestName, err)
	}

	return nil
}

func (r *Repo) AddTargetWithExpires(path string, custom json.RawMessage, expires time.Time) error {
	return r.AddTargetsWithExpires([]string{path}, custom, expires)
}

func (r *Repo) AddTargetsWithExpires(paths []string, custom json.RawMessage, expires time.Time) error {
	return r.AddTargetsWithExpiresToPreferredRole(paths, custom, expires, "")
}

func (r *Repo) AddTargetWithExpiresToPreferredRole(path string, custom json.RawMessage, expires time.Time, preferredRole string) error {
	return r.AddTargetsWithExpiresToPreferredRole([]string{path}, custom, expires, preferredRole)
}

// AddTargetsWithExpiresToPreferredRole signs the staged targets at `paths`.
//
// If preferredRole is not the empty string, the target is added to the given
// role's manifest if delegations allow it. If delegations do not allow the
// preferredRole to sign the given path, an error is returned.
func (r *Repo) AddTargetsWithExpiresToPreferredRole(paths []string, custom json.RawMessage, expires time.Time, preferredRole string) error {
	if !validExpires(expires) {
		return ErrInvalidExpires{expires}
	}

	normalizedPaths := make([]string, len(paths))
	for i, path := range paths {
		normalizedPaths[i] = util.NormalizeTarget(path)
	}

	// As we iterate through staged targets files, we accumulate changes to their
	// corresponding targets metadata.
	updatedTargetsMeta := map[string]*data.Targets{}

	if err := r.local.WalkStagedTargets(normalizedPaths, func(path string, target io.Reader) (err error) {
		originalMeta, delegation, err := r.targetDelegationForPath(path, preferredRole)
		if err != nil {
			return err
		}

		// This is the targets role that needs to sign the target file.
		targetsRoleName := delegation.Delegatee.Name

		targetsMeta := originalMeta
		if tm, ok := updatedTargetsMeta[targetsRoleName]; ok {
			// Metadata in updatedTargetsMeta overrides staged/commited metadata.
			targetsMeta = tm
		}

		fileMeta, err := util.GenerateTargetFileMeta(target, r.hashAlgorithms...)
		if err != nil {
			return err
		}

		// If we have custom metadata, set it, otherwise maintain
		// existing metadata if present
		if len(custom) > 0 {
			fileMeta.Custom = &custom
		} else if tf, ok := targetsMeta.Targets[path]; ok {
			fileMeta.Custom = tf.Custom
		}

		// G2 -> we no longer desire any readers to ever observe non-prefix targets.
		delete(targetsMeta.Targets, "/"+path)
		targetsMeta.Targets[path] = fileMeta

		updatedTargetsMeta[targetsRoleName] = targetsMeta

		return nil
	}); err != nil {
		return err
	}

	if len(updatedTargetsMeta) == 0 {
		// This is potentially unexpected behavior kept for backwards compatibility.
		// See https://github.com/DataDog/go-tuf/issues/243
		t, err := r.topLevelTargets()
		if err != nil {
			return err
		}

		updatedTargetsMeta["targets"] = t
	}

	exp := expires.Round(time.Second)
	for roleName, targetsMeta := range updatedTargetsMeta {
		targetsMeta.Expires = exp

		manifestName := roleName + ".json"
		if !r.local.FileIsStaged(manifestName) {
			targetsMeta.Version++
		}

		err := r.setMeta(manifestName, targetsMeta)
		if err != nil {
			return fmt.Errorf("error setting metadata for %q: %w", manifestName, err)
		}
	}

	return nil
}

func (r *Repo) RemoveTarget(path string) error {
	return r.RemoveTargets([]string{path})
}

func (r *Repo) RemoveTargets(paths []string) error {
	return r.RemoveTargetsWithExpires(paths, data.DefaultExpires("targets"))
}

func (r *Repo) RemoveTargetWithExpires(path string, expires time.Time) error {
	return r.RemoveTargetsWithExpires([]string{path}, expires)
}

// If paths is empty, all targets will be removed.
func (r *Repo) RemoveTargetsWithExpires(paths []string, expires time.Time) error {
	if !validExpires(expires) {
		return ErrInvalidExpires{expires}
	}

	for metaName := range r.meta {
		if metaName != "targets.json" && !roles.IsDelegatedTargetsManifest(metaName) {
			continue
		}

		err := r.removeTargetsWithExpiresFromMeta(metaName, paths, expires)
		if err != nil {
			return fmt.Errorf("could not remove %v from %v: %w", paths, metaName, err)
		}
	}

	return nil
}

func (r *Repo) removeTargetsWithExpiresFromMeta(metaName string, paths []string, expires time.Time) error {
	roleName := strings.TrimSuffix(metaName, ".json")
	t, err := r.targets(roleName)
	if err != nil {
		return err
	}
	removed_targets := []string{}
	if len(paths) == 0 {
		for rt := range t.Targets {
			removed_targets = append(removed_targets, rt)
		}
		t.Targets = make(data.TargetFiles)
	} else {
		removed := false
		for _, path := range paths {
			path = util.NormalizeTarget(path)
			if _, ok := t.Targets[path]; !ok {
				r.logger.Printf("[%v] The following target is not present: %v\n", metaName, path)
				continue
			}
			removed = true
			// G2 -> we no longer desire any readers to ever observe non-prefix targets.
			delete(t.Targets, "/"+path)
			delete(t.Targets, path)
			removed_targets = append(removed_targets, path)
		}
		if !removed {
			return nil
		}
	}
	t.Expires = expires.Round(time.Second)
	if !r.local.FileIsStaged(metaName) {
		t.Version++
	}

	err = r.setMeta(metaName, t)
	if err == nil {
		r.logger.Printf("[%v] Removed targets:\n", metaName)
		for _, v := range removed_targets {
			r.logger.Println("*", v)
		}
		if len(t.Targets) != 0 {
			r.logger.Printf("[%v] Added/staged targets:\n", metaName)
			for k := range t.Targets {
				r.logger.Println("*", k)
			}
		} else {
			r.logger.Printf("[%v] There are no added/staged targets\n", metaName)
		}
	}
	return err
}

func (r *Repo) Snapshot() error {
	return r.SnapshotWithExpires(data.DefaultExpires("snapshot"))
}

func (r *Repo) snapshotMetadata() []string {
	ret := []string{"targets.json"}

	for name := range r.meta {
		if !roles.IsVersionedManifest(name) &&
			roles.IsDelegatedTargetsManifest(name) {
			ret = append(ret, name)
		}
	}

	return ret
}

func (r *Repo) SnapshotWithExpires(expires time.Time) error {
	if !validExpires(expires) {
		return ErrInvalidExpires{expires}
	}

	snapshot, err := r.snapshot()
	if err != nil {
		return err
	}

	// Verify root metadata before verifying signatures on role metadata.
	if err := r.verifySignatures("root.json"); err != nil {
		return err
	}

	for _, metaName := range r.snapshotMetadata() {
		if err := r.verifySignatures(metaName); err != nil {
			return err
		}
		var err error
		snapshot.Meta[metaName], err = r.snapshotFileMeta(metaName)
		if err != nil {
			return err
		}
	}
	snapshot.Expires = expires.Round(time.Second)
	if !r.local.FileIsStaged("snapshot.json") {
		snapshot.Version++
	}
	err = r.setMeta("snapshot.json", snapshot)
	if err == nil {
		r.logger.Println("Staged snapshot.json metadata with expiration date:", snapshot.Expires)
	}
	return err
}

func (r *Repo) Timestamp() error {
	return r.TimestampWithExpires(data.DefaultExpires("timestamp"))
}

func (r *Repo) TimestampWithExpires(expires time.Time) error {
	if !validExpires(expires) {
		return ErrInvalidExpires{expires}
	}

	if err := r.verifySignatures("snapshot.json"); err != nil {
		return err
	}
	timestamp, err := r.timestamp()
	if err != nil {
		return err
	}
	timestamp.Meta["snapshot.json"], err = r.timestampFileMeta("snapshot.json")
	if err != nil {
		return err
	}
	timestamp.Expires = expires.Round(time.Second)
	if !r.local.FileIsStaged("timestamp.json") {
		timestamp.Version++
	}

	err = r.setMeta("timestamp.json", timestamp)
	if err == nil {
		r.logger.Println("Staged timestamp.json metadata with expiration date:", timestamp.Expires)
	}
	return err
}

func (r *Repo) fileVersions() (map[string]int64, error) {
	versions := make(map[string]int64)

	for fileName := range r.meta {
		if roles.IsVersionedManifest(fileName) {
			continue
		}

		roleName := strings.TrimSuffix(fileName, ".json")

		var version int64

		switch roleName {
		case "root":
			root, err := r.root()
			if err != nil {
				return nil, err
			}
			version = root.Version
		case "snapshot":
			snapshot, err := r.snapshot()
			if err != nil {
				return nil, err
			}
			version = snapshot.Version
		case "timestamp":
			continue
		default:
			// Targets or delegated targets manifest.
			targets, err := r.targets(roleName)
			if err != nil {
				return nil, err
			}

			version = targets.Version
		}

		versions[fileName] = version
	}

	return versions, nil
}

func (r *Repo) fileHashes() (map[string]data.Hashes, error) {
	hashes := make(map[string]data.Hashes)

	for fileName := range r.meta {
		if roles.IsVersionedManifest(fileName) {
			continue
		}

		roleName := strings.TrimSuffix(fileName, ".json")

		switch roleName {
		case "snapshot":
			timestamp, err := r.timestamp()
			if err != nil {
				return nil, err
			}

			if m, ok := timestamp.Meta[fileName]; ok {
				hashes[fileName] = m.Hashes
			}
		case "timestamp":
			continue
		default:
			snapshot, err := r.snapshot()
			if err != nil {
				return nil, err
			}
			if m, ok := snapshot.Meta[fileName]; ok {
				hashes[fileName] = m.Hashes
			}

			if roleName != "root" {
				// Scalability issue: Commit/fileHashes loads all targets metadata into memory
				// https://github.com/DataDog/go-tuf/issues/245
				t, err := r.targets(roleName)
				if err != nil {
					return nil, err
				}
				for name, m := range t.Targets {
					hashes[path.Join("targets", name)] = m.Hashes
				}
			}

		}

	}

	return hashes, nil
}

func (r *Repo) Commit() error {
	// check we have all the metadata
	for _, name := range topLevelMetadata {
		if _, ok := r.meta[name]; !ok {
			return ErrMissingMetadata{name}
		}
	}

	// check roles are valid
	root, err := r.root()
	if err != nil {
		return err
	}
	for name, role := range root.Roles {
		if len(role.KeyIDs) < role.Threshold {
			return ErrNotEnoughKeys{name, len(role.KeyIDs), role.Threshold}
		}
	}

	// verify hashes in snapshot.json are up to date
	snapshot, err := r.snapshot()
	if err != nil {
		return err
	}
	for _, name := range r.snapshotMetadata() {
		expected, ok := snapshot.Meta[name]
		if !ok {
			return fmt.Errorf("tuf: snapshot.json missing hash for %s", name)
		}
		actual, err := r.snapshotFileMeta(name)
		if err != nil {
			return err
		}
		if err := util.SnapshotFileMetaEqual(actual, expected); err != nil {
			return fmt.Errorf("tuf: invalid %s in snapshot.json: %s", name, err)
		}
	}

	// verify hashes in timestamp.json are up to date
	timestamp, err := r.timestamp()
	if err != nil {
		return err
	}
	snapshotMeta, err := r.timestampFileMeta("snapshot.json")
	if err != nil {
		return err
	}
	if err := util.TimestampFileMetaEqual(snapshotMeta, timestamp.Meta["snapshot.json"]); err != nil {
		return fmt.Errorf("tuf: invalid snapshot.json in timestamp.json: %s", err)
	}

	for _, name := range topLevelMetadata {
		if err := r.verifySignatures(name); err != nil {
			return err
		}
	}

	versions, err := r.fileVersions()
	if err != nil {
		return err
	}
	hashes, err := r.fileHashes()
	if err != nil {
		return err
	}

	err = r.local.Commit(root.ConsistentSnapshot, versions, hashes)
	if err == nil {
		r.logger.Println("Committed successfully")
	}
	return err
}

func (r *Repo) Clean() error {
	err := r.local.Clean()
	if err == nil {
		r.logger.Println("Removed all staged metadata and target files")
	}
	return err
}

func (r *Repo) verifySignatures(metaFilename string) error {
	s, err := r.SignedMeta(metaFilename)
	if err != nil {
		return err
	}

	role := strings.TrimSuffix(metaFilename, ".json")

	dbs, err := r.dbsForRole(role)
	if err != nil {
		return err
	}

	for _, db := range dbs {
		if err := db.Verify(s, role, 0); err != nil {
			return ErrInsufficientSignatures{metaFilename, err}
		}
	}

	return nil
}

func (r *Repo) snapshotFileMeta(roleFilename string) (data.SnapshotFileMeta, error) {
	b, ok := r.meta[roleFilename]
	if !ok {
		return data.SnapshotFileMeta{}, ErrMissingMetadata{roleFilename}
	}
	return util.GenerateSnapshotFileMeta(bytes.NewReader(b), r.hashAlgorithms...)
}

func (r *Repo) timestampFileMeta(roleFilename string) (data.TimestampFileMeta, error) {
	b, ok := r.meta[roleFilename]
	if !ok {
		return data.TimestampFileMeta{}, ErrMissingMetadata{roleFilename}
	}
	return util.GenerateTimestampFileMeta(bytes.NewReader(b), r.hashAlgorithms...)
}

func (r *Repo) Payload(roleFilename string) ([]byte, error) {
	s, err := r.SignedMeta(roleFilename)
	if err != nil {
		return nil, err
	}

	p, err := cjson.EncodeCanonical(s.Signed)
	if err != nil {
		return nil, err
	}

	return p, nil
}

func (r *Repo) CheckRoleUnexpired(role string, validAt time.Time) error {
	var expires time.Time
	switch role {
	case "root":
		root, err := r.root()
		if err != nil {
			return err
		}
		expires = root.Expires
	case "snapshot":
		snapshot, err := r.snapshot()
		if err != nil {
			return err
		}
		expires = snapshot.Expires
	case "timestamp":
		timestamp, err := r.timestamp()
		if err != nil {
			return err
		}
		expires = timestamp.Expires
	case "targets":
		targets, err := r.topLevelTargets()
		if err != nil {
			return err
		}
		expires = targets.Expires
	default:
		return fmt.Errorf("invalid role: %s", role)
	}
	if expires.Before(validAt) || expires.Equal(validAt) {
		return fmt.Errorf("role expired on: %s", expires)
	}
	return nil
}

// GetMeta returns the underlying meta file map from the store.
func (r *Repo) GetMeta() (map[string]json.RawMessage, error) {
	return r.local.GetMeta()
}
