package opsconsole

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"strings"
	"time"

	"iop/apps/edge/internal/bootstrap"
	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/config"
)

// TargetState holds the current console target: node selection, adapter execution
// target, session, and background mode. It mirrors what a single console session
// is operating against.
type TargetState struct {
	NodeRef    string
	Adapter    string
	Target     string
	SessionID  string
	Background bool
	TimeoutSec int
}

// Run boots an edge runtime and drives the interactive ops console loop.
// It is the body of `edge console` and is the only entry point exposed to cmd.
func Run(ctx context.Context, cfg *config.EdgeConfig, in io.Reader, out io.Writer) error {
	rt, err := bootstrap.NewRuntime(cfg)
	if err != nil {
		return err
	}
	if err := rt.Start(ctx); err != nil {
		return err
	}
	defer func() { _ = rt.Stop() }()

	edgeSvc := rt.Service
	eventBus := rt.EventBus
	events := NewEventRouter(out, rt.Registry, rt.Logger)

	consoleCtx, stopConsoleEvents := context.WithCancel(ctx)
	defer stopConsoleEvents()
	runEvents, unsubscribeRuns := eventBus.SubscribeAllRuns(4096)
	defer unsubscribeRuns()
	nodeEvents, unsubscribeNodes := eventBus.SubscribeAllNodes(64)
	defer unsubscribeNodes()
	go events.Drain(consoleCtx, runEvents, nodeEvents)

	target := &TargetState{
		Adapter:    cfg.Console.Adapter,
		Target:     cfg.Console.ResolveTarget(),
		SessionID:  NormalizeSessionID(cfg.Console.SessionID),
		Background: cfg.Console.Background,
		TimeoutSec: cfg.Console.TimeoutSec,
	}

	fmt.Fprintf(out, "IOP Edge console listening on %s\n", cfg.Server.Listen)
	fmt.Fprintf(out, "Console target node=%s adapter=%s target=%s session=%s background=%v\n",
		target.NodeRef, target.Adapter, target.Target, target.SessionID, target.Background)
	fmt.Fprintln(out, "Start node.sh on another host, then type a message here.")
	fmt.Fprintln(out, "Commands: /nodes, /node <id|alias>, /session <id>, /background on|off, /capabilities, /transport, /exit")

	scanner := bufio.NewScanner(in)
	for {
		fmt.Fprint(out, "edge> ")
		if !scanner.Scan() {
			fmt.Fprintln(out)
			return scanner.Err()
		}

		cmd := ParseCommand(scanner.Text())
		switch cmd.Kind {
		case CommandEmpty:
			continue
		case CommandExit:
			fmt.Fprintln(out, "bye")
			return nil
		case CommandNodes:
			PrintNodes(out, edgeSvc.ListNodeSnapshots(), target.NodeRef)
		case CommandSetNode:
			if cmd.UsageErr != "" {
				fmt.Fprintln(out, cmd.UsageErr)
				continue
			}
			if _, err := edgeSvc.ResolveNodeSnapshot(cmd.Arg); err != nil {
				fmt.Fprintf(out, "error: %v\n", err)
				continue
			}
			target.NodeRef = cmd.Arg
			fmt.Fprintf(out, "node → %s\n", target.NodeRef)
		case CommandSetSession:
			if cmd.UsageErr != "" {
				fmt.Fprintln(out, cmd.UsageErr)
				continue
			}
			target.SessionID = cmd.Arg
			fmt.Fprintf(out, "session → %s\n", target.SessionID)
		case CommandSetBackground:
			if cmd.UsageErr != "" {
				fmt.Fprintln(out, cmd.UsageErr)
				continue
			}
			target.Background = cmd.Arg == "on"
			if target.Background {
				fmt.Fprintln(out, "background → on")
			} else {
				fmt.Fprintln(out, "background → off")
			}
		case CommandCapabilities:
			if err := SendCapabilities(ctx, edgeSvc, out, target); err != nil {
				fmt.Fprintf(out, "error: %v\n", err)
			}
		case CommandTransportStatus:
			if err := SendTransportStatus(ctx, edgeSvc, out, target); err != nil {
				fmt.Fprintf(out, "error: %v\n", err)
			}
		case CommandPrompt:
			if err := SendRun(ctx, edgeSvc, events, out, target, cmd.Arg); err != nil {
				fmt.Fprintf(out, "error: %v\n", err)
			}
		}
	}
}

// CommandKind enumerates the console slash commands and the implicit "prompt"
// command (anything not starting with `/`).
type CommandKind int

const (
	CommandEmpty CommandKind = iota
	CommandExit
	CommandNodes
	CommandSetNode
	CommandSetSession
	CommandSetBackground
	CommandCapabilities
	CommandTransportStatus
	CommandPrompt
)

// Command is the parsed result of a single console input line.
type Command struct {
	Kind     CommandKind
	Arg      string
	UsageErr string
}

func ParseCommand(line string) Command {
	msg := strings.TrimSpace(line)
	if msg == "" {
		return Command{Kind: CommandEmpty}
	}
	lower := strings.ToLower(msg)
	switch {
	case lower == "/exit" || lower == "/quit" || lower == "exit" || lower == "quit":
		return Command{Kind: CommandExit}
	case lower == "/nodes":
		return Command{Kind: CommandNodes}
	case lower == "/capabilities":
		return Command{Kind: CommandCapabilities}
	case lower == "/transport":
		return Command{Kind: CommandTransportStatus}
	case lower == "/node" || strings.HasPrefix(lower, "/node "):
		parts := strings.Fields(msg)
		if len(parts) < 2 || parts[1] == "" {
			return Command{Kind: CommandSetNode, UsageErr: "usage: /node <id|alias>"}
		}
		return Command{Kind: CommandSetNode, Arg: parts[1]}
	case lower == "/session" || strings.HasPrefix(lower, "/session "):
		parts := strings.Fields(msg)
		if len(parts) < 2 || parts[1] == "" {
			return Command{Kind: CommandSetSession, UsageErr: "usage: /session <id>"}
		}
		return Command{Kind: CommandSetSession, Arg: parts[1]}
	case lower == "/background" || strings.HasPrefix(lower, "/background "):
		parts := strings.Fields(lower)
		if len(parts) < 2 || (parts[1] != "on" && parts[1] != "off") {
			return Command{Kind: CommandSetBackground, UsageErr: "usage: /background on|off"}
		}
		return Command{Kind: CommandSetBackground, Arg: parts[1]}
	default:
		return Command{Kind: CommandPrompt, Arg: msg}
	}
}

func PrintNodes(out io.Writer, nodes []edgeservice.NodeSnapshot, selectedRef string) {
	if len(nodes) == 0 {
		fmt.Fprintln(out, "no nodes connected")
		return
	}
	for _, node := range nodes {
		marker := "  "
		if selectedRef != "" && (node.NodeID == selectedRef || node.Alias == selectedRef || node.Label == selectedRef) {
			marker = "* "
		}
		fmt.Fprintf(out, "%s%s = %s (%s)\n", marker, node.Label, node.NodeID, node.Alias)
	}
}

func SendRun(ctx context.Context, edgeSvc *edgeservice.Service, events *EventRouter, out io.Writer, target *TargetState, message string) error {
	runID := edgeservice.NewRunID()
	var untrack func()
	if !target.Background {
		untrack = events.TrackForeground(runID)
		defer untrack()
	}

	handle, err := edgeSvc.SubmitRun(ctx, edgeservice.SubmitRunRequest{
		NodeRef:    target.NodeRef,
		RunID:      runID,
		Adapter:    target.Adapter,
		Target:     target.Target,
		SessionID:  target.SessionID,
		Prompt:     message,
		Background: target.Background,
		TimeoutSec: target.TimeoutSec,
		Metadata: map[string]string{
			"source": "edge-ops-console",
		},
	})
	if err != nil {
		return err
	}
	defer handle.Close()

	dispatch := handle.Dispatch()
	fmt.Fprintf(out, "[edge] sent run_id=%s node=%s adapter=%s target=%s session=%s background=%v\n",
		dispatch.RunID, dispatch.NodeLabel, dispatch.Adapter, dispatch.Target, dispatch.SessionID, dispatch.Background)

	if target.Background {
		fmt.Fprintf(out, "[edge] background run dispatched, events will arrive asynchronously\n")
		return nil
	}

	timer := time.NewTimer(handle.WaitTimeout())
	defer timer.Stop()

	var response *ResponseStream
	stream := handle.Stream()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-timer.C:
			return fmt.Errorf("timed out waiting for node response")
		case event := <-stream.NodeEvents:
			if edgeservice.IsNodeDisconnected(event) {
				return fmt.Errorf("node disconnected: %s", event.GetReason())
			}
		case event := <-stream.Events:
			if event.GetRunId() != dispatch.RunID {
				continue
			}
			label := events.NodeLabel(event)
			if response == nil {
				response = NewResponseStream(out, nodeMessagePrefix(label))
			}

			switch event.GetType() {
			case "start":
				fmt.Fprintf(out, "%s start run_id=%s\n", nodeEventPrefix(label), dispatch.RunID)
			case "reasoning_delta":
				writeReasoningDelta(out, label, event.GetDelta())
			case "delta":
				response.Write(event.GetDelta())
			case "complete":
				response.Finish()
				fmt.Fprintf(out, "%s complete run_id=%s detail=%q\n", nodeEventPrefix(label), dispatch.RunID, event.GetMessage())
				return nil
			case "cancelled":
				response.FinishIfStarted()
				fmt.Fprintf(out, "%s cancelled run_id=%s\n", nodeEventPrefix(label), dispatch.RunID)
				return nil
			case "error":
				response.FinishIfStarted()
				fmt.Fprintf(out, "%s error run_id=%s detail=%q\n", nodeEventPrefix(label), dispatch.RunID, event.GetError())
				return fmt.Errorf("node reported error")
			default:
				fmt.Fprintf(out, "%s %s run_id=%s detail=%q\n", nodeEventPrefix(label), event.GetType(), dispatch.RunID, event.GetMessage())
			}
		}
	}
}

func NormalizeSessionID(id string) string {
	return edgeservice.NormalizeSessionID(id)
}
