package policyutil

import (
	"bytes"
	"errors"
	"fmt"

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

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 *policySessionUsageConstraints) (*executePolicyTickets, error) {
	var usageCpHash tpm2.Digest
	if usage.canCpHash() {
		var err error
		usageCpHash, err = usage.cpHash(alg)
		if err != nil {
			return nil, fmt.Errorf("cannot compute cpHash from constraints: %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 executePolicyResourcesAuthorizer interface {
	Authorize(tpm2.ResourceContext) error
}

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

	authorizer executePolicyResourcesAuthorizer
	tpm        TPMHelper

	usage                *policySessionUsageConstraints
	ignoreAuthorizations []PolicyAuthorizationID
	ignoreNV             []Named

	pathChooser *policyPathChooser

	remaining   policyBranchPath
	currentPath policyBranchPath
}

func newPolicyExecuteRunner(session PolicyExecuteSession, params *policyExecuteParams, tickets *executePolicyTickets, resources *executePolicyResources, authorizer executePolicyResourcesAuthorizer, details *PolicyBranchDetails) *policyExecuteRunner {
	return &policyExecuteRunner{
		policySessionContext: session.Context(),
		policySession: newTeePolicySession(
			session,
			newRecorderPolicySession(session.HashAlg(), details),
		),
		policyTickets:        tickets,
		policyResources:      resources,
		authorizer:           authorizer,
		tpm:                  params.tpm,
		usage:                &params.usage,
		ignoreAuthorizations: params.ignoreAuthorizations,
		ignoreNV:             params.ignoreNV,
		pathChooser:          newPolicyPathChooser(session.HashAlg(), resources, params.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, commandParams *authorizationCommandParams, 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{
			tpm: r.tpm,
			usage: policySessionUsageConstraints{
				hasCommand:  true,
				hasHandles:  true,
				hasParams:   true,
				commandCode: commandParams.commandCode,
				handles:     commandParams.handles,
				params:      commandParams.params,
			},
			ignoreAuthorizations: r.ignoreAuthorizations,
			ignoreNV:             r.ignoreNV,
		}

		var details PolicyBranchDetails
		runner := newPolicyExecuteRunner(policySession, params, r.policyTickets, r.policyResources.forSession(session), r.authorizer, &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) {
	// Pop the next supplied path component and find matching branches.
	var candidateIndices []int
	next, remaining := r.remaining.PopNextComponent()
	switch next {
	case "":
		// Choose from all branches
		for i := range branches {
			candidateIndices = append(candidateIndices, i)
		}
	default:
		// Filter branches
		var err error
		candidateIndices, err = branches.filterBranches(next)
		if err != nil {
			return 0, "", fmt.Errorf("cannot filter branches with pattern %q: %w", next, err)
		}
	}

	var selected int

	switch len(candidateIndices) {
	case 0:
		return 0, "", fmt.Errorf("no branch with name that matches pattern %q", next) // next is never empty here
	case 1:
		selected = candidateIndices[0]
		r.remaining = remaining
	default:
		// We have muliple candidate branches - try to automatically choose a path.
		var candidateBranches policyBranches
		for _, i := range candidateIndices {
			candidateBranches = append(candidateBranches, branches[i])
		}

		path, err := r.pathChooser.choose(candidateBranches)
		if err != nil {
			var patternStr string
			if next != "" {
				patternStr = fmt.Sprintf(" with pattern %q", next)
			}
			return 0, "", fmt.Errorf("cannot automatically choose path from branches%s: %w", patternStr, err)
		}

		switch next {
		case "":
			// Save the entire path chosen from this subtree.
			r.remaining = path
		case "**":
			// Special case for the greedy wildcard match. Prepend the entire path
			// chosen from this subtree to the remaining components.
			r.remaining = path.Concat(string(remaining))
		default:
			// Prepend the first component of the path chosen from this subtree
			// to the remaining components.
			component, _ := path.PopNextComponent()
			r.remaining = policyBranchPath(component).Concat(string(remaining))
		}

		// Pop the next path component again and find matching branches. This
		// shouldn't fail now.
		next, remaining := r.remaining.PopNextComponent()
		candidateIndices, err = branches.filterBranches(next)
		switch {
		case err != nil:
			return 0, "", fmt.Errorf("internal error: cannot filter branches after automatically choosing path: %w", err)
		case len(candidateIndices) != 1:
			return 0, "", errors.New("internal error: unexpected number of branches after automatically choosing path")
		default:
			selected = candidateIndices[0]
			r.remaining = remaining
		}
	}

	return selected, branches[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
}

type policySessionUsageConstraints struct {
	hasCommand bool
	hasHandles bool
	hasParams  bool

	commandCode tpm2.CommandCode
	handles     []NamedHandle
	params      []interface{}

	authIndex uint8

	noAuthValue bool
}

func (u *policySessionUsageConstraints) canCpHash() bool {
	return u.hasCommand && u.hasHandles && u.hasParams
}

func (u *policySessionUsageConstraints) 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...)
}

func (u *policySessionUsageConstraints) canNameHash() bool {
	return u.hasHandles
}

func (u *policySessionUsageConstraints) nameHash(alg tpm2.HashAlgorithmId) (tpm2.Digest, error) {
	var handleNames []Named
	for _, handle := range u.handles {
		handleNames = append(handleNames, handle)
	}
	return ComputeNameHash(alg, handleNames...)
}

func (u *policySessionUsageConstraints) authHandle() NamedHandle {
	if len(u.handles) == 0 {
		return nil
	}
	return u.handles[u.authIndex]
}

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

type policyExecuteParams struct {
	resources            PolicyExecuteResources
	tpm                  TPMHelper
	tickets              []*PolicyTicket
	usage                policySessionUsageConstraints
	path                 string
	ignoreAuthorizations []PolicyAuthorizationID
	ignoreNV             []Named
}

type PolicyExecuteOption func(*policyExecuteParams)

// WithResources allows [Policy.Execute] to obtain resources required by a policy, using
// an implementation of [PolicyExecuteResources] created by [NewPolicyExecuteResources].
// This must be supplied for any policy that executes TPM2_PolicyNV, TPM2_PolicySecret,
// TPM2_PolicySigned or TPM2_PolicyAuthorize assertions.
func WithResources(tpm *tpm2.TPMContext, options ...PolicyExecuteResourcesOption) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.resources = NewPolicyExecuteResources(tpm, options...)
	}
}

// WithPolicyExecuteResources allows [Policy.Execute] to obtain resources required by a
// policy. This must be supplied for any policy that executes TPM2_PolicyNV, TPM2_PolicySecret,
// TPM2_PolicySigned or TPM2_PolicyAuthorize assertions.
func WithPolicyExecuteResources(resources PolicyExecuteResources) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.resources = resources
	}
}

// WithTPMHelper allows [Policy.Execute] to make use of other TPM functions, using an
// implementation of [TPMHelper] created by [NewTPMHelper]. This 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.
func WithTPMHelper(tpm *tpm2.TPMContext, options ...TPMHelperOption) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.tpm = NewTPMHelper(tpm, options...)
	}
}

// WithExternalTPMHelper allows [Policy.Execute] to make use of other TPM functions. This
// 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.
func WithExternalTPMHelper(tpm TPMHelper) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.tpm = tpm
	}
}

// WithTickets supplies tickets for TPM2_PolicySecret and TPM2_PolicySigned assertions.
// These are also supplied to sub-policies.
func WithTickets(tickets []*PolicyTicket) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.tickets = tickets
	}
}

// WithSessionUsageHandlesConstraint tells [Policy.Execute] which handles will be used
// with the executed policy, and assists with automatically choosing branches where a
// policy has branches containing TPM2_PolicyNameHash, TPM2_PolicyDuplicationSelect or
// TPM2_PolicyNvWritten assertions.
//
// This clears the effect of [WithSessionUsageAuthIndex].
func WithSessionUsageHandlesContraint(handles ...NamedHandle) PolicyExecuteOption {
	if len(handles) == 0 || len(handles) > 3 {
		panic("invalid number of handles")
	}
	return func(p *policyExecuteParams) {
		p.usage = policySessionUsageConstraints{
			hasHandles:  true,
			handles:     handles,
			noAuthValue: p.usage.noAuthValue,
		}
	}
}

// WithSessionUsageHandlesConstraint tells [Policy.Execute] which command, handles and
// parameters will be used with the executed policy, and assists with automatically choosing
// branches where a policy has branches containing TPM2_PolicyCommandCode, TPM2_PolicyCpHash,
// TPM2_PolicyNameHash, TPM2_PolicyDuplicationSelect or TPM2_PolicyNvWritten assertions.
//
// This clears the effect of [WithSessionUsageAuthIndex].
func WithSessionUsageCommandConstraint(commandCode tpm2.CommandCode, handles []NamedHandle, params ...any) PolicyExecuteOption {
	if len(handles) == 0 || len(handles) > 3 {
		panic("invalid number of handles")
	}
	return func(p *policyExecuteParams) {
		p.usage = policySessionUsageConstraints{
			hasCommand:  true,
			hasHandles:  true,
			hasParams:   true,
			commandCode: commandCode,
			handles:     handles,
			params:      params,
			noAuthValue: p.usage.noAuthValue,
		}
	}
}

// WithSessionUsageNoAuthValueConstraint tells [Policy.Execute] that an authorization value
// will not be available when the executed policy is used. This ensures that branches with
// TPM2_PolicyAuthValue or TPM2_PolicyPassword assertions will not be selected.
func WithSessionUsageNoAuthValueConstraint() PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.usage.noAuthValue = true
	}
}

// WithSessionUsageAuthIndex tells [Policy.Execute] the index (zero indexed) of the handle
// that the executed policy will be used for. 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.
//
// This must be used after [WithSessionUsageHandlesConstraint] or
// [WithSessionUsageCommandConstraint].
func WithSessionUsageAuthIndex(index uint8) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		if !p.usage.hasHandles {
			panic("must use with WithSessionUsageHandlesConstraint or WithSessionUsageCommandConstraint")
		}
		if int(index) >= len(p.usage.handles) {
			panic("invalid index")
		}
		p.usage.authIndex = index
	}
}

// WithPathConstraint provides a way to explicitly select which branches or authorized
// policies [Policy.Execute] should execute, or constraining which of these can be considered
// when automatically choosing them, by specifying patterns.
//
// 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 choose 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
//
// A component can be a pattern that is compatible with filepath.Match. In the case where
// the pattern matches more than one branch, then [Policy.Execute] will attempt to
// automatically choose 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 automatically choose an
// appropriate execution path for the remainder of the policy.
func WithPathConstraint(path string) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.path = path
	}
}

// WithIgnoreAuthorizationsConstraint tells [Policy.Execute] which branches or authorized policies
// containing TPM2_PolicySigned, TPM2_PolicySecret or TPM2_PolicyAuthorize assertions that should
// not be automatically chosen. The assertions are identified by an ID. This can be used where
// these assertions have failed on previous runs. This propagates to sub-policies.
func WithIgnoreAuthorizationsConstraint(ids []PolicyAuthorizationID) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.ignoreAuthorizations = ids
	}
}

// WithIgnoreNVConstraint tells [Policy.Execute] which branches or authorized policies containing
// TPM2_PolicyNV assertions that should not be automatically chosen. The assertions are identified
// by the NV index name. This can be used where these assertions have failed due to an authorization
// issue on previous runs. This propagates to sub-policies.
func WithIgnoreNVConstraint(nv []Named) PolicyExecuteOption {
	return func(p *policyExecuteParams) {
		p.ignoreNV = nv
	}
}

// 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.
//
// 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 with the
// [WithPathConstraint] option. Alternatively, if a path is not specified explicitly, or it contains
// a pattern that matches multiple branches, an appropriate execution path is chosen 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
// session usage contraints are 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
//     those supplied via [WithSessionUsageCommandConstraint] or
//     [WithSessionUsageHandlesConstraint].
//   - It contains a TPM2_PolicyAuthValue or TPM2_PolicyPassword assertion and this isn't permitted
//     because of the [WithSessionUsageNoAuthValue] option.
//   - 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 [WithSessionUsageCommandConstraint]
//     or [WithSessionUsageHandlesConstraint]..
//   - It uses TPM2_PolicySigned, TPM2_PolicySecret or TPM2_PolicyAuthorize and the specific
//     authorization was supplied to [WithIgnoreAuthorizationsConstraint].
//   - It uses TPM2_PolicyNV and the NV index was supplied to [WithIgnoreNVConstraint].
//   - 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, supply the assertion
//     details to [WithIgnoreAuthorizationsConstraint].
//   - TPM2_PolicySigned assertions always succeed. Where they are known to not succeed because
//     an assertion can't be provided or it is invalid, supply the assertion details to
//     [WithIgnoreAuthorizationsConstraint].
//   - TPM2_PolicyAuthorize assertions always succeed if policies are returned from the
//     implementation of [PolicyExecuteResources.AuthorizedPolicies]. Where these are known
//     to not succeed, supply the assertion details to [WithIgnoreAuthorizationsConstraint].
//   - TPM2_PolicyNV assertions on NV indexes that require authorization to read will always
//     succeed. Where these are known to not suceed, supply the NV index name to
//     [WithIgnoreNVConstraint].
//
// 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 PolicyExecuteSession, options ...PolicyExecuteOption) (result *PolicyExecuteResult, err error) {
	if session == nil {
		return nil, errors.New("no session")
	}

	params := new(policyExecuteParams)
	for _, opt := range options {
		opt(params)
	}
	if params.resources == nil {
		params.resources = new(nullPolicyResources)
	}
	if params.tpm == nil {
		params.tpm = new(nullTpmHelper)
	}

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

	var details PolicyBranchDetails
	runner := newPolicyExecuteRunner(
		session,
		params,
		tickets,
		newExecutePolicyResources(session.Context(), params.resources, tickets, params.ignoreAuthorizations, params.ignoreNV),
		params.resources,
		&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
}
