package debugger

import (
	
	
	
	
	
	
	
	
	
	
	
	
	
	

	
	
	
	

	

	ammcp 
	am 
	
	
)

const (
	// TODO customize
	playInterval = 500 * time.Millisecond
	// TODO add param --max-clients
	maxClients            = 5000
	timeFormat            = "15:04:05.000000000"
	fastJumpAmount        = 50
	arrowThrottleMs       = 200
	logUpdateDebounce     = 300 * time.Millisecond
	sidebarUpdateDebounce = time.Second
	searchAsTypeWindow    = 1500 * time.Millisecond
	heartbeatInterval     = 1 * time.Minute
	// heartbeatInterval = 5 * time.Second
	// msgMaxAge             = 0

	// maxMemMb = 100
	maxMemMb        = 50
	msgMaxThreshold = 300
	// max txs queued for scrolling the timelines
	scrollTxThrottle = 3
	logFile          = "log.md"
)

type Client struct {
	*server.Client

	CursorTx1       int
	ReaderCollapsed bool
	CursorStep1     int
	SelectedState   string
	// extracted log entries per tx ID
	// TODO GC when all entries are closedAt and the first client's tx is later
	//  than the latest closedAt; whole tx needs to be disposed at the same time
	LogReader map[string][]*types.LogReaderEntry

	logRenderedCursor1    int
	logRenderedLevel      am.LogLevel
	logRenderedFilters    *types.Filters
	logRenderedGroup      string
	logRenderedTimestamps bool
	logReaderMx           sync.Mutex
}

func init() {
	gob.Register(server.Exportable{})
	gob.Register(am.Relation(0))
}

func newClient(, ,  string,  *server.Exportable) *Client {
	 := &Client{
		Client: &server.Client{
			Id:         ,
			ConnId:     ,
			SchemaHash: ,
			Exportable: ,
		},
		LogReader: make(map[string][]*types.LogReaderEntry),
	}
	.ParseSchema()

	return 
}

func ( *Client) ( string,  *types.LogReaderEntry) int {
	.logReaderMx.Lock()
	defer .logReaderMx.Unlock()

	.LogReader[] = append(.LogReader[], )

	return len(.LogReader[]) - 1
}

func ( *Client) ( string,  int) *types.LogReaderEntry {
	.logReaderMx.Lock()
	defer .logReaderMx.Unlock()

	,  := .LogReader[]
	if ! ||  >= len() {
		return nil
	}

	return []
}

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

// ///// SSH

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

func ( ssh.Session) (tcell.Screen, error) {
	, ,  := .Pty()
	if ! {
		return nil, errors.New("no pty requested")
	}
	,  := terminfo.LookupTerminfo(.Term)
	if  != nil {
		return nil, 
	}
	,  := tcell.NewTerminfoScreenFromTtyTerminfo(&tty{
		Session: ,
		size:    .Window,
		ch:      ,
	}, )
	if  != nil {
		return nil, 
	}
	return , nil
}

type tty struct {
	ssh.Session
	size     ssh.Window
	ch       <-chan ssh.Window
	resizecb func()
	mu       sync.Mutex
}

func ( *tty) () error {
	go func() {
		for  := range .ch {
			.mu.Lock()
			.size = 
			.notifyResize()
			.mu.Unlock()
		}
	}()
	return nil
}

func ( *tty) () error {
	return nil
}

func ( *tty) () error {
	return nil
}

func ( *tty) () ( tcell.WindowSize,  error) {
	.mu.Lock()
	defer .mu.Unlock()

	return tcell.WindowSize{
		Width:  .size.Width,
		Height: .size.Height,
	}, nil
}

func ( *tty) ( func()) {
	.mu.Lock()
	defer .mu.Unlock()

	.resizecb = 
}

func ( *tty) () {
	if .resizecb != nil {
		.resizecb()
	}
}

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

// ///// MISC

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

// openURL opens the specified URL in the default browser of the user.
// https://gist.github.com/sevkin/9798d67b2cb9d07cb05f89f14ba682f8
func openURL( string) error {
	var  string
	var  []string

	switch runtime.GOOS {
	case "windows":
		 = "cmd.exe"
		 = []string{
			"/c", "rundll32", "url.dll,FileProtocolHandler",
			strings.ReplaceAll(, "&", "^&"),
		}
	case "darwin":
		 = "open"
		 = []string{}
	default:
		if isWSL() {
			 = "cmd.exe"
			 = []string{"start", }
		} else {
			 = "xdg-open"
			 = []string{}
		}
	}

	 := exec.Command(, ...)
	 := .Start()
	if  != nil {
		return 
	}
	 = .Wait()
	if  != nil {
		return 
	}

	return nil
}

// isWSL checks if the Go program is running inside Windows Subsystem for Linux
func isWSL() bool {
	,  := exec.Command("uname", "-r").Output()
	if  != nil {
		return false
	}
	return strings.Contains(strings.ToLower(string()), "microsoft")
}

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

// ///// MCP SERVER

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

type mcpServer struct {
	*ammcp.Server
	d *Debugger
}

func newMcpServer( *Debugger) (*mcpServer, error) {
	,  := ammcp.New(.Mach, ammcp.Opts{
		Name: "am-dbg",
		Desc: "MCP to control the asyncmachine-go TUI debugger. " +
			"The double-line border panel is currently focused. " +
			"Most features are accessible via ToggleTool.",
		MutCallback: func( context.Context) error {
			 := .Mach.Eval("MCPTool", func() {
				.hRedrawFull(false)
			}, )
			if ! {
				return am.ErrEvalTimeout
			}
			return nil
		},
		StatesInclude:  states.DebuggerGroups.Mcp,
		StatesReadonly: states.DebuggerGroups.McpReadonly,
		Args:           types.ArgsRpc,
		StateCalls:     types.StateCalls,
		Version:        .params.Version,
	})
	if  != nil {
		return nil, 
	}

	// bind keys from keyboard.go
	 := slices.Collect(maps.Keys(.getKeystrokes()))
	 = append(, "enter", "tab", "shift+tab", "up", "down")
	 := mcp.NewTool(
		"PressKey",
		mcp.WithDescription("Press a key combination"),
		mcp.WithString(
			"key",
			mcp.Required(),
			mcp.Description("Name of the state to add"),
			mcp.Enum(...),
		),
	)

	// TODO mouse clicks

	// TUI

	 := mcp.NewTool(
		"TextScreen",
		mcp.WithDescription("Get text content of the TUI screen"),
	)
	 := mcp.NewTool(
		"TextLogReader",
		mcp.WithDescription("Get text content of the Log Reader pane"),
	)
	 := mcp.NewTool(
		"TextHelp",
		mcp.WithDescription("Get text content of the Help dialog"),
	)
	 := mcp.NewTool(
		"TextLog",
		mcp.WithDescription("Get text content of the current log buffer"),
	)
	 := mcp.NewTool(
		"TextClients",
		mcp.WithDescription("Get a full clients list"),
	)
	 := mcp.NewTool(
		"NetworkGraph",
		mcp.WithDescription("Get an XML of the network graph"),
	)
	 := mcp.NewTool(
		"Transition Steps",
		mcp.WithDescription(
			"Get a markdown sequence diagram of the transition steps",
		),
	)

	// methods

	 := mcp.NewTool("GoToMachAddr",
		mcp.WithDescription("Show a specific machine, transition, or mach time"),
		mcp.WithString(
			"url",
			mcp.Required(),
			mcp.Description("Address to go to"),
		))

	// server
	 := &mcpServer{
		Server: ,
		d:      ,
	}

	// bind handlers
	.Mcp.AddTool(, .textScreen)
	.Mcp.AddTool(, .textReader)
	.Mcp.AddTool(, .textHelp)
	.Mcp.AddTool(, .textLog)
	.Mcp.AddTool(, .textClients)
	.Mcp.AddTool(, .netGraph)
	.Mcp.AddTool(, .txSteps)
	.Mcp.AddTool(, .goToMachAddr)
	.Mcp.AddTool(, .pressKey)

	return , nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	,  := .RequireString("url")
	if  != nil {
		return mcp.NewToolResultError(.Error()), nil
	}
	,  := types.ParseMachUrl()
	if  != nil {
		return mcp.NewToolResultError(.Error()), nil
	}
	.d.GoToMachAddress(, false)

	return mcp.NewToolResultText("ok"), nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	// check if reader visible
	if .d.Mach.Not1(ss.LogReaderVisible) {
		return mcp.NewToolResultError(fmt.Sprintf(
			"state %s not active", ss.LogReaderVisible,
		)), nil
	}

	return mcp.NewToolResultText(.d.LogReaderText()), nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	return mcp.NewToolResultText(.d.App.ScreenText()), nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	return mcp.NewToolResultText(textViewToText(.d.helpDialogLeft) + "\n\n" +
		textViewToText(.d.helpDialogRight)), nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	 := *.d.Params.Load()
	if !.OutputLog {
		return mcp.NewToolResultError("--output-log not set"), nil
	}

	,  := os.ReadFile(path.Join(.OutputDir, logFile))
	if  != nil {
		return nil, 
	}
	return mcp.NewToolResultText(string()), nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	 := *.d.Params.Load()
	if !.OutputLog {
		return mcp.NewToolResultError("--output-clients not set"), nil
	}

	,  := os.ReadFile(path.Join(.OutputDir, "clients.txt"))
	if  != nil {
		return nil, 
	}
	return mcp.NewToolResultText(string()), nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	 := *.d.Params.Load()
	if !.OutputLog {
		return mcp.NewToolResultError("--output-clients not set"), nil
	}

	,  := os.ReadFile(path.Join(.OutputDir, "graph.xml"))
	if  != nil {
		return nil, 
	}
	return mcp.NewToolResultText(string()), nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	 := *.d.Params.Load()
	if !.OutputLog {
		return mcp.NewToolResultError("--output-tx not set"), nil
	}

	,  := os.ReadFile(path.Join(.OutputDir, "tx.md"))
	if  != nil {
		return nil, 
	}
	return mcp.NewToolResultText(string()), nil
}

func ( *mcpServer) (
	 context.Context,  mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
	,  := .RequireString("key")
	if  != nil {
		return mcp.NewToolResultError(.Error()), nil
	}

	// split "key" param on + and parse the modifier
	 := strings.Split(, "+")
	var  tcell.ModMask = tcell.ModNone
	 := [len()-1]
	for ,  := range [:len()-1] {
		switch strings.ToLower() {
		case "alt":
			 |= tcell.ModAlt
		case "ctrl":
			 |= tcell.ModCtrl
		case "shift":
			 |= tcell.ModShift
		}
	}

	// create evKey and rune
	var  tcell.Key
	var  rune
	 := make(map[string]tcell.Key)
	for ,  := range tcell.KeyNames {
		[strings.ToLower()] = 
	}
	 := strings.ToLower()
	if ,  := [];  {
		 = 
	} else {
		, _ = utf8.DecodeRuneInString(strings.ToLower())
		if  == utf8.RuneError {
			return mcp.NewToolResultError("key rune err"), nil
		}
		 = tcell.KeyRune
	}

	.d.App.QueueEvent(tcell.NewEventKey(, , ))
	return mcp.NewToolResultText("ok"), nil
}