package relay

import (
	
	
	
	
	
	
	
	
	

	
	
	

	amhelp 
	am 
	arpc 
	ssrpc 
	
	
	typesDbg 
	
	
)

var (
	ssR  = states.RelayStates
	ssT  = states.WsTcpTunStates
	Pass = types.Pass
)

type A = types.A

type Relay struct {
	Mach    *am.Machine
	Args    types.Args
	HttpMux *http.ServeMux

	// WS TCP tunnels
	wsTcpTuns map[string]*WsTcpTun
	// counter of WS TCP tunnels
	cWsTcpTuns int
	dbgClients map[string]*server.Client
	out        types.OutputFunc
	lastExport time.Time
	http       *http.Server
}

// New creates a new Relay - state machine, RPC server.
func ( context.Context,  types.Args) (*Relay, error) {
	 := .Output
	if  == nil {
		 = func( string,  ...any) {
			fmt.Printf(, ...)
		}
	}

	if .Debug {
		("WASM relay debug, loading .env\n")
		// load .env
		_ = godotenv.Load()
	}

	 := &Relay{
		Args: ,

		lastExport: time.Now(),
		out:        ,
		dbgClients: make(map[string]*server.Client),
		wsTcpTuns:  make(map[string]*WsTcpTun),
	}
	 := "relay"
	if .Name != "" {
		 += "-" + .Name
	}
	,  := am.NewCommon(, , states.RelaySchema, ssR.Names(),
		, .Parent, &am.Opts{
			DontPanicToException: .Debug,
		})
	if  != nil {
		return nil, 
	}
	if .Debug {
		_ = amhelp.MachDebugEnv()
	}
	.Mach = 
	_, _ = arpc.MachReplEnv(, &arpc.ReplOpts{
		Args: types.ARpc{},
		// TODO ParseRpc
	})
	// mach.SemLogger().SetArgsMapperDef("remote_addr")
	.SemLogger().SetArgsMapper(types.LogArgs)

	return , nil
}

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

// ///// HANDLERS (COMMON)

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

func ( *Relay) ( *am.Event) {
	 := .Args

	if .RotateDbg != nil {
		 := .RotateDbg
		.out("starting relay server on %s\n", .ListenAddr)
		for ,  := range .FwdAddr {
			.out("forwarding to %s\n", )
		}
		 := typesDbg.Params{
			FwdData: .FwdAddr,
		}
		go server.StartRpc(.Mach, .ListenAddr, nil, )
	}

	if .Wasm != nil {
		.Mach.Add1(ssR.HttpStarting, nil)
	}
}

// TODO StartEnd

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

// ///// METHODS (COMMON)

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

func ( *Relay) ( *am.Event) am.Result {
	return .Mach.EvAdd1(, ssR.Start, nil)
}

func ( *Relay) ( *am.Event) am.Result {
	return .Mach.EvRemove1(, ssR.Start, nil)
}

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

// ///// HANDLERS (WASM)

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

func ( *Relay) ( *am.Event) {
	 := .Mach.NewStateCtx(ssR.HttpStarting)

	 := .Args.Wasm.ListenAddr
	.HttpMux = http.NewServeMux()
	.http = &http.Server{
		Addr:    ,
		Handler: .HttpMux,
	}
	go func() {
		if .Err() != nil {
			return // expired
		}

		go .Mach.Add1(ssR.HttpReady, nil)
		 := .http.ListenAndServe()
		if  != nil &&  != http.ErrServerClosed {
			// TODO retry?
			.Mach.AddErr(, nil)
		}
		.Mach.Remove1(ssR.HttpReady, nil)
	}()
}

func ( *Relay) ( *am.Event) {
	// TODO /dial - RPC clients (WS to TCP dial)
	// ctx := r.Mach.NewStateCtx(ssR.HttpReady)

	.out("WASM relay listening on http://%s\n", .Args.Wasm.ListenAddr)

	// static
	if  := .Args.Wasm.StaticDir;  != "" {
		.out("WASM relay serving %s\n", )
		.HttpMux.Handle("/", http.FileServer(http.Dir()))
	}

	// tun listen
	.HttpMux.HandleFunc("/listen/",
		func( http.ResponseWriter,  *http.Request) {
			.HandleWsTcpListen(, , )
		})
}

func ( *Relay) (
	 *am.Event,  http.ResponseWriter,  *http.Request,
) {
	// TODO loop guard to detect flood (fix thread safety)
	// TODO create a REPL file for repl- machs

	// TODO req.Context() needs body to be read to close
	,  := context.WithCancel(.Context())
	defer ()

	// metric
	.Mach.EvAdd1(, ssR.WsTunListenConn, nil)

	// parse "machid/localhost:1234"
	,  := strings.CutPrefix(.URL.Path, arpc.WsPathListen)
	if ! {
		.Mach.EvAddErrState(, ssR.ErrNetwork, fmt.Errorf(
			"invalid /listen path: %s", .URL.Path), nil)
		return
	}
	,  := path.Split()
	 = strings.TrimSuffix(, "/")
	.Mach.Log("WS TCP tunnel for %s at %s", , )

	// TODO origin security
	// TODO ID security

	// websocket
	,  := websocket.Accept(, , &websocket.AcceptOptions{
		InsecureSkipVerify: true,
	})
	if  != nil {
		.Mach.EvAddErrState(, ssR.ErrNetwork, , Pass(&A{
			Addr: ,
			Id:   ,
		}))
		return
	}

	// check client matchers
	for ,  := range .Args.Wasm.ClientMatchers {
		if !.Id.MatchString() {
			continue
		}
		.Mach.Log("WS tunnel for %s accepted by client matcher", )

		 := websocket.NetConn(, , websocket.MessageBinary)
		,  := .NewClient(, , )
		if  != nil {
			.Mach.EvAddErr(,
				fmt.Errorf("failed to create RPC client for %s: %s", , ), nil)
		} else {
			// stay alive until disconn
			<-.Mach.When1(ssrpc.ClientStates.Connected, )
			<-.Mach.WhenNot1(ssrpc.ClientStates.Connected, )
			.Stop(nil, , true)
			return
		}
	}

	// init tun
	var  *am.Machine
	 = .Mach.Eval("listen_init", func() {
		// rm old TODO also by duped IDs
		// TODO races and causes 2 tunnels per 1 client
		if ,  := .wsTcpTuns[];  {
			.Mach.Log("disposing existing WS TCP tunnel for %s at %s", , )
			amhelp.Dispose(.Mach)
			time.Sleep(100 * time.Millisecond)
		}

		// init mach
		 := fmt.Sprintf("%s-wtt-%d", .Mach.Id(), .cWsTcpTuns)
		.cWsTcpTuns++
		,  := NewWsTcpTun(, , , , .RemoteAddr, ,
			.Mach, .Args.Debug)
		if  != nil {
			.Mach.EvAddErr(,
				fmt.Errorf("failed to create tun mach %s: %s", , ), nil)
			return
		}

		// ok
		 = .Mach
		.wsTcpTuns[] = 
	}, )
	// err
	if ! ||  == nil {
		_ = .Close(websocket.StatusInternalError, "tun creation failed")
		return
	}

	// REPL addr file
	if  := .Args.Wasm.ReplAddrDir;  != "" &&
		strings.HasPrefix(, "repl-") {

		 :=  + ".addr"
		.Mach.Log("REPL detected, creating %s", )
		 := os.WriteFile(filepath.Join(, ), []byte(), 0o644)
		if  != nil {
			.Mach.EvAddErr(, fmt.Errorf(
				"failed to write REPL addr file %s: %s", , ), nil)
		}
	}

	// start and wait
	.EvAdd1(, ssT.Start, nil)
	<-.WhenDisposed()

	// clean up
	.Mach.Eval("listen_end", func() {
		amhelp.Dispose()
		.Mach.EvAdd1(, ssR.WsTunListenDisconn, nil)
	}, )
}

func ( *Relay) ( *am.Event) {
	if  := .http;  != nil {
		.Mach.AddErr(.Close(), nil)
	}
}

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

// ///// HANDLERS (DBG)

// ///// ///// /////
// TODO typed args for dbg server

func ( *Relay) ( *am.Event) bool {
	,  := .Args["msgs_tx"].([]*dbg.DbgMsgTx)
	,  := .Args["conn_ids"].([]string)
	return  && 
}

func ( *Relay) ( *am.Event) {
	 := .Args["msgs_tx"].([]*dbg.DbgMsgTx)
	 := .Args["conn_ids"].([]string)

	for ,  := range  {

		// TODO check tokens
		 := .MachineID
		 := .dbgClients[]
		if ,  := .dbgClients[]; ! {
			.Mach.Log("Error: client not found: %s\n", )
			continue
		}

		if .MsgStruct == nil {
			.Mach.Log("Error: schema missing for %s, ignoring tx\n", )
			continue
		}

		// verify it's from the same client
		if .ConnId != [] {
			.Mach.Log("Error: conn_id mismatch for %s, ignoring tx\n", )
			continue
		}

		// append the msg
		.MsgTxs = append(.MsgTxs, )
	}

	 := .Args.RotateDbg
	if time.Since(.lastExport) > .IntervalTime || .msgsCount() > .IntervalTx {
		.lastExport = time.Now()
		if  := .hExportData();  != nil {
			// TODO err handler
			.out("Error: export failed %s\n", )
			.Mach.AddErr(, nil)
		}
	}
}

func ( *Relay) ( *am.Event) bool {
	,  := .Args["msg_struct"].(*dbg.DbgMsgStruct)
	,  := .Args["conn_id"].(string)
	if ! || ! || .ID == "" {
		.Mach.Log("Error: msg_struct malformed\n")
		return false
	}

	return true
}

func ( *Relay) ( *am.Event) {
	// initial structure data
	 := .Args["msg_struct"].(*dbg.DbgMsgStruct)
	 := .Args["conn_id"].(string)
	var  *server.Client

	// update existing client
	if ,  := .dbgClients[.ID];  {
		if .ConnId != "" && .ConnId ==  {
			.Mach.Log("schema changed for %s", .ID)
			.out("schema changed for %s\n", .ID)
			// TODO use MsgStructPatch
			// TODO keep old revisions
			.MsgStruct = 
			 = 
			.ParseSchema()

		} else {
			.Mach.Log("client %s already exists, overriding", .ID)
			.out("client %s already exists, overriding\n", .ID)
		}
	}

	// create a new client
	if  == nil {
		.out("new client %s\n", .ID)
		// TODO server.NewClient
		 = &server.Client{
			Id:         .ID,
			ConnId:     ,
			SchemaHash: amhelp.SchemaHash(.States),
			Exportable: &server.Exportable{
				MsgStruct: ,
			},
		}
		.Connected.Store(true)
		.dbgClients[.ID] = 
	}

	// TODO remove the last active client if over the limit
	// if len(r.clients) > maxClients {
	// 	var (
	// 		lastActiveTime time.Time
	// 		lastActiveID   string
	// 	)
	// 	// TODO get time from msgs
	// 	for id, c := range r.clients {
	// 		active := c.LastActive()
	// 		if active.After(lastActiveTime) || lastActiveID == "" {
	// 			lastActiveTime = active
	// 			lastActiveID = id
	// 		}
	// 	}
	// 	r.Mach.Add1(ss.RemoveClient, am.A{"Client.id": lastActiveID})
	// }

	.Mach.Add1(ssR.InitClient, am.A{"id": .ID})
}

func ( *Relay) ( *am.Event) bool {
	,  := .Args["conn_id"].(string)
	if ! {
		.Mach.Log("Error: DisconnectEvent malformed\n")
		return false
	}

	return true
}

func ( *Relay) ( *am.Event) {
	 := .Args["conn_id"].(string)
	for ,  := range .dbgClients {
		if .ConnId != "" && .ConnId ==  {
			// mark as disconnected
			.Connected.Store(false)
			.Mach.Log("client %s disconnected", .Id)
			.out("client %s disconnected\n", .Id)
			break
		}
	}

	// export on last client
	if len(.dbgClients) == 0 {
		if  := .hExportData();  != nil {
			// TODO err handler
			.out("Error: export failed %s\n", )
			.Mach.AddErr(, nil)
		}
	}
}

// TODO SetArgs

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

// ///// METHODS (DBG)

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

// TODO ExportDataState
func ( *Relay) () error {
	// TODO export text logs
	// TODO configurable suffix
	 := .msgsCount()
	.out("exporting %d transitions for %d clients\n", , len(.dbgClients))

	 := .Args.RotateDbg
	 := .Filename + "-" + time.Now().Format("2006-01-02T15-04-05")

	// create file
	 := filepath.Join(.Dir, +".gob.br")
	,  := os.Create()
	if  != nil {
		return 
	}
	defer func() {
		.Mach.AddErr(.Close(), nil)
	}()

	// prepare the format
	 := make([]*server.Exportable, len(.dbgClients))
	 := 0
	for ,  := range .dbgClients {
		[] = .Exportable
		++
	}

	// create a new brotli writer
	 := brotli.NewWriter()
	defer func() {
		.Mach.AddErr(.Close(), nil)
	}()

	// encode
	 := gob.NewEncoder()
	 = .Encode()
	if  != nil {
		return 
	}

	// flush old msgs
	for ,  := range .dbgClients {
		.Exportable.MsgTxs = nil
	}

	return nil
}

func ( *Relay) () int {
	 := 0
	for ,  := range .dbgClients {
		 += len(.MsgTxs)
	}

	return 
}