package node

import (
	
	
	
	
	

	amhelp 
	am 
	
	
	ssrpc 
	ampipe 
)

var ssC = states.ClientStates

// Client is a node client, connecting to a supervisor and then a worker.
type Client struct {
	*am.ExceptionHandler
	Mach *am.Machine

	Name       string
	SuperAddr  string
	LogEnabled bool
	// LeaveSuper is a flag to leave the supervisor after connecting to the
	// worker. TODO
	LeaveSuper bool
	// ConnTimeout is the time to wait for an outbound connection to be
	// established. Default is 5 seconds.
	ConnTimeout time.Duration

	// network

	SuperRpc  *rpc.Client
	WorkerRpc *rpc.Client

	// internal

	// current nodes list (set by Start)
	nodeList  []string
	stateDeps *ClientStateDeps
}

// implement ConsumerHandlers
var _ ssrpc.ConsumerHandlers = &Client{}

// NewClient creates a new Client instance with the provided context, id,
// workerKind, state dependencies, and options. Returns a pointer to the Client
// instance and an error if any validation or initialization fails.
func ( context.Context,  string,  string,
	 *ClientStateDeps,  *ClientOpts,
) (*Client, error) {
	// validate
	if  == "" {
		return nil, errors.New("client: workerStruct required")
	}
	if  == nil {
		return nil, errors.New("client: stateNames required")
	}
	if .WorkerSStruct == nil {
		return nil, errors.New("client: workerStruct required")
	}
	if .WorkerSNames == nil {
		return nil, errors.New("client: stateNames required")
	}
	// TODO defaults
	if .ClientSStruct == nil {
		return nil, errors.New("client: workerStruct required")
	}
	// TODO defaults
	if .ClientSNames == nil {
		return nil, errors.New("client: stateNames required")
	}
	if  == "" {
		return nil, errors.New("client: workerKind required")
	}
	if  == nil {
		 = &ClientOpts{}
	}

	 := amhelp.Implements(.WorkerSNames, states.WorkerStates.Names())
	if  != nil {
		 := fmt.Errorf(
			"worker has to implement am/node/states/WorkerStates: %w", )
		return nil, 
	}

	 := fmt.Sprintf("%s-%s-%s", , ,
		time.Now().Format("150405"))

	 := &Client{
		Name:        ,
		ConnTimeout: 5 * time.Second,
		LogEnabled:  os.Getenv(EnvAmNodeLogClient) != "",
		stateDeps:   ,
	}
	if amhelp.IsDebug() {
		.ConnTimeout = 10 * .ConnTimeout
	}
	,  := am.NewCommon(, "nc-"+, .ClientSStruct,
		.ClientSNames, , .Parent, &am.Opts{
			Tags: []string{"node-client"},
		})
	if  != nil {
		return nil, 
	}

	.SemLogger().SetArgsMapper(LogArgs)
	.Mach = 
	amhelp.MachDebugEnv()

	// check base states
	 = amhelp.Implements(.StateNames(), ssC.Names())
	if  != nil {
		 := fmt.Errorf(
			"client has to implement am/node/states/ClientStates: %w", )
		return nil, 
	}

	return , nil
}

// ///// ///// /////

// ///// HANDLERS

// ///// ///// /////

func ( *Client) ( *am.Event) bool {
	 := ParseArgs(.Args)
	return  != nil && len(.NodesList) > 0
}

func ( *Client) ( *am.Event) {
	var  error
	 := .Mach.NewStateCtx(ssC.Start)
	 := ParseArgs(.Args)
	 := .NodesList[0]
	.nodeList = .NodesList

	// init super rpc (but dont connect just yet)
	.SuperRpc,  = rpc.NewClient(, , GetSuperClientId(.Name),
		states.SupervisorSchema, ssS.Names(), &rpc.ClientOpts{
			Parent:   .Mach,
			Consumer: .Mach,
		})
	if  != nil {
		 := fmt.Errorf("failed to connect to the Supervisor: %w", )
		_ = AddErrRpc(.Mach, , nil)
		return
	}

	// bind to super rpc
	 = errors.Join(
		ampipe.BindConnected(.SuperRpc.Mach, .Mach, ssC.SuperDisconnected,
			ssC.SuperConnecting, ssC.SuperConnected, ssC.SuperDisconnecting),
		ampipe.BindErr(.SuperRpc.Mach, .Mach, ssC.ErrSupervisor),
		ampipe.BindReady(.SuperRpc.Mach, .Mach, ssC.SuperReady, ""),
	)
	if  != nil {
		.Mach.AddErr(, nil)
		return
	}

	// unblock
	go func() {
		// try all nodes TODO randomize
		for ,  := range .NodesList {
			if .Err() != nil {
				return // expired
			}
			.Mach.Log("trying node %d: %s", , )

			// (re)start and wait
			// TODO handle in ExceptionState when SuperConnecting active
			.Mach.Remove1(am.StateException, nil)
			.SuperRpc.Addr = 
			// fewer retries, bc of fallbacks
			// TODO config via a composable RetryPolicy from rpc-c
			.SuperRpc.ConnRetries = 3
			.SuperRpc.Start()
			 := amhelp.WaitForAny(, .ConnTimeout,
				.Mach.When1(ssC.SuperReady, nil),
				.SuperRpc.Mach.WhenNot1(ssrpc.ClientStates.Start, nil),
			)
			if .Err() != nil {
				return // expired
			}

			// stopped rpc client is an error
			if .SuperRpc.Mach.Not1(ssrpc.ClientStates.Start) {
				// TODO blacklist the node for X time
				// re-start
				.SuperRpc.Stop(, false)
				.Mach.Remove1(ssC.Exception, nil)
				continue
			}

			if  != nil {
				 := errors.Join(, .SuperRpc.Mach.Err())
				_ = AddErrRpc(.Mach, , nil)

				return
			}
		}
	}()
}

func ( *Client) ( *am.Event) {
	if .SuperRpc != nil {
		.SuperRpc.Stop(context.TODO(), true)
	}
	if .WorkerRpc != nil {
		.WorkerRpc.Stop(context.TODO(), true)
	}
}

func ( *Client) ( *am.Event) bool {
	return .SuperRpc.Worker.Is1(ssS.WorkersAvailable)
}

func ( *Client) ( *am.Event) {
	// supervisor needs IDs of RPC clients for routing and ACL
	.SuperRpc.Worker.Add1(ssS.ProvideWorker, PassRpc(&A{
		SuperRpcId:  .SuperRpc.Mach.Id(),
		WorkerRpcId: rpc.GetClientId(GetWorkerClientId(.Name)),
	}))
}

func ( *Client) ( *am.Event) bool {
	 := rpc.ParseArgs(.Args)
	return  != nil && .Name != "" && .Payload != nil
}

// WorkerPayloadState handles both Supervisor and Worker inbound payloads, but
// this shared code only deals with [states.ClientStatesDef.WorkerRequested].
func ( *Client) ( *am.Event) {
	.Mach.Remove1(ssC.WorkerPayload, nil)
	 := rpc.ParseArgs(.Args)
	.log("worker %s delivered: %s", .Payload.Source, .Name)

	if .Name != ssS.ProvideWorker {
		// worker impl will handle
		return
	}

	if .Mach.Not1(ssC.WorkerRequested) {
		.log("worker not requested")
		return
	}

	 := .Mach.NewStateCtx(ssC.WorkerRequested)
	 := .Mach.NewStateCtx(ssC.Start)
	,  := .Payload.Data.(string)
	if ! ||  == "" {
		 := errors.New("invalid worker address")
		.Mach.AddErrState(ssC.ErrSupervisor, , nil)

		return
	}
	.log("connecting to worker: %s", )

	// unblock
	go func() {
		// connect to the worker
		var  error
		.WorkerRpc,  = rpc.NewClient(, , GetWorkerClientId(.Name),
			.stateDeps.WorkerSStruct, .stateDeps.WorkerSNames, &rpc.ClientOpts{
				Parent:   .Mach,
				Consumer: .Mach,
			})
		if  != nil {
			 := fmt.Errorf("failed to connect to the Worker: %w", )
			_ = AddErrRpc(.Mach, , nil)
			return
		}
		// delay for rpc/Mux
		.WorkerRpc.HelloDelay = 100 * time.Millisecond

		// bind to worker rpc
		 = errors.Join(
			ampipe.BindConnected(.WorkerRpc.Mach, .Mach, ssC.WorkerDisconnected,
				ssC.WorkerConnecting, ssC.WorkerConnected, ssC.WorkerDisconnecting),
			ampipe.BindErr(.WorkerRpc.Mach, .Mach, ssC.ErrWorker),
			ampipe.BindReady(.WorkerRpc.Mach, .Mach, ssC.WorkerReady, ""),
		)
		if  != nil {
			.Mach.AddErr(, nil)
			return
		}

		// start and wait
		.WorkerRpc.Start()
		 = amhelp.WaitForAny(, .ConnTimeout,
			.Mach.When1(ssC.WorkerReady, nil),
			.WorkerRpc.Mach.WhenErr(),
		)
		// TODO retry
		if  != nil {
			.Mach.Remove1(ssC.WorkerRequested, nil)
			return
		}
	}()
}

// ///// ///// /////

// ///// METHODS

// ///// ///// /////

// Start initializes the client with a list of node addresses to connect to.
func ( *Client) ( []string) {
	.Mach.Add1(ssC.Start, Pass(&A{
		NodesList: ,
	}))
}

// Stop halts the client's connection to both the supervisor and worker RPCs,
// and removes the client state from the state machine.
func ( *Client) ( context.Context) {
	if .SuperRpc != nil {
		.SuperRpc.Stop(, false)
	}
	if .WorkerRpc != nil {
		.WorkerRpc.Stop(, false)
	}
	.Mach.Remove1(ssC.Start, nil)
}

// ReqWorker sends a request to add a "WorkerRequested" state to the client's
// state machine and waits for "WorkerReady" state.
func ( *Client) ( context.Context) error {
	// failsafe worker request
	,  := amhelp.NewReqAdd1(.Mach, ssC.WorkerRequested, nil).Run()
	if  != nil {
		return 
	}

	 = amhelp.WaitForAll(, .ConnTimeout,
		.Mach.When1(ssC.WorkerReady, nil))
	if  != nil {
		return 
	}

	.log("worker connected: %s", .WorkerRpc.Worker.Id())
	return nil
}

// Dispose deallocates resources and stops the client's RPC connections.
func ( *Client) ( context.Context) {
	.Stop()
	.SuperRpc = nil
	.WorkerRpc = nil
	.Mach.Dispose()
}

func ( *Client) ( string,  ...any) {
	if !.LogEnabled {
		return
	}
	.Mach.Log(, ...)
}

// ///// ///// /////

// ///// MISC

// ///// ///// /////

// ClientStateDeps contains the state definitions and names of the client and
// worker machines, needed to create a new client.
type ClientStateDeps struct {
	ClientSStruct am.Schema
	ClientSNames  am.S
	WorkerSStruct am.Schema
	WorkerSNames  am.S
}

// ClientOpts provides configuration options for creating a new client state
// machine.
type ClientOpts struct {
	// Parent is a parent state machine for a new client state machine. See
	// [am.Opts].
	Parent am.Api
	// TODO
	Tags []string
}

// GetWorkerClientId returns a Node Client machine ID from a name.
func ( string) string {
	return "nc-worker-" + 
}

// GetClientId returns a Node Client machine ID from a name.
func ( string) string {
	return "nc-super-" + 
}