package rpc

import (
	
	
	
	
	
	
	
	
	
	
	

	
	amhelp 
	am 
	
)

type ClockUpdateFunc func(now am.Time, qTick uint64, machTick uint32)

// NetMachConn is a mutation interface for NetworkMachine instances.
// It's meant to be (optionally) injected by whatever creates network machines,
// so they can communicate with the server (or another source).
type NetMachConn interface {
	Call(ctx context.Context, method ServerMethod, args any, resp any) bool
	Notify(ctx context.Context, method ServerMethod, args any) bool
}

// NetMachInternal are internal methods of a NetworkMachine instance returned
// by the constructor.
type NetMachInternal struct {
	nm *NetworkMachine
}

func ( *NetMachInternal) (
	 am.Time,  uint64,  uint32,
) {
	.nm.updateClock(, , )
}

func ( *NetMachInternal) () {
	.nm.clockMx.Lock()
}

func ( *NetMachInternal) () {
	.nm.clockMx.Unlock()
}

// NetworkMachine is a subset of `pkg/machine#Machine` for RPC. Lacks the queue
// and other local methods. Most methods are clock-based, thus executed locally.
// NetworkMachine implements [am.Api].
type NetworkMachine struct {
	// If true, the machine will print all exceptions to stdout. Default: true.
	// Requires an ExceptionHandler binding and Machine.PanicToException set.
	LogStackTrace bool

	// internal

	// RPC client parenting this NetworkMachine. If nil, the machine is read-only
	// and won't allow for mutations / network calls.
	conn NetMachConn
	// remoteId is the ID of the remote state machine.
	remoteId string

	// net machine internal

	// embed and reuse subscriptions
	subs     *am.Subscriptions
	id       string
	ctx      context.Context
	disposed atomic.Bool
	// Err is the last error that occurred on this netowrk instance.
	err    atomic.Pointer[error]
	schema am.Schema
	// external lock
	clockMx         sync.RWMutex
	schemaMx        sync.RWMutex
	machTime        am.Time
	machClock       am.Clock
	queueTick       uint64
	stateNames      am.S
	activeStates    atomic.Pointer[am.S]
	activeStatesDbg am.S
	indexStateCtx   am.IndexStateCtx
	indexWhen       am.IndexWhen
	indexWhenTime   am.IndexWhenTime
	// TODO indexWhenArgs
	// indexWhenArgs am.IndexWhenArgs
	whenDisposed   chan struct{}
	tracers        []am.Tracer
	tracersMx      sync.RWMutex
	handlers       []*handler
	handlersMx     sync.Mutex
	parentId       string
	tags           []string
	logLevel       atomic.Pointer[am.LogLevel]
	logger         atomic.Pointer[am.LoggerFn]
	logEntriesLock sync.Mutex
	logEntries     []*am.LogEntry
	// If true, logs will start with the machine's id (5 chars).
	// Default: true.
	logId     atomic.Bool
	semLogger *semLogger
	// execQueue executed handlers and tracers
	machTick        uint32
	t               atomic.Pointer[am.Transition]
	disposeHandlers []am.HandlerDispose
	filterMutations bool
}

var ssNS = states.NetSourceStates

var _ am.Api = &NetworkMachine{}

// NewNetworkMachine creates a new instance of a NetworkMachine.
func (
	 context.Context,  string,  NetMachConn,  am.Schema,
	 am.S,  *am.Machine,  []string,  bool,
) (*NetworkMachine, *NetMachInternal, error) {
	// validate
	if  == nil {
		return nil, nil, errors.New("ctx cannot be nil")
	}
	if  != nil && len() != len() {
		return nil, nil, errors.New(
			"schema and stateNames must have the same length")
	}
	if  == nil {
		return nil, nil, errors.New("parent cannot be nil")
	}
	if  == nil {
		 = []string{"rpc-worker", "src-id:"}
	}

	 := &NetworkMachine{
		LogStackTrace: true,

		conn:            ,
		id:              ,
		ctx:             .Ctx(),
		schema:          ,
		stateNames:      ,
		indexWhen:       am.IndexWhen{},
		indexStateCtx:   am.IndexStateCtx{},
		indexWhenTime:   am.IndexWhenTime{},
		whenDisposed:    make(chan struct{}),
		machTime:        make(am.Time, len()),
		machClock:       am.Clock{},
		queueTick:       1,
		parentId:        .Id(),
		tags:            ,
		filterMutations: ,
	}
	.logId.Store(true)

	// init clock
	for ,  := range  {
		.machClock[] = 0
	}
	.subs = am.NewSubscriptionManager(, .machClock,
		.is, .not, .log)
	.semLogger = &semLogger{mach: }
	 := am.LogNothing
	.logLevel.Store(&)
	.activeStates.Store(&am.S{})
	.OnDispose(func( string,  context.Context) {
		.Dispose()
	})

	// ret a priv func to update the clock of this instance
	return , &NetMachInternal{}, nil
}

// ///// RPC methods

// ///// Mutations (remote)

// Add is [am.Api.Add].
func ( *NetworkMachine) ( am.S,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}

	.MustParseStates()

	// reject early
	if .filterMutations && !.mutAccepted(am.MutationAdd, ) {
		return am.Canceled
	}

	// call rpc
	 := &MsgSrvMutation{}
	 := &MsgCliMutation{
		States: amhelp.StatesToIndexes(.StateNames(), ),
		Args:   ,
	}
	if !.conn.Call(.Ctx(), ServerAdd, , ) {
		return am.Canceled
	}

	return .Result
}

// Add1 is [am.Api.Add1].
func ( *NetworkMachine) ( string,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	return .Add(am.S{}, )
}

// AddNS is a NoSync method - an efficient way for adding states, as it
// doesn't wait for, nor transfers a response. Because of which it doesn't
// update the clock. Use Sync() to update the clock after a batch of AddNS
// calls.
func ( *NetworkMachine) ( am.S,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}

	.MustParseStates()

	// reject early
	if .filterMutations && !.mutAccepted(am.MutationAdd, ) {
		return am.Canceled
	}

	// call rpc
	 := &MsgCliMutation{
		States: amhelp.StatesToIndexes(.StateNames(), ),
		Args:   ,
	}
	if !.conn.Notify(.Ctx(), ServerAddNS, ) {
		return am.Canceled
	}

	return am.Executed
}

// Add1NS is a single state version of AddNS.
func ( *NetworkMachine) ( string,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	return .AddNS(am.S{}, )
}

// Remove is [am.Api.Remove].
func ( *NetworkMachine) ( am.S,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}

	.MustParseStates()

	// reject early
	if .filterMutations && !.mutAccepted(am.MutationRemove, ) {
		return am.Canceled
	}

	// call rpc
	 := &MsgSrvMutation{}
	 := &MsgCliMutation{
		States: amhelp.StatesToIndexes(.StateNames(), ),
		Args:   ,
	}
	if !.conn.Call(.Ctx(), ServerRemove, , ) {
		return am.Canceled
	}

	return .Result
}

// Remove1 is [am.Api.Remove1].
func ( *NetworkMachine) ( string,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	return .Remove(am.S{}, )
}

// Set is [am.Api.Set].
func ( *NetworkMachine) ( am.S,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}

	.MustParseStates()

	// reject early
	if .filterMutations && !.mutAccepted(am.MutationSet, ) {
		return am.Canceled
	}

	// call rpc
	 := &MsgSrvMutation{}
	 := &MsgCliMutation{
		States: amhelp.StatesToIndexes(.StateNames(), ),
		Args:   ,
	}
	if !.conn.Call(.Ctx(), ServerSet, , ) {
		return am.Canceled
	}

	return .Result
}

// AddErr is [am.Api.AddErr].
func ( *NetworkMachine) ( error,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	return .AddErrState(am.StateException, , )
}

// AddErrState is [am.Api.AddErrState].
func ( *NetworkMachine) (
	 string,  error,  am.A,
) am.Result {
	if .conn == nil || .disposed.Load() {
		return am.Canceled
	}

	// keep the last err locally
	.err.Store(&)

	// log a stack trace to the local log
	if .LogStackTrace {
		 := utils.CaptureStackTrace()
		.log(am.LogChanges, fmt.Sprintf("ERROR: %s\nTrace:\n%s", , ))
	}

	// build args
	 := &am.AT{Err: }

	 := am.S{, am.StateException}
	// mark errors added locally with ErrOnClient
	if .Has1(ssNS.ErrOnClient) {
		 = append(, ssNS.ErrOnClient)
	}

	return .Add(, am.PassMerge(, ))
}

// EvAdd is [am.Api.EvAdd].
func ( *NetworkMachine) (
	 *am.Event,  am.S,  am.A,
) am.Result {
	if .conn == nil {
		return am.Canceled
	}

	.MustParseStates()

	// TODO mutation filtering (req schema)

	// call rpc
	 := &MsgSrvMutation{}
	 := &MsgCliMutation{
		States: amhelp.StatesToIndexes(.StateNames(), ),
		Args:   ,
		Event:  .Export(),
	}
	if !.conn.Call(.Ctx(), ServerAdd, , ) {
		return am.Canceled
	}

	return .Result
}

// EvAdd1 is [am.Api.EvAdd1].
func ( *NetworkMachine) (
	 *am.Event,  string,  am.A,
) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	return .EvAdd(, am.S{}, )
}

// TODO EvAddNS

// EvRemove1 is [am.Api.EvRemove1].
func ( *NetworkMachine) (
	 *am.Event,  string,  am.A,
) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	return .EvRemove(, am.S{}, )
}

// EvRemove is [am.Api.EvRemove].
func ( *NetworkMachine) (
	 *am.Event,  am.S,  am.A,
) am.Result {
	if .conn == nil {
		return am.Canceled
	}

	.MustParseStates()

	// TODO mutation filtering (req schema)

	// call rpc
	 := &MsgSrvMutation{}
	 := &MsgCliMutation{
		States: amhelp.StatesToIndexes(.StateNames(), ),
		Args:   ,
		Event:  .Export(),
	}
	if !.conn.Call(.Ctx(), ServerRemove, , ) {
		return am.Canceled
	}

	return .Result
}

// EvAddErr is [am.Api.EvAddErr].
func ( *NetworkMachine) (
	 *am.Event,  error,  am.A,
) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	return .EvAddErrState(, am.StateException, , )
}

// EvAddErrState is [am.Api.EvAddErrState].
func ( *NetworkMachine) (
	 *am.Event,  string,  error,  am.A,
) am.Result {
	if .conn == nil {
		return am.Canceled
	}

	// keep the last err locally
	.err.Store(&)

	// log a stack trace to the local log
	if .LogStackTrace {
		 := utils.CaptureStackTrace()
		.log(am.LogChanges, fmt.Sprintf("ERROR: %s\nTrace:\n%s", , ))
	}

	// build args
	 := &am.AT{Err: }

	 := am.S{, am.StateException}
	// mark errors added locally with ErrOnClient
	if .Has1(ssNS.ErrOnClient) {
		 = append(, ssNS.ErrOnClient)
	}

	return .EvAdd(, , am.PassMerge(, ))
}

// Toggle is [am.Api.Toggle].
func ( *NetworkMachine) ( am.S,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	if .Is() {
		return .Remove(, )
	} else {
		return .Add(, )
	}
}

// Toggle1 is [am.Api.Toggle1].
func ( *NetworkMachine) ( string,  am.A) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	if .Is1() {
		return .Remove1(, )
	} else {
		return .Add1(, )
	}
}

// EvToggle is [am.Api.EvToggle].
func ( *NetworkMachine) (
	 *am.Event,  am.S,  am.A,
) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	if .Is() {
		return .EvRemove(, , )
	} else {
		return .EvAdd(, , )
	}
}

// EvToggle1 is [am.Api.EvToggle1].
func ( *NetworkMachine) (
	 *am.Event,  string,  am.A,
) am.Result {
	if .conn == nil {
		return am.Canceled
	}
	if .Is1() {
		return .EvRemove1(, , )
	}
	return .EvAdd1(, , )
}

// ///// Checking (local)

// Is is [am.Api.Is].
func ( *NetworkMachine) ( am.S) bool {
	return .is()
}

// Is1 is [am.Api.Is1].
func ( *NetworkMachine) ( string) bool {
	return .Is(am.S{})
}

func ( *NetworkMachine) ( am.S) bool {
	 := .ActiveStates(nil)
	for ,  := range .MustParseStates() {
		if !slices.Contains(, ) {
			return false
		}
	}

	return true
}

// IsErr is [am.Api.IsErr].
func ( *NetworkMachine) () bool {
	return .Is1(am.StateException)
}

// Not is [am.Api.Not].
func ( *NetworkMachine) ( am.S) bool {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	return .not()
}

func ( *NetworkMachine) ( am.S) bool {
	return utils.SlicesNone(.MustParseStates(), .ActiveStates(nil))
}

// Not1 is [am.Api.No1].
func ( *NetworkMachine) ( string) bool {
	return .Not(am.S{})
}

// Any is [am.Api.Any].
func ( *NetworkMachine) ( ...am.S) bool {
	for ,  := range  {
		if .Is() {
			return true
		}
	}
	return false
}

// Any1 is [am.Api.Any1].
func ( *NetworkMachine) ( ...string) bool {
	for ,  := range  {
		if .Is1() {
			return true
		}
	}
	return false
}

// Has is [am.Api.Has].
func ( *NetworkMachine) ( am.S) bool {
	return utils.SlicesEvery(.StateNames(), )
}

// Has1 is [am.Api.Has1].
func ( *NetworkMachine) ( string) bool {
	return .Has(am.S{})
}

// IsClock is [am.Api.IsClock].
func ( *NetworkMachine) ( am.Clock) bool {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	for ,  := range  {
		if .machTime[.Index1()] !=  {
			return false
		}
	}

	return true
}

// WasClock is [am.Api.WasClock].
func ( *NetworkMachine) ( am.Clock) bool {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	for ,  := range  {
		if .machTime[.Index1()] <  {
			return false
		}
	}

	return true
}

// IsTime is [am.Api.IsTime].
func ( *NetworkMachine) ( am.Time,  am.S) bool {
	.MustParseStates()
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	 := .StateNames()
	if  == nil {
		 = 
	}

	for ,  := range  {
		if .machTime[slices.Index(, [])] !=  {
			return false
		}
	}

	return true
}

// WasTime is [am.Api.WasTime].
func ( *NetworkMachine) ( am.Time,  am.S) bool {
	.MustParseStates()
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	 := .StateNames()
	if  == nil {
		 = 
	}

	for ,  := range  {
		if .machTime[slices.Index(, [])] <  {
			return false
		}
	}

	return true
}

// Switch is [am.Api.Switch].
func ( *NetworkMachine) ( ...am.S) string {
	 := .ActiveStates(nil)
	for ,  := range  {
		for ,  := range  {
			if slices.Contains(, ) {
				return 
			}
		}
	}

	return ""
}

// ///// Waiting (local)

// WhenErr is [am.Api.WhenErr].
func ( *NetworkMachine) ( context.Context) <-chan struct{} {
	return .When([]string{am.StateException}, )
}

// When is [am.Api.When].
func ( *NetworkMachine) (
	 am.S,  context.Context,
) <-chan struct{} {
	if .disposed.Load() {
		return newClosedChan()
	}

	// locks
	.clockMx.Lock()
	defer .clockMx.Unlock()

	return .subs.When(.MustParseStates(), )
}

// When1 is an alias to When() for a single state.
// See When.
func ( *NetworkMachine) (
	 string,  context.Context,
) <-chan struct{} {
	return .When(am.S{}, )
}

// WhenNot returns a channel that will be closed when all the passed states
// become inactive or the machine gets disposed.
//
// ctx: optional context that will close the channel early.
func ( *NetworkMachine) (
	 am.S,  context.Context,
) <-chan struct{} {
	if .disposed.Load() {
		 := make(chan struct{})
		close()
		return 
	}

	// locks
	.clockMx.Lock()
	defer .clockMx.Unlock()

	return .subs.WhenNot(.MustParseStates(), )
}

// WhenNot1 is an alias to WhenNot() for a single state.
// See WhenNot.
func ( *NetworkMachine) (
	 string,  context.Context,
) <-chan struct{} {
	return .WhenNot(am.S{}, )
}

// WhenTime returns a channel that will be closed when all the passed states
// have passed the specified time. The time is a logical clock of the state.
// Machine time can be sourced from [Machine.Time](), or [Machine.Clock]().
//
// ctx: optional context that will close the channel early.
func ( *NetworkMachine) (
	 am.S,  am.Time,  context.Context,
) <-chan struct{} {
	if .disposed.Load() {
		return newClosedChan()
	}

	// close early on invalid
	if len() != len() {
		 := fmt.Errorf(
			"whenTime: states and times must have the same length (%s)",
			utils.J())
		.AddErr(, nil)

		return newClosedChan()
	}

	// locks
	.clockMx.Lock()
	defer .clockMx.Unlock()

	return .subs.WhenTime(, , )
}

// WhenTime1 waits till ticks for a single state equal the given value (or
// more).
//
// ctx: optional context that will close the channel early.
func ( *NetworkMachine) (
	 string,  uint64,  context.Context,
) <-chan struct{} {
	return .WhenTime(am.S{}, am.Time{}, )
}

// WhenTicks waits N ticks of a single state (relative to now). Uses WhenTime
// underneath.
//
// ctx: optional context that will close the channel early.moon
func ( *NetworkMachine) (
	 string,  int,  context.Context,
) <-chan struct{} {
	return .WhenTime(am.S{}, am.Time{uint64() + .Tick()}, )
}

// WhenQuery returns a channel that will be closed when the passed [clockCheck]
// function returns true. [clockCheck] should be a pure function and
// non-blocking.`
//
// ctx: optional context that will close the channel early.
func ( *NetworkMachine) (
	 func( am.Clock) bool,  context.Context,
) <-chan struct{} {
	// locks
	.clockMx.Lock()
	defer .clockMx.Unlock()

	return .subs.WhenQuery(, )
}

func ( *NetworkMachine) ( am.Result) <-chan struct{} {
	// locks
	.clockMx.Lock()
	defer .clockMx.Unlock()

	// finish early
	if .queueTick >= uint64() {
		return newClosedChan()
	}

	return .subs.WhenQueue()
}

// ///// Waiting (remote)

// WhenArgs returns a channel that will be closed when the passed state
// becomes active with all the passed args. Args are compared using the native
// '=='. It's meant to be used with async Multi states, to filter out
// a specific completion.
//
// ctx: optional context that will close the channel when done.
func ( *NetworkMachine) (
	 string,  am.A,  context.Context,
) <-chan struct{} {
	// TODO subscribe on the source via a uint8 token
	return newClosedChan()
}

// ///// Getters (remote)

// Err returns the last error.
func ( *NetworkMachine) () error {
	 := .err.Load()
	if  == nil {
		return nil
	}
	return *
}

// ///// Getters (local)

// StateNames returns a copy of all the state names.
func ( *NetworkMachine) () am.S {
	.schemaMx.Lock()
	defer .schemaMx.Unlock()

	return slices.Clone(.stateNames)
}

func ( *NetworkMachine) ( *regexp.Regexp) am.S {
	 := am.S{}
	for ,  := range .StateNames() {
		if .MatchString() {
			 = append(, )
		}
	}

	return 
}

// ActiveStates returns a copy of the currently active states.
func ( *NetworkMachine) ( am.S) am.S {
	 := *.activeStates.Load()
	if  == nil {
		return slices.Clone()
	}

	 := make(am.S, 0, len())
	for ,  := range  {
		if slices.Contains(, ) {
			 = append(, )
		}
	}

	return 
}

// Tick returns the current tick for a given state.
func ( *NetworkMachine) ( string) uint64 {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	return .tick()
}

func ( *NetworkMachine) ( string) uint64 {
	// TODO validate
	return .machClock[]
}

// Clock returns current machine's clock, a state-keyed map of ticks. If states
// are passed, only the ticks of the passed states are returned.
func ( *NetworkMachine) ( am.S) am.Clock {
	.clockMx.RLock()
	defer .clockMx.RUnlock()
	 := .StateNames()

	if  == nil {
		 = 
	}

	 := am.Clock{}
	for ,  := range  {
		[] = .machClock[]
	}

	return 
}

// Time returns machine's time, a list of ticks per state. Returned value
// includes the specified states, or all the states if nil.
func ( *NetworkMachine) ( am.S) am.Time {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	return .time()
}

func ( *NetworkMachine) ( am.S) am.Time {
	 := .StateNames()
	if  == nil {
		 = 
	}

	 := am.Time{}
	for ,  := range  {
		 := slices.Index(, )
		 = append(, .machTime[])
	}

	return 
}

// NewStateCtx returns a new sub-context, bound to the current clock's tick of
// the passed state.
//
// Context cancels when the state has been de-activated, or right away,
// if it isn't currently active.
//
// State contexts are used to check state expirations and should be checked
// often inside goroutines.
// TODO log reader
func ( *NetworkMachine) ( string) context.Context {
	// TODO reuse existing ctxs
	.clockMx.Lock()
	defer .clockMx.Unlock()

	if ,  := .indexStateCtx[];  {
		return .indexStateCtx[].Ctx
	}

	 := am.CtxValue{
		Id:    .id,
		State: ,
		Tick:  .machClock[],
	}
	,  := context.WithCancel(context.WithValue(.Ctx(),
		am.CtxKey, ))

	// cancel early
	if !.is(am.S{}) {
		// TODO decision msg
		()
		return 
	}

	 := &am.CtxBinding{
		Ctx:    ,
		Cancel: ,
	}

	// add an index
	.indexStateCtx[] = 
	.log(am.LogOps, "[ctx:new] %s", )

	return 
}

// ///// MISC

// Log logs is a local logger.
func ( *NetworkMachine) ( string,  ...any) {
	.log(am.LogExternal, , ...)
}

func ( *NetworkMachine) () am.SemLogger {
	return .semLogger
}

// StatesVerified returns true if the state names have been ordered
// using VerifyStates.
func ( *NetworkMachine) () bool {
	return true
}

// Ctx return worker's root context.
func ( *NetworkMachine) () context.Context {
	return .ctx
}

// Id returns the machine's id.
func ( *NetworkMachine) () string {
	return .id
}

// RemoteId returns the ID of the remote state machine.
func ( *NetworkMachine) () string {
	return .remoteId
}

// ParentId returns the id of the parent machine (if any).
func ( *NetworkMachine) () string {
	return .parentId
}

// Tags returns machine's tags, a list of unstructured strings without spaces.
func ( *NetworkMachine) () []string {
	return .tags
}

// String returns a one line representation of the currently active states,
// with their clock values. Inactive states are omitted.
// Eg: (Foo:1 Bar:3)
func ( *NetworkMachine) () string {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	 := .StateNames()
	 := .ActiveStates(nil)
	 := "("
	for ,  := range  {
		if !slices.Contains(, ) {
			continue
		}

		if  != "(" {
			 += " "
		}
		 := slices.Index(, )
		 += fmt.Sprintf("%s:%d", , .machTime[])
	}

	return  + ")"
}

// StringAll returns a one line representation of all the states, with their
// clock values. Inactive states are in square brackets.
// Eg: (Foo:1 Bar:3)[Baz:2]
func ( *NetworkMachine) () string {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	 := .StateNames()
	 := .ActiveStates(nil)
	 := "("
	 := "["
	for ,  := range  {
		 := slices.Index(, )

		if slices.Contains(, ) {
			if  != "(" {
				 += " "
			}
			 += fmt.Sprintf("%s:%d", , .machTime[])
			continue
		}

		if  != "[" {
			 += " "
		}
		 += fmt.Sprintf("%s:%d", , .machTime[])
	}

	return  + ") " +  + "]"
}

// Inspect returns a multi-line string representation of the machine (states,
// relations, clock).
// states: param for ordered or partial results.
func ( *NetworkMachine) ( am.S) string {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	 := .StateNames()
	if  == nil {
		 = 
	}

	 := .ActiveStates(nil)
	 := ""
	for ,  := range  {

		 := .schema[]
		 := "0"
		if slices.Contains(, ) {
			 = "1"
		}

		 := slices.Index(, )
		 += fmt.Sprintf("%s %s\n"+
			"    |Tick     %d\n", , , .machTime[])
		if .Auto {
			 += "    |Auto     true\n"
		}
		if .Multi {
			 += "    |Multi    true\n"
		}
		if .Add != nil {
			 += "    |Add      " + utils.J(.Add) + "\n"
		}
		if .Require != nil {
			 += "    |Require  " + utils.J(.Require) + "\n"
		}
		if .Remove != nil {
			 += "    |Remove   " + utils.J(.Remove) + "\n"
		}
		if .After != nil {
			 += "    |After    " + utils.J(.After) + "\n"
		}
	}

	return 
}

func ( *NetworkMachine) ( am.LogLevel,  string,  ...any) {
	if .semLogger.Level() <  {
		return
	}

	 := ""
	if .logId.Load() {
		 := .id
		if len() > 5 {
			 = [:5]
		}
		 = "[" +  + "] "
		 =  + 
	}

	 := fmt.Sprintf(, ...)
	 := .semLogger.Logger()
	if  != nil {
		(, , ...)
	} else {
		fmt.Println()
	}

	.logEntriesLock.Lock()
	defer .logEntriesLock.Unlock()

	.logEntries = append(.logEntries, &am.LogEntry{
		Level: ,
		Text:  ,
	})
}

// MustParseStates parses the states and returns them as a list.
// Panics when a state is not defined. It's an usafe equivalent of VerifyStates.
func ( *NetworkMachine) ( am.S) am.S {
	.schemaMx.Lock()
	defer .schemaMx.Unlock()

	// check if all states are defined in m.Schema
	for ,  := range  {
		if !slices.Contains(.stateNames, ) {
			panic(fmt.Sprintf("state %s is not defined for %s (via %s)", ,
				.remoteId, .id))
		}
	}

	return utils.SlicesUniq()
}

// Index1 returns the index of a state in the machine's StateNames() list.
func ( *NetworkMachine) ( string) int {
	return slices.Index(.StateNames(), )
}

func ( *NetworkMachine) ( am.S) []int {
	 := make([]int, len())
	for ,  := range  {
		[] = .Index1()
	}

	return 
}

// Dispose disposes the machine and all its emitters. You can wait for the
// completion of the disposal with `<-mach.WhenDisposed`.
func ( *NetworkMachine) () {
	if !.disposed.CompareAndSwap(false, true) {
		return
	}

	// tracers
	.tracersMx.Lock()
	defer .tracersMx.Unlock()
	for ,  := range .tracers {
		.MachineDispose(.id)
	}

	// run doDispose handlers
	// TODO timeouts?
	for ,  := range .disposeHandlers {
		(.id, .ctx)
	}

	utils.CloseSafe(.whenDisposed)

	// TODO push remotely?
}

// IsDisposed returns true if the machine has been disposed.
func ( *NetworkMachine) () bool {
	return .disposed.Load()
}

// WhenDisposed returns a channel that will be closed when the machine is
// disposed. Requires bound handlers. Use Machine.Disposed in case no handlers
// have been bound.
func ( *NetworkMachine) () <-chan struct{} {
	return .whenDisposed
}

// Export exports the machine state: id, time and state names.
func ( *NetworkMachine) () (*am.Serialized, am.Schema, error) {
	.clockMx.RLock()
	defer .clockMx.RUnlock()
	.schemaMx.RLock()
	defer .schemaMx.RUnlock()

	.log(am.LogChanges, "[import] exported at %d ticks", .time(nil))

	return &am.Serialized{
		ID:          .id,
		Time:        .time(nil),
		StateNames:  .StateNames(),
		MachineTick: .machTick,
		QueueTick:   .queueTick,
	}, am.CloneSchema(.schema), nil
}

// Schema returns a copy of machine's state structure.
func ( *NetworkMachine) () am.Schema {
	return .schema
}

// BindHandlers is [am.Api.BindHandlers].
//
// NetworkMachine supports only pipe handlers (final ones, without negotiation).
func ( *NetworkMachine) ( any) error {
	 := reflect.ValueOf()
	if .Kind() != reflect.Ptr || .Elem().Kind() != reflect.Struct {
		return errors.New("BindTracer expects a pointer to a struct")
	}
	 := reflect.TypeOf().Elem().Name()
	.handlersMx.Lock()
	defer .handlersMx.Unlock()

	 := newHandler(, , &)

	// TODO race
	// old := m.getHandlers(false)
	// m.setHandlers(false, append(old, h))

	.handlers = append(.handlers, )
	if  != "" {
		.log(am.LogOps, "[handlers] bind %s", )
	} else {
		// index for anon handlers
		// TODO race
		.log(am.LogOps, "[handlers] bind %d", len(.handlers))
	}

	return nil
}

// HasHandlers is [am.Api.HasHandlers].
func ( *NetworkMachine) () bool {
	// TODO lock
	// w.handlersLock.Lock()
	// defer w.handlersLock.Unlock()

	return len(.handlers) > 0
}

// DetachHandlers is [am.Api.DetachHandlers].
func ( *NetworkMachine) ( any) error {
	 := .handlers

	for ,  := range  {
		if .h ==  {
			.handlers = utils.SlicesWithout(, )
			// TODO
			// h.dispose()

			return nil
		}
	}

	return errors.New("handlers not bound")
}

// BindTracer is [am.Machine.BindTracer].
//
// NetworkMachine tracers cannot mutate synchronously, as network machines
// don't have a queue and WILL deadlock when nested.
func ( *NetworkMachine) ( am.Tracer) error {
	.tracersMx.Lock()
	defer .tracersMx.Unlock()

	 := reflect.ValueOf()
	if .Kind() != reflect.Ptr || .Elem().Kind() != reflect.Struct {
		return errors.New("BindTracer expects a pointer to a struct")
	}
	 := reflect.TypeOf().Elem().Name()

	.tracers = append(.tracers, )
	.log(am.LogOps, "[tracers] bind %s", )

	return nil
}

// DetachTracer is [am.Api.DetachTracer].
func ( *NetworkMachine) ( am.Tracer) error {
	.tracersMx.Lock()
	defer .tracersMx.Unlock()

	 := reflect.ValueOf()
	if .Kind() != reflect.Ptr || .Elem().Kind() != reflect.Struct {
		return errors.New("DetachTracer expects a pointer to a struct")
	}
	 := reflect.TypeOf().Elem().Name()

	for ,  := range .tracers {
		if  ==  {
			// TODO check
			.tracers = slices.Delete(.tracers, , +1)
			.log(am.LogOps, "[tracers] detach %s", )

			return nil
		}
	}

	return errors.New("tracer not bound")
}

// Tracers is [am.Api.Tracers].
func ( *NetworkMachine) () []am.Tracer {
	.clockMx.Lock()
	defer .clockMx.Unlock()

	return slices.Clone(.tracers)
}

// updateClock updates the clock of this NetworkMachine and requires a locked
// clockMx, which is then unlocked by this method.
func ( *NetworkMachine) (
	 am.Time,  uint64,  uint32,
) {
	// TODO require mutType and called states for SyncAllMutations and handlers

	.tracersMx.Lock()
	defer .tracersMx.Unlock()

	 := .machTime
	 := maps.Clone(.machClock)

	 := .ActiveStates(nil)
	 := am.S{}
	 := .StateNames()
	for ,  := range  {
		if am.IsActiveTick([]) {
			 = append(, )
		}
	}
	 = .activateRequired()
	 := am.StatesDiff(, )
	 := am.StatesDiff(, )

	 := &am.Transition{
		MachApi: ,
		Id:      utils.RandId(8),

		TimeBefore: ,
		TimeAfter:  ,
		Mutation: &am.Mutation{
			// TODO use add and remove when all ticks passed
			Type:   am.MutationSet,
			Called: .Index(),
			Args:   nil,
			IsAuto: false,
		},
		LogEntries:    .logEntries,
		TargetIndexes: .Index(),
	}
	.IsCompleted.Store(true)
	.IsSettled.Store(true)
	// TODO may not be true for qTicks-only updates
	.IsAccepted.Store(true)
	.t.Store()
	.logEntries = nil

	// call tracers
	for ,  := range .tracers {
		.TransitionInit()
	}
	for ,  := range .tracers {
		.TransitionStart()
	}

	// set active states
	.machTime = 
	for ,  := range .machTime {
		.machClock[[]] = 
	}
	.machTick = 
	// the local queue ticks later than the new one, all queue subs are invalid
	if .queueTick >  {
		.log(am.LogOps, "[queueTick] flushing (%d to %s)", .queueTick, )
		.queueFlush()
	}
	.queueTick = 
	.activeStates.Store(&)
	.activeStatesDbg = 

	// handlers
	.processHandlers(, )

	// unlock for tracers (always locked by the caller)
	.clockMx.Unlock()

	for ,  := range .tracers {
		// TODO dbg tracing doesnt show up in the UI?
		.TransitionEnd()
	}

	// subscriptions
	.processSubscriptions(, , )
	.t.Store(nil)
}

// activateRequired will fake required states (when not all synced and
// schema present)
func ( *NetworkMachine) ( am.S) am.S {
	// skip for schema-less netmachs
	if .schema == nil {
		return 
	}

	// locks
	.schemaMx.RLock()
	defer .schemaMx.RUnlock()

	 := slices.Clone()
	 := make(map[string]bool)
	var  func(string)
	 = func( string) {
		if ![] {
			[] = true
			for ,  := range .schema[].Require {
				 = append(, )
				()
			}
		}
	}

	// recurse on all the active states
	for ,  := range  {
		()
	}

	return utils.SlicesUniq()
}

func ( *NetworkMachine) ( bool) []*handler {
	if ! {
		.handlersMx.Lock()
		defer .handlersMx.Unlock()
	}

	return slices.Clone(.handlers)
}

func ( *NetworkMachine) (,  am.S) {
	// no changes
	if len()+len() == 0 {
		return
	}

	for ,  := range .getHandlers(false) {
		if  == nil {
			continue
		}

		// TODO ensure multi states covered
		for ,  := range  {
			.handle(, , , am.SuffixState)
		}
		for ,  := range  {
			.handle(, , , am.SuffixEnd)
		}

		// global handler
		.handle(, , am.StateAny, am.SuffixState)
	}
}

// handle runs a single handler method (currently only pipes).
func ( *NetworkMachine) ( *handler,  int, ,  string) {
	.mx.Lock()
	 :=  + 
	 := am.NewEvent(nil, )
	.Name = 
	.MachineId = .remoteId

	// TODO descriptive name
	 := strconv.Itoa() + ":" + .name

	if .semLogger.Level() >= am.LogEverything {
		 := utils.TruncateStr(, 15)
		 = utils.PadString(strings.ReplaceAll(
			, " ", "_"), 15, "_")
		.log(am.LogEverything, "[handle:%-15s] %s", , )
	}

	// cache
	,  := .missingCache[]
	if  {
		.mx.Unlock()

		return
	}
	,  := .methodCache[]
	if ! {
		 = .methods.MethodByName()

		// support field handlers
		if !.IsValid() {
			 = .methods.Elem().FieldByName()
		}
		if !.IsValid() {
			.missingCache[] = struct{}{}
			.mx.Unlock()
			return
		}
		.methodCache[] = 
	}

	// call the handler (pipes dont block)
	.log(am.LogOps, "[handler:%d] %s", , )

	// tracers
	// m.tracersMx.RLock()
	 := .t.Load()
	for  := range .tracers {
		.tracers[].HandlerStart(, , )
	}
	// m.tracersMx.RUnlock()

	// call TODO should go-fork to avoid nested deadlocks?
	_ = .Call([]reflect.Value{reflect.ValueOf()})

	// tracers
	// m.tracersMx.RLock()
	for  := range .tracers {
		.tracers[].HandlerEnd(, , )
	}
	// m.tracersMx.RUnlock()

	// locks
	.mx.Unlock()
}

func ( *NetworkMachine) (
	,  am.S,  am.Clock,
) {
	// lock
	.clockMx.RLock()

	// collect
	 := .subs.ProcessStateCtx()
	 := slices.Concat(
		.subs.ProcessWhen(, ),
		.subs.ProcessWhenTime(),
		.subs.ProcessWhenQueue(.queueTick),
		.subs.ProcessWhenQuery(),
	)

	// unlock
	.clockMx.RUnlock()

	// close outside the critical zone
	for ,  := range  {
		()
	}
	for ,  := range  {
		close()
	}
}

// AddBreakpoint1 is [am.Api.AddBreakpoint1].
func ( *NetworkMachine) (
	 string,  string,  bool,
) {
	// TODO
}

// AddBreakpoint is [am.Api.AddBreakpoint].
func ( *NetworkMachine) (
	 am.S,  am.S,  bool,
) {
	// TODO
}

// Groups is [am.Api.Groups].
func ( *NetworkMachine) () (map[string][]int, []string) {
	// TODO maybe sync along with schema?
	return nil, nil
}

// CanAdd is [am.Api.CanAdd].
func ( *NetworkMachine) ( am.S,  am.A) am.Result {
	// TODO check relations
	return am.Executed
}

// CanAdd1 is [am.Api.CanAdd1].
func ( *NetworkMachine) ( string,  am.A) am.Result {
	// TODO check relations
	return am.Executed
}

// CanRemove is [am.Api.CanRemove].
func ( *NetworkMachine) ( am.S,  am.A) am.Result {
	// TODO check relations
	return am.Executed
}

// CanRemove1 is [am.Api.CanRemove1].
func ( *NetworkMachine) ( string,  am.A) am.Result {
	// TODO check relations
	return am.Executed
}

// Transition is [am.Machine.Transition].
func ( *NetworkMachine) () *am.Transition {
	return .t.Load()
}

// QueueLen is [am.Api.QueueLen].
func ( *NetworkMachine) () uint16 {
	// TODO implement outbound throttling
	return 0
}

func ( *NetworkMachine) () {
	.subs.QueueFlush()
}

// QueueTick is [am.Api.QueueTick].
func ( *NetworkMachine) () uint64 {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	return .queueTick
}

// MachineTick is [am.Api.MachineTick].
func ( *NetworkMachine) () uint32 {
	.clockMx.RLock()
	defer .clockMx.RUnlock()

	return .machTick
}

// ParseStates is [am.Api.ParseStates].
func ( *NetworkMachine) ( am.S) am.S {
	.schemaMx.Lock()
	defer .schemaMx.Unlock()

	// check if all states are defined in the schema
	 := make(map[string]struct{})
	 := false
	for  := range  {
		if ,  := .schema[[]]; ! {
			continue
		}
		if ,  := [[]]; ! {
			[[]] = struct{}{}
		} else {
			// mark as duplicated
			 = true
		}
	}

	if  {
		return utils.SlicesUniq()
	}
	return slices.Collect(maps.Keys())
}

// OnDispose is [am.Api.OnDispose].
func ( *NetworkMachine) ( am.HandlerDispose) {
	.handlersMx.Lock()
	defer .handlersMx.Unlock()

	.disposeHandlers = append(.disposeHandlers, )
}

func ( *NetworkMachine) (
	 am.MutationType,  am.S,
) bool {
	if .schema == nil {
		return true
	}

	// TODO adapt [am.RelationsResolver] to filter here
	return true
}

// debug
// func (w *NetworkMachine) QueueDump() []string {
// 	return nil
// }