package cmd

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"os/signal"
	"strings"
	"sync"
	"time"

	"github.com/keepmind9/acp-sdk-go/client"
	"github.com/keepmind9/acp-sdk-go/core"
	"github.com/keepmind9/acp-sdk-go/helpers"
	"github.com/keepmind9/acp-sdk-go/schema"
	"github.com/keepmind9/acp-sdk-go/transport"
	"github.com/spf13/cobra"
)

var (
	flagServer  string
	flagWorkDir string
	flagEnv     []string
	flagName    string
)

func init() {
	RunCmd.Flags().StringVar(&flagServer, "server", "", "Server command to run (e.g. \"opencode --acp --model gpt-4\")")
	RunCmd.Flags().StringVar(&flagWorkDir, "work-dir", "", "Working directory for the server subprocess (default: current directory)")
	RunCmd.Flags().StringArrayVar(&flagEnv, "env", nil, "Environment variables for the server (KEY=VALUE)")
	RunCmd.Flags().StringVar(&flagName, "name", "acp-cli", "Client name announced during handshake")
	RunCmd.MarkFlagRequired("server")
}

var RunCmd = &cobra.Command{
	Use:   "run",
	Short: "Start an ACP server and connect an interactive client to it",
	Long: `Spawns an ACP server subprocess and connects an interactive client to it.
The client sends user input to the agent and displays responses.

The --work-dir flag sets both the subprocess working directory and the Cwd
sent to the agent in the new_session request.

Examples:
  acp-cli run --server ./examples/agent
  acp-cli run --server claude
  acp-cli run --server "opencode --acp" --work-dir /project --env API_KEY=xxx --env DEBUG=true`,
	Args: cobra.NoArgs,
	RunE: run,
}

func run(cmd *cobra.Command, args []string) error {
	serverCmd, serverArgs, err := parseServerCommand(flagServer)
	if err != nil {
		return fmt.Errorf("--server: %w", err)
	}

	opts := []transport.SpawnOption{transport.WithArgs(serverArgs...)}
	if flagWorkDir != "" {
		opts = append(opts, transport.WithCwd(flagWorkDir))
	}
	if len(flagEnv) > 0 {
		opts = append(opts, transport.WithEnv(parseEnv(flagEnv)))
	}

	subprocess, err := transport.Spawn(serverCmd, opts...)
	if err != nil {
		return fmt.Errorf("spawn server: %w", err)
	}
	defer subprocess.Close()

	impl := &interactiveClient{Base: &client.Base{}}
	conn := core.ConnectToAgent(impl, subprocess)
	go conn.ReceiveLoop()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt)
	go func() {
		<-sigChan
		fmt.Println("\nInterrupted.")
		conn.Close()
		os.Exit(0)
	}()

	initResp, err := conn.Initialize(&schema.InitializeRequest{
		ProtocolVersion:    ptrProtocolVersion(1),
		ClientCapabilities: &schema.ClientCapabilities{},
		ClientInfo:         &schema.Implementation{Name: flagName, Version: "0.1.0"},
	})
	if err != nil {
		return fmt.Errorf("initialize: %w", err)
	}
	fmt.Printf("Connected to %s (protocol v%d)\n\n", orUnknown(initResp.AgentInfo), *initResp.ProtocolVersion)

	session, err := conn.NewSession(&schema.NewSessionRequest{
		McpServers: []*schema.McpServer{},
	})
	if err != nil {
		return fmt.Errorf("new_session: %w", err)
	}
	sessionID := string(*session.SessionId)
	fmt.Printf("Session: %s\n\n", sessionID)

	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("> ")
	for scanner.Scan() {
		line := scanner.Text()

		select {
		case <-sigChan:
			fmt.Println("\nInterrupted.")
			return nil
		default:
		}

		if err := handleCommand(line, sessionID, conn, impl, cmd.OutOrStdout()); err != nil {
			if err == io.EOF {
				return nil
			}
			return err
		}
		fmt.Print("> ")
	}

	return nil
}

// parseServerCommand parses a server command string into a command and arguments.
// It handles double-quoted argument groups (e.g. "opencode --acp --model gpt-4").
func parseServerCommand(cmd string) (string, []string, error) {
	if cmd == "" {
		return "", nil, fmt.Errorf("server command cannot be empty")
	}
	args := splitShellArgs(cmd)
	return args[0], args[1:], nil
}

// splitShellArgs splits a string by spaces, respecting double-quoted segments.
// Does not handle escapes, single quotes, or dollar signs.
func splitShellArgs(s string) []string {
	var args []string
	var current strings.Builder
	inQuote := false

	for i := 0; i < len(s); i++ {
		ch := s[i]
		if ch == '"' {
			inQuote = !inQuote
			continue
		}
		if ch == ' ' && !inQuote {
			if current.Len() > 0 {
				args = append(args, current.String())
				current.Reset()
			}
			continue
		}
		current.WriteByte(ch)
	}
	if current.Len() > 0 {
		args = append(args, current.String())
	}
	return args
}

// parseEnv converts a list of KEY=VALUE strings into a map.
func parseEnv(envVars []string) map[string]string {
	result := make(map[string]string, len(envVars))
	for _, e := range envVars {
		if idx := strings.IndexByte(e, '='); idx > 0 {
			result[e[:idx]] = e[idx+1:]
		}
	}
	return result
}

func handleCommand(line, sessionID string, conn *client.ClientSideConnection, client *interactiveClient, w io.Writer) error {
	switch strings.TrimSpace(line) {
	case "", " ":
		return nil

	case ":exit", ":quit":
		return io.EOF

	case ":cancel":
		if err := conn.Cancel(sessionID); err != nil {
			fmt.Fprintf(w, "cancel failed: %v\n", err)
		}
		return nil

	case ":help":
		fmt.Fprintln(w, "Commands:")
		fmt.Fprintln(w, "  :exit, :quit  Exit the client")
		fmt.Fprintln(w, "  :cancel       Cancel the current prompt")
		fmt.Fprintln(w, "  :help         Show this help")
		return nil
	}

	// Reset thought buffer for new prompt.
	client.mu.Lock()
	client.thoughtBuf.Reset()
	client.firstChunk = time.Time{}
	client.firstThought = time.Time{}
	client.mu.Unlock()

	block := helpers.TextBlock(line)
	client.mu.Lock()
	client.promptSent = time.Now()
	client.mu.Unlock()
	fmt.Fprintf(os.Stderr, "[prompt t=0]\n")
	_, err := conn.Prompt(&schema.PromptRequest{
		SessionId: ptrSessionId(sessionID),
		Prompt:    []*schema.ContentBlock{&block},
	})

	// Flush any remaining thoughts in buffer.
	client.mu.Lock()
	if client.thoughtBuf.Len() > 0 {
		fmt.Printf("\n[...%s]\n", client.thoughtBuf.String())
		client.thoughtBuf.Reset()
	}
	client.mu.Unlock()

	if err != nil {
		fmt.Fprintf(w, "prompt failed: %v\n", err)
	}
	return nil
}

// interactiveClient implements the Client interface for interactive use.
// It only overrides SessionUpdate to display agent output; all other methods
// use the Base defaults (not supported / no-op).
type interactiveClient struct {
	*client.Base
	mu          sync.Mutex
	thoughtBuf  strings.Builder
	promptSent  time.Time
	firstChunk  time.Time
	firstThought time.Time
}

func (c *interactiveClient) SessionUpdate(_ context.Context, notif *schema.SessionNotification) error {
	switch notif.Update.SessionUpdate {
	case schema.SessionUpdateKindAgentMessageChunk:
		if notif.Update.AgentMessageChunk != nil {
			c.mu.Lock()
			defer c.mu.Unlock()
			if c.firstChunk.IsZero() {
				c.firstChunk = time.Now()
				fmt.Fprintf(os.Stderr, "[chunk t=%v]\n", time.Since(c.promptSent))
			}
			stripANSIStream(notif.Update.AgentMessageChunk.Content.Text.Text)
		}
	case schema.SessionUpdateKindAgentThoughtChunk:
		if notif.Update.AgentThoughtChunk != nil {
			c.mu.Lock()
			defer c.mu.Unlock()
			if c.firstChunk.IsZero() && c.firstThought.IsZero() {
				c.firstThought = time.Now()
			}
			c.thoughtBuf.WriteString(notif.Update.AgentThoughtChunk.Content.Text.Text)
		}
	case schema.SessionUpdateKindToolCall:
		if notif.Update.ToolCall != nil {
			title := notif.Update.ToolCall.Title
			if title == "" {
				title = "<tool>"
			}
			fmt.Printf("\n[%s]\n", title)
		}
	case schema.SessionUpdateKindToolCallUpdate:
		if notif.Update.ToolCallUpdate != nil {
			id := notif.Update.ToolCallUpdate.ToolCallId
			status := ""
			if notif.Update.ToolCallUpdate.Status != nil {
				status = string(*notif.Update.ToolCallUpdate.Status)
			}
			fmt.Printf("[tool] `%s` -> %s\n", *id, status)
		}
	case schema.SessionUpdateKindPlan:
		if notif.Update.Plan != nil && notif.Update.Plan.Entries != nil {
			fmt.Println("\n[plan]")
			for _, entry := range notif.Update.Plan.Entries {
				status := ""
				if entry.Status != nil {
					status = string(*entry.Status)
				}
				fmt.Printf("  %-10s %s\n", status, entry.Content)
			}
		}
	}
	return nil
}

func printContent(text string) {
	clean := stripANSI(text)
	fmt.Print(clean)
}

// stripANSIStream writes text to stdout, stripping ANSI escape sequences in-place
// without allocating intermediate strings. Flushes immediately for piped usage.
func stripANSIStream(s string) {
	out := make([]byte, 0, len(s))
	for i := 0; i < len(s); {
		if s[i] == '\x1b' {
			j := i + 1
			for j < len(s) && s[j] != 'm' {
				j++
			}
			if j < len(s) {
				j++
			}
			i = j
			continue
		}
		out = append(out, s[i])
		i++
	}
	fmt.Print(string(out))
	os.Stdout.Sync()
}

func stripANSI(s string) string {
	var result strings.Builder
	result.Grow(len(s))
	for i := 0; i < len(s); {
		if s[i] == '\x1b' {
			j := i + 1
			for j < len(s) && s[j] != 'm' {
				j++
			}
			if j < len(s) {
				j++
			}
			i = j
			continue
		}
		result.WriteByte(s[i])
		i++
	}
	return result.String()
}

func ptrSessionId(s string) *schema.SessionId { v := schema.SessionId(s); return &v }

func ptrProtocolVersion(v int) *schema.ProtocolVersion {
	pv := schema.ProtocolVersion(v)
	return &pv
}

func orUnknown(info *schema.Implementation) string {
	if info == nil || info.Name == "" {
		return "unknown"
	}
	return info.Name
}
