// Copyright 2023 Canonical Ltd.
// Licensed under the LGPLv3 with static-linking exception.
// See LICENCE file for details.

package policyutil

import (
	"bytes"
	"crypto"
	"errors"
	"fmt"
	"hash/fnv"
	"io"
	"reflect"
	"strings"
	"unicode/utf8"

	"github.com/canonical/go-tpm2"
	"github.com/canonical/go-tpm2/mu"
)

const (
	pathForbiddenChars = "{}*<>"

	// We use command codes to identify element types. In the case
	// where we need a custom command code for a special element,
	// we set the vendor bit (0x20000000) and also set one of the
	// reserved bits (0xdfff0000)
	commandRawPolicyOR     tpm2.CommandCode = 0x20010171
	commandPolicyPCRDigest tpm2.CommandCode = 0x2002017F
)

var (
	// ErrMissingDigest is returned from [Policy.Execute] when a TPM2_PolicyCpHash or
	// TPM2_PolicyNameHash assertion is missing a digest for the selected session algorithm.
	ErrMissingDigest = errors.New("missing digest for session algorithm")
)

type (
	taskFn       func() error
	authMapKey   uint32
	ticketMapKey uint32
)

func mapKey(vals ...interface{}) uint32 {
	h := fnv.New32()
	mu.MustMarshalToWriter(h, vals...)
	return h.Sum32()
}

func makeAuthMapKey(authName tpm2.Name, policyRef tpm2.Nonce) authMapKey {
	return authMapKey(mapKey(authName, policyRef))
}

func makeTicketMapKey(ticket *PolicyTicket) ticketMapKey {
	return ticketMapKey(mapKey(ticket.AuthName, ticket.PolicyRef, ticket.CpHash))
}

// PolicyTicket corresponds to a ticket generated from a TPM2_PolicySigned or TPM2_PolicySecret
// assertion and is returned by [Policy.Execute]. Generated tickets can be supplied to
// [Policy.Execute] in the future in order to satisfy these assertions as long as they haven't
// expired.
type PolicyTicket struct {
	AuthName  tpm2.Name    // The name of the auth object associated with the corresponding assertion
	PolicyRef tpm2.Nonce   // The policy ref of the corresponding assertion
	CpHash    tpm2.Digest  // The cpHash supplied to the assertion that generated this ticket
	Timeout   tpm2.Timeout // The timeout returned by the assertion that generated this ticket

	// Ticket is the actual ticket returned by the TPM for the assertion that generated this ticket.
	// The Tag field indicates whether this was generated by TPM2_PolicySigned or TPM2_PolicySecret.
	Ticket *tpm2.TkAuth
}

// PolicyError is returned from [Policy.Execute] and other methods when an error
// is encountered during some processing of a policy. It provides an indication of
// where an error occurred.
type PolicyError struct {
	Path string // the path of the branch at which the error occurred

	task string
	err  error
}

type policyDelimiterError interface {
	error
	isPolicyDelimiterError()
}

// makePolicyError returns a PolicyError in the following way:
//   - If the supplied error already contains a PolicyError from the current policy,
//     it is unwrapped to the first error within the current policy and returned.
//   - If the supplied error does not contain a PolicyError from the current policy
//     the error is wrapped with a new PolicyError.
//
// A policy boundary is indicated by the presence of a policyDelimiterError.
func makePolicyError(err error, path policyBranchPath, task string) *PolicyError {
	pErr := &PolicyError{Path: string(path), task: task, err: err}

	var delim policyDelimiterError
	errors.As(err, &delim)

	var pErrNext *PolicyError
	for errors.As(errors.Unwrap(pErr), &pErrNext) {
		if delim != nil {
			var delim2 policyDelimiterError
			if !errors.As(pErrNext, &delim2) || delim2 != delim {
				break
			}
		}
		pErr = pErrNext
	}

	return pErr
}

func (e *PolicyError) Error() string {
	branch := "root branch"
	if len(e.Path) > 0 {
		branch = "branch '" + e.Path + "'"
	}
	return fmt.Sprintf("cannot run '%s' task in %s: %v", e.task, branch, e.err)
}

func (e *PolicyError) Unwrap() error {
	return e.err
}

// PolicyNVError is returned from [Policy.Execute] and other methods when an error
// is encountered when executing a TPM2_PolicyNV assertion. If there was an error
// authorizing use of the NV index with a policy session, this will wrap a
// *[ResourceAuthorizeError].
type PolicyNVError struct {
	Index tpm2.Handle // The NV index handle
	Name  tpm2.Name   // The NV index name

	err error
}

func (e *PolicyNVError) Error() string {
	return fmt.Sprintf("cannot complete assertion with NV index %v (name: %#x): %v", e.Index, e.Name, e.err)
}

func (e *PolicyNVError) Unwrap() error {
	return e.err
}

// PolicyAuthorizationError is returned from [Policy.Execute] if:
//   - the policy uses TPM2_PolicySecret and the associated resource could not be authorized. When
//     this occurs because there was an error loading the associated resource, this will wrap a
//     *[ResourceLoadError]. If there was an error authorizing use of the resource with a policy
//     session, this will wrap a *[ResourceAuthorizeError].
//   - the policy uses TPM2_PolicySigned and no or an invalid signed authorization was supplied.
//   - the policy uses TPM2_PolicyAuthorize and no or an invalid authorized policy was supplied.
type PolicyAuthorizationError struct {
	AuthName  tpm2.Name
	PolicyRef tpm2.Nonce
	err       error
}

func (e *PolicyAuthorizationError) Error() string {
	return fmt.Sprintf("cannot complete authorization with authName=%#x, policyRef=%#x: %v", e.AuthName, e.PolicyRef, e.err)
}

func (e *PolicyAuthorizationError) Unwrap() error {
	return e.err
}

// ResourceLoadError is returned from [Policy.Execute] if the policy uses TPM2_PolicySecret
// and the associated resource could not be loaded. If loading the resource required
// authorization with a policy session and that failed, this will wrap another *[PolicyError].
type ResourceLoadError struct {
	Name tpm2.Name
	err  error
}

func (e *ResourceLoadError) Error() string {
	return fmt.Sprintf("cannot load resource with name %#x: %v", e.Name, e.err)
}

func (e *ResourceLoadError) Unwrap() error {
	return e.err
}

func (*ResourceLoadError) isPolicyDelimiterError() {}

// ResourceAuthorizeError is returned from [Policy.Execute] if an error is encountered
// when trying to authorize a resource required by a policy. This should be wrappped in
// either a *[PolicyNVError] or *[PolicyAuthorizationError] which indicates the assertion
// that the error occurred for. This may wrap another *[PolicyError].
type ResourceAuthorizeError struct {
	Name tpm2.Name
	err  error
}

func (e *ResourceAuthorizeError) Error() string {
	return fmt.Sprintf("cannot authorize resource with name %#x: %v", e.Name, e.err)
}

func (e *ResourceAuthorizeError) Unwrap() error {
	return e.err
}

func (*ResourceAuthorizeError) isPolicyDelimiterError() {}

type policyBranchName string

func (n policyBranchName) isValid() bool {
	if !utf8.ValidString(string(n)) {
		return false
	}
	if strings.ContainsAny(string(n), pathForbiddenChars) {
		return false
	}
	return true
}

func (n policyBranchName) Marshal(w io.Writer) error {
	if !n.isValid() {
		return errors.New("invalid name")
	}
	_, err := mu.MarshalToWriter(w, []byte(n))
	return err
}

func (n *policyBranchName) Unmarshal(r io.Reader) error {
	var b []byte
	if _, err := mu.UnmarshalFromReader(r, &b); err != nil {
		return err
	}
	name := policyBranchName(b)
	if !name.isValid() {
		return errors.New("invalid name")
	}
	*n = name
	return nil
}

type policyBranchPath string

func (p policyBranchPath) PopNextComponent() (next string, remaining policyBranchPath) {
	remaining = p
	for len(remaining) > 0 {
		s := strings.SplitN(string(remaining), "/", 2)
		remaining = ""
		if len(s) == 2 {
			remaining = policyBranchPath(s[1])
		}
		component := s[0]
		if len(component) > 0 {
			return component, remaining
		}
	}

	return "", ""
}

func (p policyBranchPath) Concat(path string) policyBranchPath {
	var pathElements []string
	if p != "" {
		pathElements = append(pathElements, string(p))
	}
	if path != "" {
		pathElements = append(pathElements, path)
	}
	return policyBranchPath(strings.Join(pathElements, "/"))
}

type authorizedPolicy struct {
	policyBranch
	authorization *PolicyAuthorization
}

type policyTickets interface {
	ticket(authName tpm2.Name, policyRef tpm2.Nonce) *PolicyTicket
	addTicket(ticket *PolicyTicket)
	invalidTicket(ticket *PolicyTicket)
}

type policyRunner interface {
	session() policySession
	tickets() policyTickets
	resources() policyResources

	authResourceName() tpm2.Name
	loadExternal(public *tpm2.Public) (ResourceContext, error)
	authorize(auth ResourceContext, askForPolicy bool, usage *PolicySessionUsage, prefer tpm2.SessionType) (SessionContext, error)
	runBranch(branches policyBranches) (selected int, err error)
	runAuthorizedPolicy(keySign *tpm2.Public, policyRef tpm2.Nonce, policies []*authorizedPolicy) (approvedPolicy tpm2.Digest, checkTicket *tpm2.TkVerified, err error)
	notifyPolicyPCRDigest() error
}

type taggedHash struct {
	HashAlg tpm2.HashAlgorithmId
	Digest  tpm2.Digest
}

func (h taggedHash) Marshal(w io.Writer) error {
	ta := tpm2.MakeTaggedHash(h.HashAlg, h.Digest)
	_, err := mu.MarshalToWriter(w, ta)
	return err
}

func (h *taggedHash) Unmarshal(r io.Reader) error {
	var ta tpm2.TaggedHash
	if _, err := mu.UnmarshalFromReader(r, &ta); err != nil {
		return err
	}

	if ta.HashAlg != tpm2.HashAlgorithmNull && !ta.HashAlg.IsValid() {
		return errors.New("invalid digest algorithm")
	}

	*h = taggedHash{
		HashAlg: ta.HashAlg,
		Digest:  ta.Digest()}
	return nil
}

type taggedHashList []taggedHash

type policyNVElement struct {
	NvIndex   *tpm2.NVPublic
	OperandB  tpm2.Operand
	Offset    uint16
	Operation tpm2.ArithmeticOp
}

func (*policyNVElement) name() string { return "TPM2_PolicyNV assertion" }

func (e *policyNVElement) run(runner policyRunner) (err error) {
	nvIndex, err := tpm2.NewNVIndexResourceContextFromPub(e.NvIndex)
	if err != nil {
		return fmt.Errorf("cannot create nvIndex context: %w", err)
	}

	var auth ResourceContext = newResourceContext(nvIndex, nil)
	askForPolicy := true
	switch {
	case e.NvIndex.Attrs&tpm2.AttrNVPolicyRead != 0:
		// use NV index for auth
	case e.NvIndex.Attrs&tpm2.AttrNVAuthRead != 0:
		// use NV index for auth
	case e.NvIndex.Attrs&tpm2.AttrNVOwnerRead != 0:
		auth, err = runner.resources().loadedResource(tpm2.MakeHandleName(tpm2.HandleOwner))
		askForPolicy = false
	case e.NvIndex.Attrs&tpm2.AttrNVPPRead != 0:
		auth, err = runner.resources().loadedResource(tpm2.MakeHandleName(tpm2.HandlePlatform))
		askForPolicy = false
	default:
		return errors.New("invalid nvIndex read auth mode")
	}
	if err != nil {
		return &PolicyNVError{
			Index: nvIndex.Handle(),
			Name:  nvIndex.Name(),
			err:   fmt.Errorf("cannot create auth context: %w", err),
		}
	}

	usage := NewPolicySessionUsage(
		tpm2.CommandPolicyNV,
		[]NamedHandle{auth.Resource(), nvIndex, runner.session().Name()},
		e.OperandB, e.Offset, e.Operation,
	)

	authSession, err := runner.authorize(auth, askForPolicy, usage, tpm2.SessionTypePolicy)
	if err != nil {
		return &PolicyNVError{
			Index: nvIndex.Handle(),
			Name:  nvIndex.Name(),
			err:   &ResourceAuthorizeError{Name: nvIndex.Name(), err: err},
		}
	}
	defer authSession.Flush()

	if err := runner.session().PolicyNV(auth.Resource(), nvIndex, e.OperandB, e.Offset, e.Operation, authSession.Session()); err != nil {
		return &PolicyNVError{Index: nvIndex.Handle(), Name: nvIndex.Name(), err: err}
	}

	return nil
}

type policySecretElement struct {
	AuthObjectName tpm2.Name
	PolicyRef      tpm2.Nonce
	CpHashA        tpm2.Digest
	Expiration     int32
}

func (*policySecretElement) name() string { return "TPM2_PolicySecret assertion" }

func (e *policySecretElement) run(runner policyRunner) (err error) {
	if ticket := runner.tickets().ticket(e.AuthObjectName, e.PolicyRef); ticket != nil {
		err := runner.session().PolicyTicket(ticket.Timeout, ticket.CpHash, ticket.PolicyRef, ticket.AuthName, ticket.Ticket)
		switch {
		case tpm2.IsTPMParameterError(err, tpm2.ErrorExpired, tpm2.CommandPolicyTicket, 1):
			// The ticket has expired - ignore this and fall through to PolicySecret
			runner.tickets().invalidTicket(ticket)
		case tpm2.IsTPMParameterError(err, tpm2.ErrorTicket, tpm2.CommandPolicyTicket, 5):
			// The ticket is invalid - ignore this and fall through to PolicySecret
			runner.tickets().invalidTicket(ticket)
		case err != nil:
			return &PolicyAuthorizationError{AuthName: e.AuthObjectName, PolicyRef: e.PolicyRef, err: err}
		default:
			// The ticket was accepted
			return nil
		}
	}

	authObject, err := runner.resources().loadedResource(e.AuthObjectName)
	if err != nil {
		return &PolicyAuthorizationError{
			AuthName:  e.AuthObjectName,
			PolicyRef: e.PolicyRef,
			err:       &ResourceLoadError{Name: e.AuthObjectName, err: err},
		}
	}
	defer authObject.Flush()

	usage := NewPolicySessionUsage(
		tpm2.CommandPolicySecret,
		[]NamedHandle{authObject.Resource(), runner.session().Name()},
		e.CpHashA, e.PolicyRef, e.Expiration,
	)

	authSession, err := runner.authorize(authObject, false, usage, tpm2.SessionTypeHMAC)
	if err != nil {
		return &PolicyAuthorizationError{
			AuthName:  e.AuthObjectName,
			PolicyRef: e.PolicyRef,
			err:       &ResourceAuthorizeError{Name: e.AuthObjectName, err: err},
		}
	}
	defer authSession.Flush()

	timeout, ticket, err := runner.session().PolicySecret(authObject.Resource(), e.CpHashA, e.PolicyRef, e.Expiration, authSession.Session())
	if err != nil {
		return &PolicyAuthorizationError{AuthName: e.AuthObjectName, PolicyRef: e.PolicyRef, err: err}
	}

	runner.tickets().addTicket(&PolicyTicket{
		AuthName:  e.AuthObjectName,
		PolicyRef: e.PolicyRef,
		CpHash:    nil,
		Timeout:   timeout,
		Ticket:    ticket})
	return nil
}

type policySignedElement struct {
	AuthKey   *tpm2.Public
	PolicyRef tpm2.Nonce
	Unused1   tpm2.Digest
	Unused2   int32
}

func (*policySignedElement) name() string { return "TPM2_PolicySigned assertion" }

func (e *policySignedElement) run(runner policyRunner) error {
	authKeyName := e.AuthKey.Name()
	if !authKeyName.IsValid() {
		return errors.New("invalid auth key name")
	}

	if ticket := runner.tickets().ticket(authKeyName, e.PolicyRef); ticket != nil {
		err := runner.session().PolicyTicket(ticket.Timeout, ticket.CpHash, ticket.PolicyRef, ticket.AuthName, ticket.Ticket)
		switch {
		case tpm2.IsTPMParameterError(err, tpm2.ErrorExpired, tpm2.CommandPolicyTicket, 1):
			// The ticket has expired - ignore this and fall through to PolicySigned
			runner.tickets().invalidTicket(ticket)
		case tpm2.IsTPMParameterError(err, tpm2.ErrorTicket, tpm2.CommandPolicyTicket, 5):
			// The ticket is invalid - ignore this and fall through to PolicySigned
			runner.tickets().invalidTicket(ticket)
		case err != nil:
			return &PolicyAuthorizationError{AuthName: authKeyName, PolicyRef: e.PolicyRef, err: err}
		default:
			// The ticket was accepted
			return nil
		}
	}

	auth, err := runner.resources().signedAuthorization(authKeyName, e.PolicyRef)
	if err != nil {
		return &PolicyAuthorizationError{
			AuthName:  authKeyName,
			PolicyRef: e.PolicyRef,
			err:       fmt.Errorf("cannot obtain signed authorization: %w", err),
		}
	}

	authKey, err := runner.loadExternal(e.AuthKey)
	if err != nil {
		return fmt.Errorf("cannot create authKey context: %w", err)
	}
	defer authKey.Flush()

	includeNonceTPM := false
	if len(auth.NonceTPM) > 0 {
		includeNonceTPM = true
	}

	timeout, ticket, err := runner.session().PolicySigned(authKey.Resource(), includeNonceTPM, auth.CpHash, e.PolicyRef, auth.Expiration, auth.PolicyAuthorization.Signature)
	if err != nil {
		return &PolicyAuthorizationError{AuthName: authKeyName, PolicyRef: e.PolicyRef, err: err}
	}

	runner.tickets().addTicket(&PolicyTicket{
		AuthName:  authKeyName,
		PolicyRef: e.PolicyRef,
		CpHash:    auth.CpHash,
		Timeout:   timeout,
		Ticket:    ticket})
	return nil
}

type policyAuthorizations []PolicyAuthorization

type policyAuthorizeElement struct {
	PolicyRef tpm2.Nonce
	KeySign   *tpm2.Public
}

func (*policyAuthorizeElement) name() string { return "authorized policy" }

func (e *policyAuthorizeElement) run(runner policyRunner) error {
	keySignName := e.KeySign.Name()
	if !keySignName.IsValid() {
		return errors.New("invalid keySign")
	}

	policies, err := runner.resources().authorizedPolicies(keySignName, e.PolicyRef)
	if err != nil {
		return &PolicyAuthorizationError{AuthName: keySignName, PolicyRef: e.PolicyRef, err: err}
	}

	// Filter out policies that aren't computed for the current session algorithm or
	// don't have a matching authorization, although we shouldn't really have any
	// without a matching authorization.
	var candidatePolicies []*authorizedPolicy
	for _, policy := range policies {
		digest, err := policy.Digest(runner.session().HashAlg())
		if err == ErrMissingDigest {
			// no suitable digest
			continue
		}
		if err != nil {
			return err
		}

		// Find the signed authorization
		var policyAuth *PolicyAuthorization
		for _, auth := range policy.policy.PolicyAuthorizations {
			if auth.Signature == nil {
				continue
			}
			if !bytes.Equal(auth.AuthKey.Name(), keySignName) {
				continue
			}
			if !bytes.Equal(auth.PolicyRef, e.PolicyRef) {
				continue
			}
			if ok, _ := auth.Verify(digest); !ok {
				continue
			}
			policyAuth = &auth
			break
		}
		if policyAuth == nil {
			// no matching authorization - this shouldn't really happen.
			continue
		}

		candidatePolicies = append(candidatePolicies, &authorizedPolicy{
			policyBranch: policyBranch{
				Name:          policyBranchName(fmt.Sprintf("%x", digest)),
				Policy:        policy.policy.Policy,
				PolicyDigests: taggedHashList{{HashAlg: runner.session().HashAlg(), Digest: digest}},
			},
			authorization: policyAuth,
		})
	}

	approvedPolicy, checkTicket, err := runner.runAuthorizedPolicy(e.KeySign, e.PolicyRef, candidatePolicies)
	if err != nil {
		return &PolicyAuthorizationError{AuthName: keySignName, PolicyRef: e.PolicyRef, err: err}
	}

	if err := runner.session().PolicyAuthorize(approvedPolicy, e.PolicyRef, keySignName, checkTicket); err != nil {
		return &PolicyAuthorizationError{AuthName: keySignName, PolicyRef: e.PolicyRef, err: err}
	}
	return nil
}

type policyAuthValueElement struct{}

func (*policyAuthValueElement) name() string { return "TPM2_PolicyAuthValue assertion" }

func (*policyAuthValueElement) run(runner policyRunner) error {
	return runner.session().PolicyAuthValue()
}

type policyCommandCodeElement struct {
	CommandCode tpm2.CommandCode
}

func (*policyCommandCodeElement) name() string { return "TPM2_PolicyCommandCode assertion" }

func (e *policyCommandCodeElement) run(runner policyRunner) error {
	return runner.session().PolicyCommandCode(e.CommandCode)
}

type policyCounterTimerElement struct {
	OperandB  tpm2.Operand
	Offset    uint16
	Operation tpm2.ArithmeticOp
}

func (*policyCounterTimerElement) name() string { return "TPM2_PolicyCounterTimer assertion" }

func (e *policyCounterTimerElement) run(runner policyRunner) error {
	return runner.session().PolicyCounterTimer(e.OperandB, e.Offset, e.Operation)
}

type cpHashParams struct {
	CommandCode tpm2.CommandCode
	Handles     []tpm2.Name
	CpBytes     []byte
}

type policyCpHashElement struct {
	Digest tpm2.Digest
}

func (*policyCpHashElement) name() string { return "TPM2_PolicyCpHash assertion" }

func (e *policyCpHashElement) run(runner policyRunner) error {
	return runner.session().PolicyCpHash(e.Digest)
}

type policyNameHashElement struct {
	Digest tpm2.Digest
}

func (*policyNameHashElement) name() string { return "TPM2_PolicyNameHash assertion" }

func (e *policyNameHashElement) run(runner policyRunner) error {
	return runner.session().PolicyNameHash(e.Digest)
}

type policyBranch struct {
	Name          policyBranchName
	PolicyDigests taggedHashList
	Policy        policyElements
}

type policyBranches []*policyBranch

func (b policyBranches) selectBranch(next string) (int, error) {
	switch {
	case next[0] == '{':
		// select branch by index
		var selected int
		if _, err := fmt.Sscanf(string(next), "{%d}", &selected); err != nil {
			return 0, fmt.Errorf("cannot select branch: badly formatted path component \"%s\": %w", next, err)
		}
		if selected < 0 || selected >= len(b) {
			return 0, fmt.Errorf("cannot select branch: selected path %d out of range", selected)
		}
		return selected, nil
	case strings.ContainsAny(string(next), pathForbiddenChars):
		return 0, fmt.Errorf("cannot select branch: invalid component \"%s\"", next)
	default:
		// select branch by name
		for i, branch := range b {
			if len(branch.Name) == 0 {
				continue
			}
			if string(branch.Name) == next {
				return i, nil
			}
		}
		return 0, fmt.Errorf("cannot select branch: no branch with name \"%s\"", next)
	}
}

type policyRawORElement struct {
	HashList tpm2.DigestList
}

func (*policyRawORElement) name() string { return "TPM2_PolicyOR assertion" }

func (e *policyRawORElement) run(runner policyRunner) error {
	return runner.session().PolicyOR(e.HashList)
}

type policyORElement struct {
	Branches policyBranches
}

func (*policyORElement) name() string { return "branch node" }

func (e *policyORElement) run(runner policyRunner) error {
	selected, err := runner.runBranch(e.Branches)
	if err != nil {
		return err
	}

	// Obtain the branch digests
	var digests tpm2.DigestList
	for _, branch := range e.Branches {
		found := false
		for _, digest := range branch.PolicyDigests {
			if digest.HashAlg != runner.session().HashAlg() {
				continue
			}

			digests = append(digests, digest.Digest)
			found = true
			break
		}
		if !found {
			return ErrMissingDigest
		}
	}

	tree, err := newPolicyOrTree(runner.session().HashAlg(), digests)
	if err != nil {
		return fmt.Errorf("cannot compute PolicyOR tree: %w", err)
	}

	skipIntermediates := false
	if selected < 0 {
		selected = 0
		skipIntermediates = true
	}
	pHashLists := tree.selectBranch(selected)

	if skipIntermediates {
		return runner.session().PolicyOR(pHashLists[len(pHashLists)-1])
	} else {
		for _, pHashList := range pHashLists {
			if err := runner.session().PolicyOR(pHashList); err != nil {
				return err
			}
		}
	}
	return nil
}

type pcrValue struct {
	PCR    tpm2.Handle
	Digest taggedHash
}

type pcrValueList []pcrValue

type policyPCRElement struct {
	PCRs pcrValueList
}

func (*policyPCRElement) name() string { return "TPM2_PolicyPCR assertion" }

func (e *policyPCRElement) run(runner policyRunner) error {
	values, err := e.pcrValues()
	if err != nil {
		return err
	}
	pcrs, pcrDigest, err := ComputePCRDigestFromAllValues(runner.session().HashAlg(), values)
	if err != nil {
		return fmt.Errorf("cannot compute PCR digest: %w", err)
	}
	return runner.session().PolicyPCR(pcrDigest, pcrs)
}

func (e *policyPCRElement) pcrValues() (tpm2.PCRValues, error) {
	values := make(tpm2.PCRValues)
	for i, value := range e.PCRs {
		if value.PCR.Type() != tpm2.HandleTypePCR {
			return nil, fmt.Errorf("invalid PCR handle at index %d", i)
		}
		if err := values.SetValue(value.Digest.HashAlg, int(value.PCR), value.Digest.Digest); err != nil {
			return nil, fmt.Errorf("invalid PCR value at index %d: %w", i, err)
		}
	}
	return values, nil
}

type policyPCRDigestElement struct {
	PCRDigest tpm2.Digest
	PCRs      tpm2.PCRSelectionList
}

func (*policyPCRDigestElement) name() string { return "TPM2_PolicyPCR assertion" }

func (e *policyPCRDigestElement) run(runner policyRunner) error {
	if err := runner.notifyPolicyPCRDigest(); err != nil {
		return err
	}
	return runner.session().PolicyPCR(e.PCRDigest, e.PCRs)
}

type policyDuplicationSelectElement struct {
	Object        tpm2.Name
	NewParent     tpm2.Name
	IncludeObject bool
}

func (*policyDuplicationSelectElement) name() string { return "TPM2_PolicyDuplicationSelect assertion" }

func (e *policyDuplicationSelectElement) run(runner policyRunner) error {
	object := e.Object
	if len(object) == 0 && !e.IncludeObject {
		object = runner.authResourceName()
	}
	return runner.session().PolicyDuplicationSelect(object, e.NewParent, e.IncludeObject)
}

type policyPasswordElement struct{}

func (*policyPasswordElement) name() string { return "TPM2_PolicyPassword assertion" }

func (*policyPasswordElement) run(runner policyRunner) error {
	return runner.session().PolicyPassword()
}

type policyNvWrittenElement struct {
	WrittenSet bool
}

func (*policyNvWrittenElement) name() string { return "TPM2_PolicyNvWritten assertion" }

func (e *policyNvWrittenElement) run(runner policyRunner) error {
	return runner.session().PolicyNvWritten(e.WrittenSet)
}

type policyElementDetails struct {
	NV                *policyNVElement
	Secret            *policySecretElement
	Signed            *policySignedElement
	Authorize         *policyAuthorizeElement
	AuthValue         *policyAuthValueElement
	CommandCode       *policyCommandCodeElement
	CounterTimer      *policyCounterTimerElement
	CpHash            *policyCpHashElement
	NameHash          *policyNameHashElement
	OR                *policyORElement
	PCR               *policyPCRElement
	DuplicationSelect *policyDuplicationSelectElement
	Password          *policyPasswordElement
	NvWritten         *policyNvWrittenElement

	RawOR     *policyRawORElement
	PCRDigest *policyPCRDigestElement
}

func (d *policyElementDetails) Select(selector reflect.Value) interface{} {
	switch selector.Interface().(tpm2.CommandCode) {
	case tpm2.CommandPolicyNV:
		return &d.NV
	case tpm2.CommandPolicySecret:
		return &d.Secret
	case tpm2.CommandPolicySigned:
		return &d.Signed
	case tpm2.CommandPolicyAuthorize:
		return &d.Authorize
	case tpm2.CommandPolicyAuthValue:
		return &d.AuthValue
	case tpm2.CommandPolicyCommandCode:
		return &d.CommandCode
	case tpm2.CommandPolicyCounterTimer:
		return &d.CounterTimer
	case tpm2.CommandPolicyCpHash:
		return &d.CpHash
	case tpm2.CommandPolicyNameHash:
		return &d.NameHash
	case tpm2.CommandPolicyOR:
		return &d.OR
	case tpm2.CommandPolicyPCR:
		return &d.PCR
	case tpm2.CommandPolicyDuplicationSelect:
		return &d.DuplicationSelect
	case tpm2.CommandPolicyPassword:
		return &d.Password
	case tpm2.CommandPolicyNvWritten:
		return &d.NvWritten
	case commandRawPolicyOR:
		return &d.RawOR
	case commandPolicyPCRDigest:
		return &d.PCRDigest
	default:
		return nil
	}
}

type policyElementRunner interface {
	name() string
	run(runner policyRunner) error
}

type policyElement struct {
	Type    tpm2.CommandCode
	Details *policyElementDetails
}

func (e *policyElement) runner() policyElementRunner {
	switch e.Type {
	case tpm2.CommandPolicyNV:
		return e.Details.NV
	case tpm2.CommandPolicySecret:
		return e.Details.Secret
	case tpm2.CommandPolicySigned:
		return e.Details.Signed
	case tpm2.CommandPolicyAuthorize:
		return e.Details.Authorize
	case tpm2.CommandPolicyAuthValue:
		return e.Details.AuthValue
	case tpm2.CommandPolicyCommandCode:
		return e.Details.CommandCode
	case tpm2.CommandPolicyCounterTimer:
		return e.Details.CounterTimer
	case tpm2.CommandPolicyCpHash:
		return e.Details.CpHash
	case tpm2.CommandPolicyNameHash:
		return e.Details.NameHash
	case tpm2.CommandPolicyOR:
		return e.Details.OR
	case tpm2.CommandPolicyPCR:
		return e.Details.PCR
	case tpm2.CommandPolicyDuplicationSelect:
		return e.Details.DuplicationSelect
	case tpm2.CommandPolicyPassword:
		return e.Details.Password
	case tpm2.CommandPolicyNvWritten:
		return e.Details.NvWritten
	case commandRawPolicyOR:
		return e.Details.RawOR
	case commandPolicyPCRDigest:
		return e.Details.PCRDigest
	default:
		panic("invalid type")
	}
}

type policyElements []*policyElement

type policy struct {
	PolicyDigests        taggedHashList
	PolicyAuthorizations policyAuthorizations
	Policy               policyElements
}

// Policy corresponds to an authorization policy. It can be serialized with
// [github.com/canonical/go-tpm2/mu].
type Policy struct {
	policy policy
}

// Marshal implements [mu.CustomMarshaller.Marshal].
func (p Policy) Marshal(w io.Writer) error {
	_, err := mu.MarshalToWriter(w, uint32(0), p.policy)
	return err
}

// Unmarshal implements [mu.CustomMarshaller.Unarshal].
func (p *Policy) Unmarshal(r io.Reader) error {
	var version uint32
	_, err := mu.UnmarshalFromReader(r, &version, &p.policy)
	if err != nil {
		return err
	}
	if version != 0 {
		return errors.New("invalid version")
	}
	return nil
}

type executePolicyTickets struct {
	usageCpHash tpm2.Digest

	tickets        map[authMapKey][]*PolicyTicket
	newTickets     map[*PolicyTicket]struct{}
	invalidTickets map[*PolicyTicket]struct{}
}

func newExecutePolicyTickets(alg tpm2.HashAlgorithmId, tickets []*PolicyTicket, usage *PolicySessionUsage) (*executePolicyTickets, error) {
	var usageCpHash tpm2.Digest
	if usage != nil {
		var handleNames []Named
		for _, handle := range usage.handles {
			handleNames = append(handleNames, handle)
		}

		var err error
		usageCpHash, err = ComputeCpHash(alg, usage.commandCode, handleNames, usage.params...)
		if err != nil {
			return nil, fmt.Errorf("cannot compute cpHash from usage: %w", err)
		}

	}

	// Drop any tickets with the duplicate authName, policyRef and cpHash
	ticketsFiltered := make(map[ticketMapKey]*PolicyTicket)
	for _, ticket := range tickets {
		key := makeTicketMapKey(ticket)
		if _, exists := ticketsFiltered[key]; exists {
			continue
		}
		ticketsFiltered[key] = ticket
	}

	ticketMap := make(map[authMapKey][]*PolicyTicket)
	for _, ticket := range ticketsFiltered {
		key := makeAuthMapKey(ticket.AuthName, ticket.PolicyRef)
		if _, exists := ticketMap[key]; !exists {
			ticketMap[key] = []*PolicyTicket{}
		}
		ticketMap[key] = append(ticketMap[key], ticket)
	}

	return &executePolicyTickets{
		usageCpHash:    usageCpHash,
		tickets:        ticketMap,
		newTickets:     make(map[*PolicyTicket]struct{}),
		invalidTickets: make(map[*PolicyTicket]struct{}),
	}, nil
}

func (t *executePolicyTickets) ticket(authName tpm2.Name, policyRef tpm2.Nonce) *PolicyTicket {
	tickets := t.tickets[makeAuthMapKey(authName, policyRef)]
	if len(tickets) == 0 {
		return nil
	}
	if len(t.usageCpHash) == 0 {
		return tickets[0]
	}
	for _, ticket := range tickets {
		if len(ticket.CpHash) == 0 {
			return ticket
		}
		if bytes.Equal(ticket.CpHash, t.usageCpHash) {
			return ticket
		}
	}
	return nil
}

func (t *executePolicyTickets) addTicket(ticket *PolicyTicket) {
	if ticket.Ticket == nil || (ticket.Ticket.Hierarchy == tpm2.HandleNull && len(ticket.Ticket.Digest) == 0) {
		// skip null tickets
		return
	}

	key := makeAuthMapKey(ticket.AuthName, ticket.PolicyRef)
	if _, exists := t.tickets[key]; !exists {
		t.tickets[key] = []*PolicyTicket{}
	}
	t.tickets[key] = append([]*PolicyTicket{ticket}, t.tickets[key]...)

	t.newTickets[ticket] = struct{}{}
}

func (t *executePolicyTickets) invalidTicket(ticket *PolicyTicket) {
	key := makeAuthMapKey(ticket.AuthName, ticket.PolicyRef)

	var tickets []*PolicyTicket
	for _, tk := range t.tickets[key] {
		if tk == ticket {
			continue
		}
		tickets = append(tickets, tk)
	}
	t.tickets[key] = tickets

	if _, exists := t.newTickets[ticket]; exists {
		delete(t.newTickets, ticket)
	} else {
		t.invalidTickets[ticket] = struct{}{}
	}
}

func (t *executePolicyTickets) currentTickets() (out []*PolicyTicket) {
	for _, tickets := range t.tickets {
		for _, ticket := range tickets {
			out = append(out, ticket)
		}
	}
	return out
}

type policyExecuteRunner struct {
	policySessionContext SessionContext
	policySession        *teePolicySession
	policyTickets        *executePolicyTickets
	policyResources      *executePolicyResources

	authorizer Authorizer
	tpm        TPMHelper

	usage                *PolicySessionUsage
	ignoreAuthorizations []PolicyAuthorizationID
	ignoreNV             []Named

	wildcardResolver *policyPathWildcardResolver

	remaining   policyBranchPath
	currentPath policyBranchPath
}

func newPolicyExecuteRunner(session PolicySession, tickets *executePolicyTickets, resources *executePolicyResources, authorizer Authorizer, tpm TPMHelper, params *PolicyExecuteParams, details *PolicyBranchDetails) *policyExecuteRunner {
	return &policyExecuteRunner{
		policySessionContext: session.Context(),
		policySession: newTeePolicySession(
			session,
			newRecorderPolicySession(session.HashAlg(), details),
		),
		policyTickets:        tickets,
		policyResources:      resources,
		authorizer:           authorizer,
		tpm:                  tpm,
		usage:                params.Usage,
		ignoreAuthorizations: params.IgnoreAuthorizations,
		ignoreNV:             params.IgnoreNV,
		wildcardResolver:     newPolicyPathWildcardResolver(session.HashAlg(), resources, tpm, params.Usage, params.IgnoreAuthorizations, params.IgnoreNV),
		remaining:            policyBranchPath(params.Path),
	}
}

func (r *policyExecuteRunner) session() policySession {
	return r.policySession
}

func (r *policyExecuteRunner) tickets() policyTickets {
	return r.policyTickets
}

func (r *policyExecuteRunner) resources() policyResources {
	return r.policyResources
}

func (r *policyExecuteRunner) authResourceName() tpm2.Name {
	if r.usage == nil {
		return nil
	}
	return r.usage.handles[r.usage.authIndex].Name()
}

func (r *policyExecuteRunner) loadExternal(public *tpm2.Public) (ResourceContext, error) {
	if public.IsAsymmetric() {
		return r.tpm.LoadExternal(nil, public, tpm2.HandleOwner)
	}

	if !public.Name().IsValid() {
		return nil, errors.New("invalid name")
	}
	sensitive, err := r.policyResources.externalSensitive(public.Name())
	if err != nil {
		return nil, fmt.Errorf("cannot obtain external sensitive area: %w", err)
	}

	return r.tpm.LoadExternal(sensitive, public, tpm2.HandleNull)
}

func (r *policyExecuteRunner) authorize(auth ResourceContext, askForPolicy bool, usage *PolicySessionUsage, prefer tpm2.SessionType) (sessionOut SessionContext, err error) {
	policy := auth.Policy()
	if policy == nil && askForPolicy {
		policy, err = r.policyResources.policy(auth.Resource().Name())
		if err != nil {
			return nil, fmt.Errorf("cannot load policy: %w", err)
		}
	}

	// build available session types
	availableSessionTypes := map[tpm2.SessionType]bool{
		tpm2.SessionTypeHMAC:   true,
		tpm2.SessionTypePolicy: true,
	}
	if policy == nil {
		// no policy was supplied for the resource
		availableSessionTypes[tpm2.SessionTypePolicy] = false
	}

	var alg tpm2.HashAlgorithmId

	switch auth.Resource().Handle().Type() {
	case tpm2.HandleTypeNVIndex:
		pub, err := r.tpm.NVReadPublic(auth.Resource())
		if err != nil {
			return nil, fmt.Errorf("cannot obtain NVPublic: %w", err)
		}
		switch {
		case pub.Attrs&(tpm2.AttrNVAuthRead|tpm2.AttrNVPolicyRead) == tpm2.AttrNVAuthRead:
			// index only supports auth read
			availableSessionTypes[tpm2.SessionTypePolicy] = false
		case pub.Attrs&(tpm2.AttrNVAuthRead|tpm2.AttrNVPolicyRead) == tpm2.AttrNVPolicyRead:
			// index only supports policy read
			availableSessionTypes[tpm2.SessionTypeHMAC] = false
		}
		alg = auth.Resource().Name().Algorithm()
	case tpm2.HandleTypePermanent:
		// Auth value is always available for permanent resources. Auth policy
		// is available if
		policyDigest, err := r.tpm.GetPermanentHandleAuthPolicy(auth.Resource().Handle())
		if err != nil {
			return nil, fmt.Errorf("cannot obtain permanent handle auth policy: %w", err)
		}
		switch {
		case policyDigest.HashAlg == tpm2.HashAlgorithmNull:
			// policy is not enabled for this resource
			alg = r.session().HashAlg()
			availableSessionTypes[tpm2.SessionTypePolicy] = false
		default:
			// policy is enabled for this resource
			alg = policyDigest.HashAlg
		}
	case tpm2.HandleTypeTransient, tpm2.HandleTypePersistent:
		pub, err := r.tpm.ReadPublic(auth.Resource())
		if err != nil {
			return nil, fmt.Errorf("cannot obtain Public: %w", err)
		}
		if pub.Attrs&tpm2.AttrUserWithAuth == 0 {
			// object only supports policy for user role
			availableSessionTypes[tpm2.SessionTypeHMAC] = false
		}
		alg = auth.Resource().Name().Algorithm()
	default:
		return nil, errors.New("unexpected handle type")
	}

	// Select session type
	sessionType := prefer
	if !availableSessionTypes[prefer] {
		var try tpm2.SessionType
		switch prefer {
		case tpm2.SessionTypeHMAC:
			try = tpm2.SessionTypePolicy
		case tpm2.SessionTypePolicy:
			try = tpm2.SessionTypeHMAC
		default:
			panic("invalid preferred session type")
		}
		if !availableSessionTypes[try] {
			return nil, errors.New("no auth types available")
		}
		sessionType = try
	}

	// Save the current policy session to make space for others that might be loaded
	restore, err := r.policySessionContext.Save()
	if err != nil {
		return nil, fmt.Errorf("cannot save session: %w", err)
	}
	defer func() {
		if restoreErr := restore(); restoreErr != nil && err == nil {
			err = fmt.Errorf("cannot restore saved session: %w", restoreErr)
		}
	}()

	session, policySession, err := r.tpm.StartAuthSession(sessionType, alg)
	if err != nil {
		return nil, fmt.Errorf("cannot create session to authorize auth object: %w", err)
	}
	defer func() {
		if err == nil {
			return
		}
		session.Flush()
	}()

	var authValueNeeded bool
	if sessionType == tpm2.SessionTypePolicy {
		params := &PolicyExecuteParams{
			Usage:                usage,
			IgnoreAuthorizations: r.ignoreAuthorizations,
			IgnoreNV:             r.ignoreNV,
		}

		var details PolicyBranchDetails
		runner := newPolicyExecuteRunner(policySession, r.policyTickets, r.policyResources.forSession(session), r.authorizer, r.tpm, params, &details)
		if err := runner.run(policy.policy.Policy); err != nil {
			return nil, err
		}

		authValueNeeded = details.AuthValueNeeded
	} else {
		authValueNeeded = true
	}

	if authValueNeeded {
		if err := r.authorizer.Authorize(auth.Resource()); err != nil {
			return nil, fmt.Errorf("cannot authorize resource: %w", err)
		}
	}

	return session, nil
}

func (r *policyExecuteRunner) runBranch(branches policyBranches) (selected int, err error) {
	if len(branches) == 0 {
		return 0, errors.New("no branches")
	}

	// Select a branch
	selected, name, err := r.selectBranch(branches)
	if err != nil {
		return 0, err
	}

	// Run it!
	r.currentPath = r.currentPath.Concat(name)
	if err := r.run(branches[selected].Policy); err != nil {
		return 0, err
	}

	return selected, nil
}

func (r *policyExecuteRunner) runAuthorizedPolicy(keySign *tpm2.Public, policyRef tpm2.Nonce, policies []*authorizedPolicy) (approvedPolicy tpm2.Digest, checkTicket *tpm2.TkVerified, err error) {
	if len(policies) == 0 {
		return nil, nil, errors.New("no policies")
	}

	var branches policyBranches
	for _, policy := range policies {
		branches = append(branches, &policy.policyBranch)
	}

	// Select a policy
	selected, name, err := r.selectBranch(branches)
	if err != nil {
		return nil, nil, err
	}

	policy := policies[selected]

	// The approved digest and authorization
	approvedPolicy = policy.PolicyDigests[0].Digest
	auth := policy.authorization

	// Verify the signature
	authKey, err := r.tpm.LoadExternal(nil, keySign, tpm2.HandleOwner)
	if err != nil {
		return nil, nil, err
	}
	defer authKey.Flush()

	tbs := ComputePolicyAuthorizationTBSDigest(keySign.Name().Algorithm().GetHash(), approvedPolicy, policyRef)
	ticket, err := r.tpm.VerifySignature(authKey.Resource(), tbs, auth.Signature)
	if err != nil {
		return nil, nil, err
	}

	// Run the policy
	r.currentPath = r.currentPath.Concat(name)
	if err := r.run(policy.Policy); err != nil {
		return nil, nil, err
	}

	return approvedPolicy, ticket, nil
}

func (r *policyExecuteRunner) notifyPolicyPCRDigest() error {
	return nil
}

func (r *policyExecuteRunner) selectBranch(branches policyBranches) (int, string, error) {
	next, remaining := r.remaining.PopNextComponent()
	if len(next) == 0 || next[0] == '*' {
		// There are no more components or the next component is a wildcard match - build a
		// list of candidate paths for this subtree
		path, err := r.wildcardResolver.resolve(branches)
		if err != nil {
			return 0, "", fmt.Errorf("cannot automatically select branch: %w", err)
		}

		switch next {
		case "":
			// We have a path for this whole subtree
			r.remaining = path
		case "**":
			// Prepend the path for this whole subtree to the remaining components
			r.remaining = path.Concat(string(remaining))
		case "*":
			// Prepend the first component of the path for this subtree to the remaining components
			component, _ := path.PopNextComponent()
			r.remaining = policyBranchPath(component).Concat(string(remaining))
		default:
			panic("not reached")
		}

		// rerun
		return r.selectBranch(branches)
	}

	// We have a branch selector
	r.remaining = remaining
	selected, err := branches.selectBranch(next)
	if err != nil {
		return 0, "", err
	}

	name := string(branches[selected].Name)
	if len(name) == 0 {
		name = next
	}

	return selected, name, nil
}

func (r *policyExecuteRunner) run(elements policyElements) error {
	for len(elements) > 0 {
		element := elements[0].runner()
		elements = elements[1:]
		if err := element.run(r); err != nil {
			return makePolicyError(err, r.currentPath, element.name())
		}
	}

	return nil
}

// PolicySessionUsage describes how a policy session will be used, and assists with
// automatically selecting branches where a policy has command context-specific branches.
type PolicySessionUsage struct {
	commandCode tpm2.CommandCode
	handles     []NamedHandle
	params      []interface{}
	authIndex   uint8
	noAuthValue bool
}

// NewPolicySessionUsage creates a new PolicySessionUsage. The returned usage
// will assume that the session is being used for authorization of the first
// handle, which is true in the vast majority of cases. If the session is being
// used for authorization of another handle, use [WithAuthIndex].
func NewPolicySessionUsage(command tpm2.CommandCode, handles []NamedHandle, params ...interface{}) *PolicySessionUsage {
	if len(handles) == 0 || len(handles) > 3 {
		panic("invalid number of handles")
	}
	return &PolicySessionUsage{
		commandCode: command,
		handles:     handles,
		params:      params,
	}
}

// WithAuthIndex indicates that the policy session is being used for authorization
// of the handle at the specified index (zero indexed). This is zero for most commands,
// where most commands only have a single handle that requires authorization. There are
// a few commands that require authorization for 2 handles: TPM2_ActivateCredential,
// TPM2_EventSequenceComplete, TPM2_Certify, TPM2_GetSessionAuditDigest,
// TPM2_GetCommandAuditDigest, TPM2_GetTime, TPM2_CertifyX509, TPM2_NV_UndefineSpaceSpecial,
// TPM2_NV_Certify, and TPM2_AC_Send.
func (u *PolicySessionUsage) WithAuthIndex(index uint8) *PolicySessionUsage {
	if int(index) >= len(u.handles) {
		panic("invalid index")
	}
	u.authIndex = index
	return u
}

// WithoutAuthValue indicates that the policy session is being used to authorize a
// resource that the authorization value cannot be determined for.
func (u *PolicySessionUsage) WithoutAuthValue() *PolicySessionUsage {
	u.noAuthValue = true
	return u
}

// CommandCode returns the command code for this usage.
func (u PolicySessionUsage) CommandCode() tpm2.CommandCode {
	return u.commandCode
}

// CpHash returns the command parameter hash for this usage for the specified session
// algorithm.
func (u PolicySessionUsage) CpHash(alg tpm2.HashAlgorithmId) (tpm2.Digest, error) {
	var handleNames []Named
	for _, handle := range u.handles {
		handleNames = append(handleNames, handle)
	}
	return ComputeCpHash(alg, u.commandCode, handleNames, u.params...)
}

// NameHash returns the name hash for this usage for the specified session algorithm.
func (u PolicySessionUsage) NameHash(alg tpm2.HashAlgorithmId) (tpm2.Digest, error) {
	var handleNames []Named
	for _, handle := range u.handles {
		handleNames = append(handleNames, handle)
	}
	return ComputeNameHash(alg, handleNames...)
}

// AllowAuthValue indicates whether this usage permits use of the auth value for the
// resource being authorized.
func (u PolicySessionUsage) AllowAuthValue() bool {
	return !u.noAuthValue
}

// AuthHandle returns the handle for the resource being authorized.
func (u PolicySessionUsage) AuthHandle() NamedHandle {
	return u.handles[u.authIndex]
}

// PolicyAuthorizationID contains an identifier for a TPM2_PolicySecret,
// TPM2_PolicySigned or TPM2_PolicyAuthorize assertion.
type PolicyAuthorizationID = PolicyAuthorizationDetails

// PolicyExecuteParams contains parameters that are useful for executing a policy.
type PolicyExecuteParams struct {
	// Tickets supplies tickets for TPM2_PolicySecret and TPM2_PolicySigned assertions.
	// These are also passed to sub-policies.
	Tickets []*PolicyTicket

	// Usage describes how the executed policy will be used, and assists with
	// automatically selecting branches where a policy has command context-specific
	// branches.
	Usage *PolicySessionUsage

	// Path provides a way to explicitly select branches or authorized policies to
	// execute. A path consists of zero or more components separated by a '/'
	// character, with each component identifying a branch to select when a branch
	// node is encountered (or a policy to select when an authorized policy is
	// required) during execution. When a branch node or authorized policy is
	// encountered, the selected sub-branch or policy is executed before resuming
	// execution in the original branch.
	//
	// When selecting a branch, a component can either identify a branch by its
	// name (if it has one), or it can be a numeric identifier of the form "{n}"
	// which selects the branch at index n.
	//
	// When selecting an authorized policy, a component identifies the policy by
	// specifying the digest of the policy for the current session algorithm.
	//
	// If a component is "**", then Policy.Execute will attempt to automatically
	// select an execution path for the entire sub-tree associated with the current
	// branch node or authorized policy. This includes choosing additional
	// branches and authorized policies encountered during the execution of the
	// selected sub-tree. Remaining path components will be consumed when resuming
	// execution in the original branch
	//
	// If a component is "*", then Policy.Execute will attempt to automatically
	// select an immediate sub-branch or authorized policy, but additional branches
	// and authorized policies encountered during the execution of the selected
	// sub-tree will consume additional path components.
	//
	// If the path has insufficent components for the branch nodes or authorized policies
	// encountered in a policy, Policy.Execute will attempt to select an appropriate
	// execution path for the remainder of the policy automatically.
	Path string

	// IgnoreAuthorizations can be used to indicate that branches containing TPM2_PolicySigned,
	// TPM2_PolicySecret or TPM2_PolicyAuthorize assertions matching the specified ID should
	// be ignored. This can be used where these assertions have failed on previous runs.
	// This propagates to sub-policies.
	IgnoreAuthorizations []PolicyAuthorizationID

	// IgnoreNV can be used to indicate that branches containing TPM2_PolicyNV assertions
	// with an NV index matching the specified name should be ignored. This can be used where
	// these assertions have failed due to an authorization issue on previous runs. This
	// propagates to sub-policies.
	IgnoreNV []Named
}

// PolicyExecuteResult is returned from [Policy.Execute].
type PolicyExecuteResult struct {
	// NewTickets contains tickets that were created as a result of executing this policy.
	NewTickets []*PolicyTicket

	// InvalidTickets contains those tickets originally supplied to [Policy.Execute] that
	// were used but found to be invalid. These tickets shouldn't be supplied to
	// [Policy.Execute] again.
	InvalidTickets []*PolicyTicket

	// AuthValueNeeded indicates that the policy executed the TPM2_PolicyAuthValue or
	// TPM2_PolicyPassword assertion.
	AuthValueNeeded bool

	// Path indicates the executed path.
	Path string

	policyCommandCode *tpm2.CommandCode
	policyCpHash      tpm2.Digest
	policyNameHash    tpm2.Digest
	policyNvWritten   *bool
}

// CommandCode returns the command code if a TPM2_PolicyCommandCode or
// TPM2_PolicyDuplicationSelect assertion was executed.
func (r *PolicyExecuteResult) CommandCode() (code tpm2.CommandCode, set bool) {
	if r.policyCommandCode == nil {
		return 0, false
	}
	return *r.policyCommandCode, true
}

// CpHash returns the command parameter hash if a TPM2_PolicyCpHash assertion
// was executed or a TPM2_PolicySecret or TPM2_PolicySigned assertion was executed
// with a cpHash.
func (r *PolicyExecuteResult) CpHash() (cpHashA tpm2.Digest, set bool) {
	if len(r.policyCpHash) == 0 {
		return nil, false
	}
	return r.policyCpHash, true
}

// NameHash returns the name hash if a TPM2_PolicyNameHash or TPM2_PolicyDuplicationSelect
// assertion was executed.
func (r *PolicyExecuteResult) NameHash() (nameHash tpm2.Digest, set bool) {
	if len(r.policyNameHash) == 0 {
		return nil, false
	}
	return r.policyNameHash, true
}

// NvWritten returns the nvWrittenSet value if a TPM2_PolicyNvWritten assertion
// was executed.
func (r *PolicyExecuteResult) NvWritten() (nvWrittenSet bool, set bool) {
	if r.policyNvWritten == nil {
		return false, false
	}
	return *r.policyNvWritten, true
}

// Execute runs this policy using the supplied policy session.
//
// The caller may supply additional parameters via the PolicyExecuteParams struct, which is an
// optional argument.
//
// Resources required by a policy are obtained from the supplied PolicyResources, which is
// optional but must be supplied for any policy that executes TPM2_PolicyNV, TPM2_PolicySecret,
// TPM2_PolicySigned or TPM2_PolicyAuthorize assertions.
//
// Some assertions need to make use of other TPM functions. Access to these is provided via
// the TPMHelper argument. This is optional, but must be supplied for any policy that executes
// TPM2_PolicyNV, TPM2_PolicySecret, TPM2_PolicySigned, or TPM2_PolicyAuthorize assertions, or
// any policy that contains branches with TPM2_PolicyPCR or TPM2_PolicyCounterTimer assertions
// where branches aren't selected explicitly.
//
// TPM2_PolicyNV assertions will create a session for authorizing the associated NV index. The
// auth type is determined automatically from the NV index attributes, but where both HMAC and
// policy auth is supported, policy auth is used.
//
// TPM2_PolicySecret assertions will create a session for authorizing the associated resource.
// The auth type is determined automatically based on the public attributes for NV indices and
// ordinary objects, but where both HMAC and policy auth is supported, HMAC auth is used. If the
// resource is a permanent resource, then only HMAC auth is used.
//
// The caller may explicitly select branches and authorized policies to execute via the Path
// argument of [PolicyExecuteParams]. Alternatively, if a path is not specified explicitly,
// or a component contains a wildcard match, an appropriate execution path is selected
// automatically where possible. This works by selecting the first suitable path, with a
// preference for paths that don't include TPM2_PolicySecret, TPM2_PolicySigned,
// TPM2_PolicyAuthValue, and TPM2_PolicyPassword assertions. It also has a preference for paths
// that don't include TPM2_PolicyNV assertions that require authorization to use or read, and for
// paths without TPM2_PolicyCommandCode, TPM2_PolicyCpHash, TPM2_PolicyNameHash and
// TPM2_PolicyDuplicatiionSelect assertions where no [PolicySessionUsage] is supplied. A path
// is omitted from the set of suitable paths if any of the following conditions are true:
//   - It contains a command code, command parameter hash, or name hash that doesn't match
//     the supplied [PolicySessionUsage].
//   - It contains a TPM2_PolicyAuthValue or TPM2_PolicyPassword assertion and this isn't permitted
//     by the supplied [PolicySessionUsage].
//   - It uses TPM2_PolicyNvWritten with a value that doesn't match the public area of the NV index
//     that the session will be used to authorize, provided via the supplied [PolicySessionUsage].
//   - It uses TPM2_PolicySigned, TPM2_PolicySecret or TPM2_PolicyAuthorize and the specific
//     authorization is included in the IgnoreAuthorizations field of [PolicyExecuteParams].
//   - It uses TPM2_PolicyNV and the NV index is included in the IgnoreNV field of
//     [PolicyExecuteParams]
//   - It uses TPM2_PolicyNV with conditions that will fail against the current NV index contents,
//     if the index has an authorization policy that permits the use of TPM2_NV_Read without any
//     other conditions, else the condition isn't checked.
//   - It uses TPM2_PolicyPCR with values that don't match the current PCR values.
//   - It uses TPM2_PolicyCounterTimer with conditions that will fail.
//
// Note that this automatic selection makes the following assumptions:
//   - TPM2_PolicySecret assertions always succeed. Where they are known to not succeed because
//     the authorization value isn't known or the resource can't be loaded, add the assertion
//     details to the IgnoreAuthorizations field of [PolicyExecuteParams].
//   - TPM2_PolicySigned assertions always succeed. Where they are known to not succeed because
//     an assertion can't be provided or it is invalid, add the assertion details to the
//     IgnoreAuthorizations field of [PolicyExecuteParams].
//   - TPM2_PolicyAuthorize assertions always succeed if policies are returned from the
//     implementation of [PolicyResourceLoader.LoadAuthorizedPolicies]. Where these are known
//     to not succeed, add the assertion details to the IgnoreAuthorizations field of
//     [PolicyExecuteParams].
//   - TPM2_PolicyNV assertions on NV indexes that require authorization to read will always
//     succeed. Where these are known to not suceed, add the assertion details to the IgnoreNV
//     field of [PolicyExecuteParams].
//
// On success, the supplied policy session may be used for authorization in a context that requires
// that this policy is satisfied. Information about the result of executing the session is also
// returned.
func (p *Policy) Execute(session PolicySession, resources PolicyResources, tpm TPMHelper, params *PolicyExecuteParams) (result *PolicyExecuteResult, err error) {
	if session == nil {
		return nil, errors.New("no session")
	}
	if resources == nil {
		resources = new(nullPolicyResources)
	}
	if tpm == nil {
		tpm = new(nullTpmHelper)
	}
	if params == nil {
		params = new(PolicyExecuteParams)
	}

	tickets, err := newExecutePolicyTickets(session.HashAlg(), params.Tickets, params.Usage)
	if err != nil {
		return nil, err
	}

	var details PolicyBranchDetails
	runner := newPolicyExecuteRunner(
		session,
		tickets,
		newExecutePolicyResources(session.Context(), resources, tickets, params.IgnoreAuthorizations, params.IgnoreNV),
		resources,
		tpm,
		params,
		&details,
	)
	if err := runner.run(p.policy.Policy); err != nil {
		return nil, err
	}

	result = &PolicyExecuteResult{
		AuthValueNeeded: details.AuthValueNeeded,
		Path:            string(runner.currentPath),
	}
	if commandCode, set := details.CommandCode(); set {
		result.policyCommandCode = &commandCode
	}
	if cpHash, set := details.CpHash(); set {
		result.policyCpHash = cpHash
	}
	if nameHash, set := details.NameHash(); set {
		result.policyNameHash = nameHash
	}
	if nvWritten, set := details.NvWritten(); set {
		result.policyNvWritten = &nvWritten
	}

	for ticket := range tickets.newTickets {
		result.NewTickets = append(result.NewTickets, ticket)
	}
	for ticket := range tickets.invalidTickets {
		result.InvalidTickets = append(result.InvalidTickets, ticket)
	}

	return result, nil
}

type nullTickets struct{}

func (*nullTickets) ticket(authName tpm2.Name, policyRef tpm2.Nonce) *PolicyTicket {
	return nil
}

func (*nullTickets) addTicket(ticket *PolicyTicket)     {}
func (*nullTickets) invalidTicket(ticket *PolicyTicket) {}

type policyComputeRunner struct {
	policySession   *computePolicySession
	policyTickets   nullTickets
	policyResources mockPolicyResources

	currentPath policyBranchPath
}

func newPolicyComputeRunner(alg tpm2.HashAlgorithmId) *policyComputeRunner {
	return &policyComputeRunner{
		policySession: newComputePolicySession(alg, nil, true),
	}
}

func (r *policyComputeRunner) session() policySession {
	return r.policySession
}

func (r *policyComputeRunner) tickets() policyTickets {
	return &r.policyTickets
}

func (r *policyComputeRunner) resources() policyResources {
	return &r.policyResources
}

func (r *policyComputeRunner) authResourceName() tpm2.Name {
	return nil
}

func (r *policyComputeRunner) loadExternal(public *tpm2.Public) (ResourceContext, error) {
	// the handle is not relevant here
	resource := tpm2.NewResourceContext(0x80000000, public.Name())
	return newResourceContext(resource, nil), nil
}

func (r *policyComputeRunner) authorize(auth ResourceContext, askForPolicy bool, usage *PolicySessionUsage, prefer tpm2.SessionType) (session SessionContext, err error) {
	return new(mockSessionContext), nil
}

func (r *policyComputeRunner) runBranch(branches policyBranches) (selected int, err error) {
	currentDigest, err := r.session().PolicyGetDigest()
	if err != nil {
		return 0, err
	}

	for i, branch := range branches {
		name := string(branch.Name)
		if len(name) == 0 {
			name = fmt.Sprintf("{%d}", i)
		}

		computedDigest, err := func() (tpm2.Digest, error) {
			origPolicySession := r.policySession
			origPath := r.currentPath
			r.policySession = newComputePolicySession(r.session().HashAlg(), currentDigest, true)
			r.currentPath = r.currentPath.Concat(name)
			defer func() {
				r.policySession = origPolicySession
				r.currentPath = origPath
			}()

			if err := r.run(branch.Policy); err != nil {
				return nil, err
			}

			return r.session().PolicyGetDigest()
		}()
		if err != nil {
			return 0, err
		}

		added := false
		for j, digest := range branch.PolicyDigests {
			if digest.HashAlg != r.session().HashAlg() {
				continue
			}

			branch.PolicyDigests[j] = taggedHash{HashAlg: r.session().HashAlg(), Digest: computedDigest}
			added = true
			break
		}
		if !added {
			branch.PolicyDigests = append(branch.PolicyDigests, taggedHash{HashAlg: r.session().HashAlg(), Digest: computedDigest})
		}
	}

	r.currentPath = r.currentPath.Concat("**")
	return -1, nil
}

func (r *policyComputeRunner) runAuthorizedPolicy(keySign *tpm2.Public, policyRef tpm2.Nonce, policies []*authorizedPolicy) (approvedPolicy tpm2.Digest, checkTicket *tpm2.TkVerified, err error) {
	return nil, nil, nil
}

func (r *policyComputeRunner) notifyPolicyPCRDigest() error {
	return fmt.Errorf("cannot compute digest for policies with TPM2_PolicyPCR assertions which contain pre-computed digests")
}

func (r *policyComputeRunner) run(elements policyElements) error {
	for len(elements) > 0 {
		element := elements[0].runner()
		elements = elements[1:]
		if err := element.run(r); err != nil {
			return makePolicyError(err, r.currentPath, element.name())
		}
	}

	return nil
}

// AddDigest computes and adds an additional digest to this policy for the specified
// algorithm. The policy should be persisted after calling this if it is going to be
// used for a resource wth the specified algorithm. On success, it returns the computed
// digest.
//
// This will fail for policies that contain TPM2_PolicyCpHash or TPM2_PolicyNameHash
// assertions, These can only be computed for a single digest algorithm, because they
// are bound to a name.
func (p *Policy) AddDigest(alg tpm2.HashAlgorithmId) (tpm2.Digest, error) {
	if !alg.Available() {
		return nil, errors.New("unavailable algorithm")
	}

	var policy *policy
	if err := mu.CopyValue(&policy, p.policy); err != nil {
		return nil, fmt.Errorf("cannot make temporary copy of policy: %w", err)
	}

	runner := newPolicyComputeRunner(alg)
	if err := runner.run(policy.Policy); err != nil {
		return nil, err
	}

	computedDigest, err := runner.session().PolicyGetDigest()
	if err != nil {
		return nil, err
	}

	addedDigest := false
	for i, d := range policy.PolicyDigests {
		if d.HashAlg == alg {
			policy.PolicyDigests[i] = taggedHash{HashAlg: alg, Digest: computedDigest}
			addedDigest = true
			break
		}
	}
	if !addedDigest {
		policy.PolicyDigests = append(policy.PolicyDigests, taggedHash{HashAlg: alg, Digest: computedDigest})
	}

	p.policy = *policy

	return computedDigest, nil
}

// Digest returns the digest for this policy for the specified algorithm, if it
// has been computed. If it hasn't been computed, ErrMissingDigest is returned.
func (p *Policy) Digest(alg tpm2.HashAlgorithmId) (tpm2.Digest, error) {
	if !alg.IsValid() {
		return nil, errors.New("invalid algorithm")
	}

	for _, digest := range p.policy.PolicyDigests {
		if digest.HashAlg == alg {
			return digest.Digest, nil
		}
	}

	return nil, ErrMissingDigest
}

// Authorize signs this policy with the supplied signer so that it can be used as an
// authorized policy for a TPM2_PolicyAuthorize assertion with the supplied authKey and
// policyRef. Calling this updates the policy, so it should be persisted afterwards.
// This signs every digest that the policy has been computed for.
//
// TPM2_PolicyAuthorize expects the digest algorithm of the signature to match the name
// algorithm of the public key, so the name algorithm of authKey must match the algorithm
// supplied through the opts argument.
//
// This expects the policy to contain a digest for the selected algorithm already.
func (p *Policy) Authorize(rand io.Reader, authKey *tpm2.Public, policyRef tpm2.Nonce, signer crypto.Signer, opts crypto.SignerOpts) (err error) {
	authName := authKey.Name()
	authAlg := authName.Algorithm()
	if opts.HashFunc() != authAlg.GetHash() {
		return errors.New("mismatched authKey name and opts")
	}
	if !authAlg.Available() {
		return errors.New("auth algorithm is unavailable")
	}

	var authorizations policyAuthorizations
	for _, auth := range p.policy.PolicyAuthorizations {
		if bytes.Equal(auth.AuthKey.Name(), authName) && bytes.Equal(auth.PolicyRef, policyRef) {
			continue
		}
		authorizations = append(authorizations, auth)
	}

	for _, approvedPolicy := range p.policy.PolicyDigests {
		auth, err := SignPolicyAuthorization(rand, approvedPolicy.Digest, authKey, policyRef, signer, opts)
		if err != nil {
			return fmt.Errorf("cannot sign authorization for digest %v: %w", approvedPolicy.HashAlg, err)
		}
		authorizations = append(authorizations, *auth)
	}

	p.policy.PolicyAuthorizations = authorizations
	return nil
}

type policyValidateRunner struct {
	policySession   *computePolicySession
	policyTickets   nullTickets
	policyResources mockPolicyResources

	currentPath policyBranchPath
}

func newPolicyValidateRunner(alg tpm2.HashAlgorithmId) *policyValidateRunner {
	return &policyValidateRunner{
		policySession: newComputePolicySession(alg, nil, false),
	}
}

func (r *policyValidateRunner) session() policySession {
	return r.policySession
}

func (r *policyValidateRunner) tickets() policyTickets {
	return &r.policyTickets
}

func (r *policyValidateRunner) resources() policyResources {
	return &r.policyResources
}

func (r *policyValidateRunner) authResourceName() tpm2.Name {
	return nil
}

func (r *policyValidateRunner) loadExternal(public *tpm2.Public) (ResourceContext, error) {
	// the handle is not relevant here
	resource := tpm2.NewResourceContext(0x80000000, public.Name())
	return newResourceContext(resource, nil), nil
}

func (r *policyValidateRunner) authorize(auth ResourceContext, askForPolicy bool, usage *PolicySessionUsage, prefer tpm2.SessionType) (session SessionContext, err error) {
	return new(mockSessionContext), nil
}

func (r *policyValidateRunner) runBranch(branches policyBranches) (selected int, err error) {
	currentDigest, err := r.session().PolicyGetDigest()
	if err != nil {
		return 0, err
	}

	for i, branch := range branches {
		name := string(branch.Name)
		if len(name) == 0 {
			name = fmt.Sprintf("{%d}", i)
		}

		computedDigest, err := func() (tpm2.Digest, error) {
			origPolicySession := r.policySession
			origPath := r.currentPath
			r.policySession = newComputePolicySession(r.session().HashAlg(), currentDigest, false)
			r.currentPath = r.currentPath.Concat(name)
			defer func() {
				r.policySession = origPolicySession
				r.currentPath = origPath
			}()

			if err := r.run(branch.Policy); err != nil {
				return nil, err
			}

			return r.session().PolicyGetDigest()
		}()
		if err != nil {
			return 0, err
		}

		for _, digest := range branch.PolicyDigests {
			if digest.HashAlg != r.session().HashAlg() {
				continue
			}

			if !bytes.Equal(digest.Digest, computedDigest) {
				return 0, fmt.Errorf("stored and computed branch digest mismatch for branch %d (computed: %x, stored: %x)", i, computedDigest, digest.Digest)
			}
			break
		}
	}

	r.currentPath = r.currentPath.Concat("**")
	return -1, nil
}

func (r *policyValidateRunner) runAuthorizedPolicy(keySign *tpm2.Public, policyRef tpm2.Nonce, policies []*authorizedPolicy) (approvedPolicy tpm2.Digest, checkTicket *tpm2.TkVerified, err error) {
	return nil, nil, nil
}

func (r *policyValidateRunner) notifyPolicyPCRDigest() error {
	return nil
}

func (r *policyValidateRunner) run(elements policyElements) error {
	for len(elements) > 0 {
		element := elements[0].runner()
		elements = elements[1:]
		if err := element.run(r); err != nil {
			return makePolicyError(err, r.currentPath, element.name())
		}
	}

	return nil
}

// Validate performs some checking of every element in the policy, and
// verifies that every branch is consistent with their stored digests. On
// success, it returns the digest correpsonding to this policy for the
// specified digest algorithm.
func (p *Policy) Validate(alg tpm2.HashAlgorithmId) (tpm2.Digest, error) {
	if !alg.Available() {
		return nil, errors.New("unavailable algorithm")
	}

	expectedDigest, err := p.Digest(alg)
	if err != nil {
		return nil, err
	}

	runner := newPolicyValidateRunner(alg)
	if err := runner.run(p.policy.Policy); err != nil {
		return nil, err
	}

	computedDigest, err := runner.session().PolicyGetDigest()
	if err != nil {
		return nil, err
	}

	if !bytes.Equal(computedDigest, expectedDigest) {
		return nil, fmt.Errorf("stored and computed policy digest mismatch (computed: %x, stored: %x)", computedDigest, expectedDigest)
	}

	for _, auth := range p.policy.PolicyAuthorizations {
		if auth.AuthKey.Name().Algorithm() != alg {
			continue
		}

		ok, err := auth.Verify(computedDigest)
		if err != nil {
			return nil, &PolicyAuthorizationError{AuthName: auth.AuthKey.Name(), PolicyRef: auth.PolicyRef, err: fmt.Errorf("cannot verify signature: %w", err)}
		}
		if !ok {
			return nil, &PolicyAuthorizationError{AuthName: auth.AuthKey.Name(), PolicyRef: auth.PolicyRef, err: errors.New("invalid signature")}
		}
	}

	return expectedDigest, nil
}

// Branches returns the path of every branch in this policy.
//
// If the authorizedPolicies argument is supplied, associated authorized policies will be
// merged into the result, otherwise missing authorized policies will be represented
// by a path component of the form "<authorize:key:%#x,ref:%#x>". The supplied algorithm
// is only really required for policies that make use of authorized policies, and is used
// to select the algorithm for encoding the path component for an authorized policy, which
// is the policy digest. Setting this to [tpm2.HashAlgorithmNull] selects the first digest
// algorithm that this policy is computed for.
func (p *Policy) Branches(alg tpm2.HashAlgorithmId, authorizedPolicies PolicyAuthorizedPolicies) ([]string, error) {
	if alg == tpm2.HashAlgorithmNull {
		if len(p.policy.PolicyDigests) == 0 {
			return nil, ErrMissingDigest
		}
		alg = p.policy.PolicyDigests[0].HashAlg
	}

	var result []string

	var makeBeginBranchFn func(policyBranchPath) treeWalkerBeginBranchFn
	makeBeginBranchFn = func(parentPath policyBranchPath) treeWalkerBeginBranchFn {
		// This function is called when starting a new branch node. It is called with information
		// about the parent branch
		return func(name string) (policySession, treeWalkerBeginBranchNodeFn, treeWalkerCompleteFullPathFn, error) {
			// This function is called at the start of a new branch. It inherits the path of the parent branch
			// (parentPath).
			branchPath := parentPath.Concat(name) // Create the new path of this branch

			// Create a new session for this branch
			session := newNullPolicySession(alg)

			// Create a new function for entering a new branch node from this branch
			beginBranchNodeFn := func() (treeWalkerBeginBranchFn, error) {
				return makeBeginBranchFn(branchPath), nil
			}

			// Create a new function that signals the end of a complete path (ie, no more elements)
			completeFullPathFn := func() error {
				result = append(result, string(branchPath))
				return nil
			}

			return session, beginBranchNodeFn, completeFullPathFn, nil
		}
	}

	walker := newTreeWalker(newMockPolicyResources(authorizedPolicies), makeBeginBranchFn(""))
	if err := walker.run(p.policy.Policy); err != nil {
		return nil, err
	}

	return result, nil
}

// PolicyNVDetails contains the properties of a TPM2_PolicyNV assertion.
type PolicyNVDetails struct {
	Auth      tpm2.Handle
	Index     tpm2.Handle
	Name      tpm2.Name
	OperandB  tpm2.Operand
	Offset    uint16
	Operation tpm2.ArithmeticOp
}

// PolicyAuthorizationDetails contains the properties of a TPM2_PolicySecret,
// TPM2_PolicySigned or TPM2_PolicyAuthorize assertion.
type PolicyAuthorizationDetails struct {
	AuthName  tpm2.Name
	PolicyRef tpm2.Nonce
}

// PolicyCounterTimerDetails contains the properties of a TPM2_PolicyCounterTimer
// assertion.
type PolicyCounterTimerDetails struct {
	OperandB  tpm2.Operand
	Offset    uint16
	Operation tpm2.ArithmeticOp
}

// PolicyPCRDetails contains the properties of a TPM2_PolicyPCR assertion.
type PolicyPCRDetails struct {
	PCRDigest tpm2.Digest
	PCRs      tpm2.PCRSelectionList
}

// PolicyBranchDetails contains the properties of a single policy branch.
type PolicyBranchDetails struct {
	NV                []PolicyNVDetails            // TPM2_PolicyNV assertions
	Secret            []PolicyAuthorizationDetails // TPM2_PolicySecret assertions
	Signed            []PolicyAuthorizationDetails // TPM2_PolicySigned assertions
	Authorize         []PolicyAuthorizationDetails // TPM2_PolicyAuthorize assertions
	AuthValueNeeded   bool                         // The branch contains a TPM2_PolicyAuthValue or TPM2_PolicyPassword assertion
	policyCommandCode tpm2.CommandCodeList
	CounterTimer      []PolicyCounterTimerDetails // TPM2_PolicyCounterTimer assertions
	policyCpHash      tpm2.DigestList
	policyNameHash    tpm2.DigestList
	PCR               []PolicyPCRDetails // TPM2_PolicyPCR assertions
	policyNvWritten   []bool
}

// IsValid indicates whether the corresponding policy branch is valid.
func (r *PolicyBranchDetails) IsValid() bool {
	if len(r.policyCommandCode) > 1 {
		for _, code := range r.policyCommandCode[1:] {
			if code != r.policyCommandCode[0] {
				return false
			}
		}
	}

	cpHashNum := 0
	if len(r.policyCpHash) > 0 {
		if len(r.policyCpHash) > 1 {
			for _, cpHash := range r.policyCpHash[1:] {
				if !bytes.Equal(cpHash, r.policyCpHash[0]) {
					return false
				}
			}
		}
		cpHashNum += 1
	}
	if len(r.policyNameHash) > 0 {
		if len(r.policyNameHash) > 1 {
			return false
		}
		cpHashNum += 1
	}
	if cpHashNum > 1 {
		return false
	}
	if len(r.policyNvWritten) > 1 {
		for _, nvWritten := range r.policyNvWritten[1:] {
			if nvWritten != r.policyNvWritten[0] {
				return false
			}
		}
	}

	return true
}

// The command code associated with a branch if set, either set by the TPM2_PolicyCommandCode
// or TPM2_PolicyDuplicationSelect assertion.
func (r *PolicyBranchDetails) CommandCode() (code tpm2.CommandCode, set bool) {
	if len(r.policyCommandCode) == 0 {
		return 0, false
	}
	return r.policyCommandCode[0], true
}

// The cpHash associated with a branch if set, either set by the TPM2_PolicyCpHash,
// TPM2_PolicySecret, or TPM2_PolicySigned assertions.
func (r *PolicyBranchDetails) CpHash() (cpHashA tpm2.Digest, set bool) {
	if len(r.policyCpHash) == 0 {
		return nil, false
	}
	return r.policyCpHash[0], true
}

// The nameHash associated with a branch if set, either set by the TPM2_PolicyNameHash
// or TPM2_PolicyDuplicationSelect assertion.
func (r *PolicyBranchDetails) NameHash() (nameHash tpm2.Digest, set bool) {
	if len(r.policyNameHash) == 0 {
		return nil, false
	}
	return r.policyNameHash[0], true
}

// The nvWrittenSet value associated with a branch if set.
func (r *PolicyBranchDetails) NvWritten() (nvWrittenSet bool, set bool) {
	if len(r.policyNvWritten) == 0 {
		return false, false
	}
	return r.policyNvWritten[0], true
}

// Details returns details of all branches with the supplied path prefix, for
// the specified algorithm. If the specified algorithm is [tpm2.HashAlgorithmNull],
// then the first algorithm the policy is computed for is used.
//
// If the authorizedPolicies argument is supplied, details of branches from associated
// authorized policies will be inserted into the result.
func (p *Policy) Details(alg tpm2.HashAlgorithmId, path string, authorizedPolicies PolicyAuthorizedPolicies) (map[string]PolicyBranchDetails, error) {
	if alg == tpm2.HashAlgorithmNull {
		if len(p.policy.PolicyDigests) == 0 {
			return nil, ErrMissingDigest
		}
		alg = p.policy.PolicyDigests[0].HashAlg
	}

	result := make(map[string]PolicyBranchDetails)

	var makeBeginBranchFn func(policyBranchPath, policyBranchPath, string, bool, *PolicyBranchDetails) treeWalkerBeginBranchFn
	makeBeginBranchFn = func(parentPath, remaining policyBranchPath, next string, consumeGreedy bool, details *PolicyBranchDetails) treeWalkerBeginBranchFn {
		// This function is called when starting a new branch node. It is called with information
		// about the parent branch
		nodeDetails := *details
		explicitlyHandledNode := false

		return func(name string) (policySession, treeWalkerBeginBranchNodeFn, treeWalkerCompleteFullPathFn, error) {
			// This function is called at the start of a new branch. It inherits the current details
			// at the point that the node was entered (details), the path of the parent branch (parentPath),
			// the remaining path components (remaining), the next path component if there is one (next), and
			// whether we are in a greedy wildcard match (consumeGreedy).
			if explicitlyHandledNode {
				// skip - a branch at this node has already handled the next component.
				return nil, nil, nil, errTreeWalkerSkipBranch
			}
			switch {
			case len(next) == 0 || next[0] == '*':
				// ok - there is no next component specified or it's a wildcard match
			case next == name:
				// ok - the next component specified matches this branch
				explicitlyHandledNode = true
			default:
				// skip - the next component was specified, it's not a wildcard match and doesn't match this branch
				return nil, nil, nil, errTreeWalkerSkipBranch
			}

			branchPath := parentPath.Concat(name) // Create the new path of this branch
			branchDetails := nodeDetails          // Copy the node details

			session := newRecorderPolicySession(alg, &branchDetails) // Create a new session

			// Create a new function for entering a new branch node from this branch
			beginBranchNodeFn := func() (treeWalkerBeginBranchFn, error) {
				remaining := remaining
				consumeGreedy := consumeGreedy

				// Create a new branch function that will only handle the required
				// branches.

				var next string
				if consumeGreedy {
					// This node is already a greedy wildcard match, so pass "*"
					// as the next component to handle all branches
					next = "*"
				} else {
					// Pop the next branch component
					next, remaining = remaining.PopNextComponent()
					if next == "**" {
						// We're entering a greedy wildcard match. This will
						// propagate to all subbranches
						consumeGreedy = true
					}
				}
				return makeBeginBranchFn(branchPath, remaining, next, consumeGreedy, &branchDetails), nil
			}

			// Create a new function that signals the end of a complete path (ie, no more elements)
			completeFullPath := func() error {
				result[string(branchPath)] = branchDetails
				return nil
			}

			return session, beginBranchNodeFn, completeFullPath, nil
		}
	}

	walker := newTreeWalker(
		newMockPolicyResources(authorizedPolicies),
		makeBeginBranchFn("", policyBranchPath(path), "*", false, new(PolicyBranchDetails)),
	)
	if err := walker.run(p.policy.Policy); err != nil {
		return nil, err
	}

	return result, nil
}

type policyStringifierRunner struct {
	w io.Writer

	policySession   policySession
	policyTickets   nullTickets
	policyResources *mockPolicyResources

	depth int

	currentPath policyBranchPath
}

func newPolicyStringifierRunner(alg tpm2.HashAlgorithmId, authorizedPolicies PolicyAuthorizedPolicies, w io.Writer) *policyStringifierRunner {
	return &policyStringifierRunner{
		w:               w,
		policySession:   newStringifierPolicySession(alg, w, 0),
		policyResources: newMockPolicyResources(authorizedPolicies),
	}
}

func (r *policyStringifierRunner) session() policySession {
	return r.policySession
}

func (r *policyStringifierRunner) tickets() policyTickets {
	return &r.policyTickets
}

func (r *policyStringifierRunner) resources() policyResources {
	return r.policyResources
}

func (r *policyStringifierRunner) authResourceName() tpm2.Name {
	return nil
}

func (r *policyStringifierRunner) loadExternal(public *tpm2.Public) (ResourceContext, error) {
	// the handle is not relevant here
	resource := tpm2.NewResourceContext(0x80000000, public.Name())
	return newResourceContext(resource, nil), nil
}

func (r *policyStringifierRunner) authorize(auth ResourceContext, askForPolicy bool, usage *PolicySessionUsage, prefer tpm2.SessionType) (session SessionContext, err error) {
	return new(mockSessionContext), nil
}

func (r *policyStringifierRunner) runBranch(branches policyBranches) (selected int, err error) {
	var treeDepth int
	switch {
	case len(branches) <= 8:
		treeDepth = 1
	case len(branches) <= 64:
		treeDepth = 2
	case len(branches) <= 512:
		treeDepth = 3
	default:
		treeDepth = 4
	}

	var digests tpm2.DigestList
	for _, branch := range branches {
		var digest tpm2.Digest
		for _, d := range branch.PolicyDigests {
			if d.HashAlg != r.session().HashAlg() {
				continue
			}
			digest = d.Digest
			break
		}
		if len(digest) == 0 {
			return 0, ErrMissingDigest
		}
		digests = append(digests, digest)
	}

	tree, err := newPolicyOrTree(r.session().HashAlg(), digests)
	if err != nil {
		return 0, fmt.Errorf("cannot compute PolicyOR tree: %w", err)
	}

	fmt.Fprintf(r.w, "\n%*s BranchNode {", r.depth*3, "")

	maybeOpenSection := func(i int) {
		extraDepth := (treeDepth - 1) * 2
		if treeDepth > 3 && i%512 == 0 {
			fmt.Fprintf(r.w, "\n%*s {", (r.depth+extraDepth-5)*3, "")
			fmt.Fprintf(r.w, "\n%*s {", (r.depth+extraDepth-4)*3, "")
		}
		if treeDepth > 2 && i%64 == 0 {
			fmt.Fprintf(r.w, "\n%*s {", (r.depth+extraDepth-3)*3, "")
			fmt.Fprintf(r.w, "\n%*s {", (r.depth+extraDepth-2)*3, "")
		}
		if treeDepth > 1 && i%8 == 0 {
			fmt.Fprintf(r.w, "\n%*s {", (r.depth+extraDepth-1)*3, "")
			fmt.Fprintf(r.w, "\n%*s {", (r.depth+extraDepth)*3, "")
		}
	}
	maybeCloseSection := func(i int, finish bool) error {
		if i == 0 {
			return errors.New("invalid index")
		}

		extraDepth := (treeDepth - 1) * 2
		digests := tree.selectBranch(i - 1)
		if treeDepth > 1 && (i%8 == 0 || finish) {
			fmt.Fprintf(r.w, "\n%*s }", (r.depth+extraDepth)*3, "")
			if len(digests) > 1 {
				session := newStringifierPolicySession(r.session().HashAlg(), r.w, r.depth+extraDepth)
				if err := session.PolicyOR(digests[0]); err != nil {
					return err
				}
			}
			fmt.Fprintf(r.w, "\n%*s }", (r.depth+extraDepth-1)*3, "")
		}
		if treeDepth > 2 && (i%64 == 0 || finish) {
			fmt.Fprintf(r.w, "\n%*s }", (r.depth+extraDepth-2)*3, "")
			if len(digests) > 2 {
				session := newStringifierPolicySession(r.session().HashAlg(), r.w, r.depth+extraDepth-2)
				if err := session.PolicyOR(digests[0]); err != nil {
					return err
				}
			}
			fmt.Fprintf(r.w, "\n%*s }", (r.depth+extraDepth-3)*3, "")
		}
		if treeDepth > 4 && (i%512 == 0 || finish) {
			fmt.Fprintf(r.w, "\n%*s }", (r.depth+extraDepth-4)*3, "")
			if len(digests) > 3 {
				session := newStringifierPolicySession(r.session().HashAlg(), r.w, r.depth+extraDepth-4)
				if err := session.PolicyOR(digests[0]); err != nil {
					return err
				}
			}
			fmt.Fprintf(r.w, "\n%*s }", (r.depth+extraDepth-5)*3, "")
		}
		return nil
	}

	maybeOpenSection(0)

	for i, branch := range branches {
		if i > 0 {
			if err := maybeCloseSection(i, false); err != nil {
				return 0, fmt.Errorf("internal error: %w", err)
			}
			maybeOpenSection(i)
		}

		err := func() error {
			origSession := r.policySession
			origPath := r.currentPath
			origDepth := r.depth

			r.depth++
			r.depth += ((treeDepth - 1) * 2)
			r.policySession = newStringifierPolicySession(r.policySession.HashAlg(), r.w, r.depth)
			name := string(branch.Name)
			if len(name) == 0 {
				name = fmt.Sprintf("{%d}", i)
			}
			r.currentPath = r.currentPath.Concat(name)
			defer func() {
				r.depth = origDepth
				r.policySession = origSession
				r.currentPath = origPath
			}()

			fmt.Fprintf(r.w, "\n%*sBranch %d", r.depth*3, "", i)
			if len(branch.Name) > 0 {
				fmt.Fprintf(r.w, " (%s)", branch.Name)
			}
			fmt.Fprintf(r.w, " {")

			fmt.Fprintf(r.w, "\n%*s # digest %v:%#x", r.depth*3, "", r.policySession.HashAlg(), digests[i])

			if err := r.run(branch.Policy); err != nil {
				return err
			}

			fmt.Fprintf(r.w, "\n%*s}", r.depth*3, "")
			return nil
		}()
		if err != nil {
			return 0, err
		}
	}
	if err := maybeCloseSection(len(branches), true); err != nil {
		return 0, fmt.Errorf("internal error: %w", err)
	}
	fmt.Fprintf(r.w, "\n%*s }", r.depth*3, "")

	return -1, nil
}

func (r *policyStringifierRunner) runAuthorizedPolicy(keySign *tpm2.Public, policyRef tpm2.Nonce, policies []*authorizedPolicy) (approvedPolicy tpm2.Digest, checkTicket *tpm2.TkVerified, err error) {
	fmt.Fprintf(r.w, "\n%*s AuthorizedPolicies {", r.depth*3, "")
	for _, policy := range policies {
		err := func() error {
			origSession := r.policySession
			origPath := r.currentPath

			r.depth++
			r.policySession = newStringifierPolicySession(r.policySession.HashAlg(), r.w, r.depth)
			r.currentPath = r.currentPath.Concat(string(policy.Name))
			defer func() {
				r.depth--
				r.policySession = origSession
				r.currentPath = origPath
			}()

			var digest tpm2.Digest
			for _, d := range policy.PolicyDigests {
				if d.HashAlg != r.policySession.HashAlg() {
					continue
				}
				digest = d.Digest
				break
			}
			if len(digest) == 0 {
				return ErrMissingDigest
			}
			fmt.Fprintf(r.w, "\n%*sAuthorizedPolicy %x {", r.depth*3, "", digest)
			fmt.Fprintf(r.w, "\n%*s # digest %v:%#x", r.depth*3, "", r.policySession.HashAlg(), digest)

			if err := r.run(policy.Policy); err != nil {
				return err
			}

			fmt.Fprintf(r.w, "\n%*s}", r.depth*3, "")
			return nil
		}()
		if err != nil {
			return nil, nil, err
		}

	}
	fmt.Fprintf(r.w, "\n%*s }", r.depth*3, "")
	return nil, nil, nil
}

func (r *policyStringifierRunner) notifyPolicyPCRDigest() error {
	return nil
}

func (r *policyStringifierRunner) run(elements policyElements) error {
	for len(elements) > 0 {
		element := elements[0].runner()
		elements = elements[1:]
		if err := element.run(r); err != nil {
			return makePolicyError(err, r.currentPath, element.name())
		}
	}

	return nil
}

func (p *Policy) string(alg tpm2.HashAlgorithmId, authorizedPolicies PolicyAuthorizedPolicies) (string, error) {
	var digest tpm2.Digest
	if alg == tpm2.HashAlgorithmNull {
		if len(p.policy.PolicyDigests) > 0 {
			alg = p.policy.PolicyDigests[0].HashAlg
			digest = p.policy.PolicyDigests[0].Digest
		}
	} else {
		for _, d := range p.policy.PolicyDigests {
			if d.HashAlg != alg {
				continue
			}
			digest = d.Digest
			break
		}
	}
	if len(digest) == 0 {
		return "", ErrMissingDigest
	}

	w := new(bytes.Buffer)
	fmt.Fprintf(w, "\nPolicy {")
	fmt.Fprintf(w, "\n # digest %v:%#x", alg, digest)
	for i, auth := range p.policy.PolicyAuthorizations {
		fmt.Fprintf(w, "\n # auth %d authName:%#x, policyRef:%#x, sigAlg:%v", i, auth.AuthKey.Name(), auth.PolicyRef, auth.Signature.SigAlg)
		if auth.Signature.SigAlg.IsValid() {
			fmt.Fprintf(w, ", hashAlg:%v", auth.Signature.HashAlg())
		}
	}

	runner := newPolicyStringifierRunner(alg, authorizedPolicies, w)
	if err := runner.run(p.policy.Policy); err != nil {
		return "", err
	}

	fmt.Fprintf(w, "\n}")
	return w.String(), nil
}

func (p *Policy) String() string {
	if len(p.policy.PolicyDigests) == 0 {
		return "%!(ERROR=no computed digests)"
	}
	return p.Stringer(p.policy.PolicyDigests[0].HashAlg, nil).String()
}

type policyStringer struct {
	alg                tpm2.HashAlgorithmId
	authorizedPolicies PolicyAuthorizedPolicies
	policy             *Policy
}

// String implements fmt.Stringer. It will print a string representation of the policy
// with the first computed digest algorithm.
func (s *policyStringer) String() string {
	str, err := s.policy.string(s.alg, s.authorizedPolicies)
	if err != nil {
		return fmt.Sprintf("%%!(ERROR=%v)", err)
	}
	return str
}

// Stringer returns a fmt.Stringer that will print a string representation of the policy
// for the specified digest algorithm. The policy must already include this algorithm. If
// the algorithm is [tpm2.HashAlgorithmNull], then the first computed algorithm will be used.
// If authorizedPolicies is supplied, the string representation will include the relevant
// authorized policies as well.
func (p *Policy) Stringer(alg tpm2.HashAlgorithmId, authorizedPolicies PolicyAuthorizedPolicies) fmt.Stringer {
	return &policyStringer{
		alg:                alg,
		authorizedPolicies: authorizedPolicies,
		policy:             p,
	}
}
