package dbg

import (
	
	
	
	
	
	
	
	
	
	

	
	
	
)

const (
	// DbgAddr is the default address of the am-dbg server.
	DbgAddr    = "localhost:6831"
	DbgAddrWeb = "localhost:6832"
	// EnvAmDbgAddr is the address of a running am-dbg instance.
	// "1" expands to "localhost:6831"
	EnvAmDbgAddr = "AM_DBG_ADDR"
)

// DbgMsg is the interface for the messages to be sent to the am-dbg server.
// TODO refac Msg
type DbgMsg interface {
	// Clock returns the state's clock, using the passed index
	Clock(statesIndex machine.S, state string) uint64
	// Is returns true if the state is active, using the passed index
	Is(statesIndex machine.S, states machine.S) bool
}

// ///// STRUCT

// DbgMsgStruct contains the state and relations data.
// TODO refac: MsgSchema
type DbgMsgStruct struct {

	// Machine ID
	ID string
	// state names defining the indexes for diffs
	StatesIndex machine.S
	// all the states with relations
	// TODO refac: Schema
	States machine.Schema
	// list of group names and state indexes
	Groups map[string][]int
	// order of groups
	GroupsOrder []string
	// parent machine ID
	Parent string
	// machine tags
	Tags []string

	// TODO include the current mach time
	//  MTime am.Time
}

func ( *DbgMsgStruct) ( machine.S,  string) uint64 {
	return 0
}

func ( *DbgMsgStruct) ( machine.S,  machine.S) bool {
	return false
}

// ///// TRANSITION

// DbgMsgTx contains transition data.
type DbgMsgTx struct {
	MachineID string
	// Transition ID
	// TODO refac: Id
	ID string
	// Clocks is represents the machine time [am.Time] from after the current
	// transition.
	// TODO refac to TimeAfter, re-gen all the assets
	Clocks machine.Time
	// QueueTick is the current queue tick in the machine.
	// transition.
	QueueTick uint64
	// TODO QueueDebug with all string entries for comparison
	// MutQueueToken is the token of a prepended mutation, can be scheduled or
	// executed, depending on IsQueued.
	MutQueueToken uint64
	// MutQueueTick is the assigned queue tick when the tx will be executed.
	// Only for IsQueued.
	MutQueueTick uint64
	// mutation type
	Type machine.MutationType
	// called states
	// TODO remove. Deprecated use CalledStateNames(index)
	CalledStates []string
	// TODO rename to CalledStates, re-gen all assets
	CalledStatesIdxs []int
	// all the transition steps
	Steps []*machine.Step
	// log entries created during the transition
	LogEntries []*machine.LogEntry
	// log entries before the transition, which happened after the prev one
	PreLogEntries []*machine.LogEntry
	// queue length at the start of the transition
	// TODO rename to QueueLen
	// TODO change to int32
	Queue int
	// Time is human time. Don't send this over the wire.
	// TODO remove or skip in msgpack
	// TODO rename to HTime
	Time *time.Time
	// transition was triggered by an auto state
	IsAuto bool
	// result of the transition
	// TODO rename to IsAccepted
	Accepted bool
	// is this a check (Can*) tx or mutation?
	IsCheck bool
	// is this a queued mutation?
	IsQueued  bool
	Args      map[string]string
	QueueDump []string

	// TODO add Mutation.Source, only if semLogger.IsGraph() == true
	// Source *am.MutSource
	// TODO include missed mutations (?) because of the queue limit
	//  since the previous tx
	// TODO add Transition.PipesAdded
	// TODO add Transition.PipesRemoved
	// TODO add Transition.CtxAdded
	// TODO add Transition.CtxRemoved
	// TODO add time taken via tracer
}

func ( *DbgMsgTx) ( machine.S,  string) uint64 {
	 := slices.Index(, )
	if len(.Clocks) <=  {
		return 0
	}
	return .Clocks[]
}

func ( *DbgMsgTx) ( machine.S,  machine.S) bool {
	for ,  := range  {
		 := .Index(, )

		// new schema issue
		if  == -1 {
			return false
		}

		if len(.Clocks) <=  || !machine.IsActiveTick(.Clocks[]) {
			return false
		}
	}

	return true
}

func ( *DbgMsgTx) ( machine.S,  string) int {
	 := slices.Index(, ) //nolint:typecheck
	return 
}

func ( *DbgMsgTx) ( machine.S) machine.S {
	 := machine.S{}

	for ,  := range  {
		 := slices.Index(, )
		if len(.Clocks) <=  {
			continue
		}
		if machine.IsActiveTick(.Clocks[]) {
			 = append(, )
		}
	}

	return 
}

func ( *DbgMsgTx) ( machine.S) machine.S {
	// old compat
	if .CalledStates != nil {
		return .CalledStates
	}

	 := make(machine.S, len(.CalledStatesIdxs))

	for ,  := range .CalledStatesIdxs {
		[] = []
	}

	return 
}

// TODO unify with Tx String
func ( *DbgMsgTx) ( machine.S) string {
	 := "mach://" + .MachineID + "/" + .ID + "\n"
	 += .Time.UTC().Format(time.RFC3339Nano) + "\n\n"
	 := "[" + .Type.String() + "] " +
		utils.J(.CalledStateNames()) + "\n"
	// TODO add source from mutation, stats, mark queued muts

	 := ""
	for ,  := range .Steps {
		 += "- " + .StringFromIndex() + "\n"
	}

	return  +  + 
}

// TODO unify with Mut String
func ( *DbgMsgTx) ( machine.S) string {
	 := "+"
	if .Type == machine.MutationRemove {
		 = "-"
	}
	 += utils.J(.CalledStateNames())

	return 
}

func ( *DbgMsgTx) ( machine.S,  string) bool {
	return .Is(, machine.S{})
}

// TODO Sum() and TimeSum(idxs []int)
func ( *DbgMsgTx) () uint64 {
	 := uint64(0)
	for ,  := range .Clocks {
		 += 
	}

	return 
}

// ///// CLIENT

type client struct {
	addr string
	rpc  *rpc.Client
}

func newDbgClient( context.Context,  string,  bool) (*client, error) {
	var  *rpc.Client
	var  error
	if  {
		// log.Printf("[dbg] dialing...")
		, ,  := websocket.Dial(,
			"ws://"++"/dbg.ws", &websocket.DialOptions{})
		if  != nil {
			// log.Printf("[dbg] err %v", err)
			return nil, fmt.Errorf("websocket.Dial failed %w", )
		}
		// log.Printf("[dbg] conn...")
		 := websocket.NetConn(, , websocket.MessageBinary)
		 = rpc.NewClient()
		// log.Printf("[dbg] client...")

	} else {
		,  = rpc.Dial("tcp4", )
		if  != nil {
			return nil, 
		}
	}

	return &client{addr: , rpc: }, nil
}

func ( *client) ( *DbgMsgTx) error {
	var  string
	// DEBUG
	// fmt.Printf("sendMsgTx %v\n", msg.CalledStates)
	// TODO const name
	 := .rpc.Call("RPCServer.DbgMsgTx", , &)
	if  != nil {
		return 
	}

	return nil
}

func ( *client) ( *DbgMsgStruct) error {
	if  == nil {
		return nil
	}

	var  string
	// TODO use Go() to not block
	// TODO const name
	// log.Printf("[dbg] rpc.Call")
	 := .rpc.Call("RPCServer.DbgMsgSchema", , &)
	if  != nil {
		// log.Printf("[dbg] rpc.Call err %v", err)
		return 
	}
	// log.Printf("[dbg] schema sent: %s", reply)

	return nil
}

// ///// TRACER

type Tracer struct {
	*machine.TracerNoOp

	Mach machine.Api
	Addr string
	// connect to a websocket
	Ws bool

	outbox   chan func()
	c        *client
	errCount atomic.Int32
	exited   atomic.Bool
	mx       sync.Mutex
	lastTx   string
	// number of queued mutations since lastTx
	queued    int
	lastMTime machine.Time
}

var _ machine.Tracer = &Tracer{}

func ( machine.Api,  string) *Tracer {
	 := &Tracer{
		Addr:   ,
		Mach:   ,
		outbox: make(chan func(), 1000),
	}
	 := .Mach.Context()

	// process the queue
	go func() {
		for {
			select {
			case  := <-.outbox:
				()
			case <-.Done():
				return
			}
		}
	}()

	return 
}

func ( *Tracer) ( machine.Api) context.Context {
	.mx.Lock()
	defer .mx.Unlock()

	gob.Register(machine.Relation(0))
	var  error
	.Mach = 
	.lastMTime = .Time(nil)

	// add to the queue
	.outbox <- func() {
		if .IsDisposed() {
			return
		}
		.c,  = newDbgClient(.Context(), .Addr, .Ws)
		if  != nil && os.Getenv(machine.EnvAmLog) != "" {
			// log.Printf("%s: failed to connect to am-dbg: %s\n", mach.Id(), err)
			return
		}

		 = sendMsgSchema(, .c)
		if  != nil && os.Getenv(machine.EnvAmLog) != "" {
			// log.Println(err, nil)
			return
		}
	}

	return nil
}

func ( *Tracer) ( machine.Api,  machine.Schema) {
	.lastMTime = .Time(nil)

	// add to the queue
	.outbox <- func() {
		 := sendMsgSchema(, .c)
		if  != nil {
			 = fmt.Errorf("failed to send new struct to am-dbg: %w", )
			.AddErr(, nil)
			return
		}
	}
}

func ( *Tracer) ( *machine.Transition) {
	 := .MachApi
	if .errCount.Load() > 10 && !.exited.Load() {
		.exited.Store(true)
		if os.Getenv(machine.EnvAmLog) != "" {
			// log.Println(mach.Id() + ": too many errors - detaching dbg tracer")
		}
		go func() {
			 := .DetachTracer()
			if  != nil && os.Getenv(machine.EnvAmLog) != "" {
				// log.Printf(mach.Id()+": failed to detach dbg tracer: %s\n", err)
			}
		}()

		return
	}
	 := .Mutation

	// skip check mutations when not logging them
	if .IsCheck && !.SemLogger().IsCan() {
		return
	}

	.InternalLogEntriesLock.Lock()
	defer .InternalLogEntriesLock.Unlock()
	.mx.Lock()
	defer .mx.Unlock()

	 := &DbgMsgTx{
		MachineID:        .Id(),
		ID:               .Id,
		Clocks:           .TimeAfter,
		Accepted:         .IsAccepted.Load(),
		IsCheck:          .IsCheck,
		Type:             .Type,
		CalledStatesIdxs: .Called,
		Steps:            .Steps,
		// no locking necessary, as the tx is finalized (read-only)
		LogEntries:    removeLogPrefix(, .LogEntries),
		PreLogEntries: removeLogPrefix(, .PreLogEntries),
		IsAuto:        .IsAuto,
		Queue:         int(.QueueLen),
		QueueTick:     .QueueTick(),
		MutQueueToken: .QueueToken,
		// debug
		// QueueDump: mach.QueueDump(),
	}

	// collect args
	 := .SemLogger()
	if .IsArgs() {
		.Args = .MapArgs(.ArgsMapper())
	}

	.lastTx = .Id
	.lastMTime = .TimeAfter
	.queued = 0

	// add to the queue
	.outbox <- func() {
		if .c == nil {
			if os.Getenv(machine.EnvAmLog) != "" {
				// log.Println(mach.Id() + ": no connection to am-dbg")
			}
			.errCount.Add(1)

			return
		}
		 := .c.sendMsgTx()
		if  != nil {
			if os.Getenv(machine.EnvAmLog) != "" {
				// log.Printf(mach.Id()+":failed to send a msg to am-dbg: %s", err)
			}
			.errCount.Add(1)
		}
	}
}

func ( *Tracer) ( string) {
	// TODO lock & dispose?
	// t.Mach = nil

	go func() {
		// TODO push out pending m.logEntries using add:Healthcheck (if set)
		time.Sleep(100 * time.Millisecond)
		if .c != nil && .c.rpc != nil {
			_ = .c.rpc.Close()
		}
	}()
}

func ( *Tracer) ( machine.Api,  *machine.Mutation) {
	// skip check mutations when not logging them
	if .IsCheck && !.SemLogger().IsCan() {
		return
	}

	.mx.Lock()
	defer .mx.Unlock()

	 := &DbgMsgTx{
		MachineID:        .Id(),
		ID:               .lastTx + "-" + strconv.Itoa(.queued),
		Clocks:           .lastMTime,
		Accepted:         true,
		IsCheck:          .IsCheck,
		Type:             .Type,
		CalledStatesIdxs: .Called,
		IsAuto:           .IsAuto,
		Queue:            int(.QueueLen),
		IsQueued:         true,
		QueueTick:        .QueueTick(),
		MutQueueTick:     .QueueTick,
		MutQueueToken:    .QueueToken,
		// debug
		// QueueDump: mach.QueueDump(),
		// TODO Source
	}

	// collect args
	 := .SemLogger()
	if .IsArgs() {
		.Args = .MapArgs(.ArgsMapper())
	}
	if  := machine.ParseArgs(.Args).Err;  != nil {
		if .Args == nil {
			.Args = make(map[string]string)
		}
		.Args["err"] = .Error()
	}
	.queued++

	// add to the queue
	.outbox <- func() {
		if .c == nil {
			if os.Getenv(machine.EnvAmLog) != "" {
				// log.Println(mach.Id() + ": no connection to am-dbg")
			}
			.errCount.Add(1)

			return
		}
		 := .c.sendMsgTx()
		if  != nil {
			if os.Getenv(machine.EnvAmLog) != "" {
				// log.Printf(mach.Id()+":failed to send a msg to am-dbg: %s", err)
			}
			.errCount.Add(1)
		}
	}
}

// ///// FUNCS

type Opts struct {
	WebSocket bool
}

// TransitionsToDbg sends transitions to the am-dbg server.
//
// opts: single optional opts struct.
func ( machine.Api,  string,  ...*Opts) error {
	if  == "" {
		 = DbgAddr
	}

	// prevent double debugging
	 := .Tracers()
	for ,  := range  {
		if ,  := .(*Tracer);  && .Addr ==  {
			return nil
		}
	}

	// add the tracer
	 := NewTracer(, )
	 := .BindTracer()
	if  != nil {
		return 
	}
	if len() > 0 && [0].WebSocket {
		.Ws = true
	}

	// call manually for existing machines
	.MachineInit()

	return nil
}

// sendMsgSchema sends the machine's states and relations
func sendMsgSchema( machine.Api,  *client) error {
	,  := .Groups()
	 := &DbgMsgStruct{
		ID:          .Id(),
		StatesIndex: .StateNames(),
		States:      .Schema(),
		Groups:      ,
		GroupsOrder: ,
		Parent:      .ParentId(),
		Tags:        .Tags(),
	}

	// TODO retries
	// log.Printf("[dbg] Schema...")
	 := .sendMsgSchema()
	if  != nil {
		return fmt.Errorf("failed to send a msg to am-dbg: %w", )
	}

	return nil
}

func removeLogPrefix(
	 machine.Api,  []*machine.LogEntry,
) []*machine.LogEntry {
	 := slices.Clone()
	if !.SemLogger().IsId() {
		return 
	}

	 := 5
	 := 3 // "[] "
	 := min(len(.Id())+, +)

	 := make([]*machine.LogEntry, len())
	for ,  := range  {
		if  == nil || len(.Text) <  {
			continue
		}

		[] = &machine.LogEntry{
			Level: .Level,
			Text:  .Text[:],
		}
	}

	return 
}