// This file wires the remote-control bridge (internal/remotecontrol, #442) into
// the full-screen TUI, the counterpart to internal/cli/repl/remotecontrol.go.
// It adds the "/remote-control" command that starts/stops an in-process
// HTTP+WebSocket server mirroring the session to a paired browser on the LAN,
// mirrors transcript output to that browser, surfaces browser-submitted prompts
// as a tea.Msg, and routes side-effect tool-call confirmations to the browser
// while a client is connected.
//
// The non-remote path is unchanged: when no session is active the mirror is a
// no-op, waitRemoteInput returns a nil Cmd, and buildConfig installs no
// BeforeToolCall (tools run under the up-front trust the TUI already grants).
package tui

import (
	"context"
	"fmt"
	"strings"

	tea "charm.land/bubbletea/v2"

	"github.com/smallnest/pigo/internal/agentcore"
	"github.com/smallnest/pigo/internal/remotecontrol"
	"github.com/smallnest/pigo/internal/trust"
)

// remoteSession owns the running server + bridge for one /remote-control
// activation. It is stored on runSession (so buildConfig can reach it to install
// the confirm seam) and is nil until the command starts a session.
type remoteSession struct {
	server *remotecontrol.Server
	bridge *remotecontrol.Bridge
	url    string
}

// hasClient reports whether a browser is currently paired and connected.
func (rs *remoteSession) hasClient() bool {
	return rs != nil && rs.bridge != nil && rs.bridge.Enabled()
}

// sendOutput mirrors session text to the remote browser. It records into the
// server's replay ring even when no client is connected, so a browser that pairs
// mid-session is replayed the recent scrollback.
func (rs *remoteSession) sendOutput(text string) {
	if rs == nil || rs.server == nil || text == "" {
		return
	}
	rs.server.SendOutput(text)
}

// startRemote builds and starts the server+bridge, storing the session on the
// runSession. It returns the pairing URL, or an error if a server is already
// running or the listener could not bind.
func (s *runSession) startRemote() (string, error) {
	if s.remote != nil {
		return s.remote.url, fmt.Errorf("already running")
	}
	// Break the server↔bridge construction cycle: build the server (Sink), then
	// the bridge over it, then route client frames back to the bridge.
	srv := remotecontrol.NewServer(remotecontrol.Config{}, nil)
	bridge := remotecontrol.NewBridge(srv)
	srv.SetHandler(bridge)
	url, err := srv.Start()
	if err != nil {
		return "", err
	}
	s.remote = &remoteSession{server: srv, bridge: bridge, url: url}
	return url, nil
}

// stopRemote shuts down the running server and clears the session. It is a no-op
// when remote control is off.
func (s *runSession) stopRemote() {
	if s.remote == nil {
		return
	}
	_ = s.remote.server.Stop(context.Background())
	s.remote = nil
}

// remoteConfirmSeam builds the BeforeToolCall seam that routes side-effect
// tool-call confirmations to the paired browser while one is connected. When no
// browser is connected (or the tool is not side-effecting, or the cwd is
// trusted) it returns nil so the tool runs under the up-front trust the TUI
// grants — the non-remote behavior is unchanged.
//
// A ctx cancellation (interrupt) makes Confirm return remote=false, which is
// treated as a denial so an interrupted run does not silently proceed.
func remoteConfirmSeam(rs *remoteSession, mgr *trust.Manager, cwd string) agentcore.BeforeToolCallFunc {
	return func(ctx context.Context, call agentcore.AgentToolCall) *agentcore.BeforeToolCallDecision {
		if !rs.hasClient() || mgr == nil {
			return nil
		}
		if !trust.SideEffectTools[call.Name] {
			return nil
		}
		if mgr.IsTrusted(cwd) {
			return nil
		}
		summary := trust.ToolCallSummary(call)
		d, remote := rs.bridge.Confirm(ctx, call.Name, summary)
		if !remote {
			return blockRemoteToolCall(call, cwd)
		}
		if d.Always {
			mgr.SetSessionTrust(cwd)
		}
		if !d.Approve {
			return blockRemoteToolCall(call, cwd)
		}
		return nil
	}
}

func blockRemoteToolCall(call agentcore.AgentToolCall, cwd string) *agentcore.BeforeToolCallDecision {
	msg := fmt.Sprintf("tool %q blocked: %s is not trusted (use /trust to trust this project)", call.Name, cwd)
	return &agentcore.BeforeToolCallDecision{
		Block:   true,
		Content: &agentcore.ContentList{agentcore.NewTextContent(msg)},
	}
}

// runRemoteControl handles the /remote-control command and its stop/status
// subcommands. It mutates m.session.remote, folds a system block into the
// transcript, and returns the listener Cmd (waitRemoteInput) on a successful
// start so browser-submitted prompts begin arriving.
func (m Model) runRemoteControl(line string) (tea.Model, tea.Cmd) {
	m.transcript.addUser(line)
	m.input.Clear()
	m.menu.close()
	defer m.relayout()

	if m.session == nil {
		m.transcript.addSystem("(remote control unavailable: no active session)")
		return m, nil
	}
	arg := strings.TrimSpace(strings.TrimPrefix(line, "/remote-control"))
	switch arg {
	case "stop":
		if m.session.remote == nil {
			m.transcript.addSystem("remote control is not running")
			return m, nil
		}
		m.session.stopRemote()
		m.transcript.addSystem("remote control stopped")
		return m, nil
	case "status":
		if m.session.remote == nil {
			m.transcript.addSystem("remote control: off")
			return m, nil
		}
		state := "waiting for a browser to connect"
		if m.session.remote.hasClient() {
			state = "browser connected"
		}
		m.transcript.addSystem(fmt.Sprintf("remote control: on (%s)\n  %s", state, m.session.remote.url))
		return m, nil
	case "":
		if m.session.remote != nil {
			m.transcript.addSystem("remote control already running:\n  " + m.session.remote.url)
			return m, nil
		}
		url, err := m.session.startRemote()
		if err != nil {
			m.transcript.addSystem("remote control: " + err.Error())
			return m, nil
		}
		var b strings.Builder
		fmt.Fprintf(&b, "Remote control started. Open this URL on a device on the same network:\n\n  %s\n", url)
		if qr, qerr := remotecontrol.Render(url); qerr == nil {
			b.WriteString("\n" + qr)
		}
		b.WriteString("\nRun /remote-control stop to end the session.")
		m.transcript.addSystem(b.String())
		return m, m.waitRemoteInput()
	default:
		m.transcript.addSystem("usage: /remote-control [stop|status]")
		return m, nil
	}
}

// remoteEcho mirrors visible transcript text to the paired browser. It is a
// no-op when remote control is off, so callers can invoke it unconditionally at
// each point the transcript gains content.
func (m Model) remoteEcho(text string) {
	if m.session != nil && m.session.remote != nil {
		m.session.remote.sendOutput(text)
	}
}

// waitRemoteInput returns a tea.Cmd that blocks on the bridge's remote-input
// channel and emits one remoteInputMsg per browser submission. The Update loop
// re-issues it after each so successive prompts keep arriving. It returns nil
// when remote control is off, which stops the listener.
func (m Model) waitRemoteInput() tea.Cmd {
	if m.session == nil || m.session.remote == nil || m.session.remote.bridge == nil {
		return nil
	}
	ch := m.session.remote.bridge.RemoteInput()
	return func() tea.Msg {
		text, ok := <-ch
		if !ok {
			return nil
		}
		return remoteInputMsg{text: text}
	}
}
