// Duet runs an ACP agent and client together in a single process,
// connecting them over stdio pipes. The client is interactive —
// type prompts at the console to send them to the agent.
package main

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"log"
	"os"
	"os/signal"
	"sync"

	"github.com/keepmind9/acp-sdk-go/client"
	"github.com/keepmind9/acp-sdk-go/helpers"
	"github.com/keepmind9/acp-sdk-go/rpc"
	"github.com/keepmind9/acp-sdk-go/schema"
	"github.com/keepmind9/acp-sdk-go/transport"
)

func ptrOutcome(o schema.RequestPermissionOutcome) *schema.RequestPermissionOutcome { return &o }
func ptrString(s string) *string                                                    { return &s }

type embeddedClient struct {
	mu sync.Mutex
}

func (c *embeddedClient) RequestPermission(_ context.Context, req *schema.RequestPermissionRequest) (*schema.RequestPermissionResponse, error) {
	title := "<permission>"
	if req.ToolCall != nil && req.ToolCall.Title != nil {
		title = *req.ToolCall.Title
	}
	fmt.Printf("\n🔐 Permission: %s\n", title)
	return &schema.RequestPermissionResponse{
		Outcome: ptrOutcome(schema.RequestPermissionOutcomeCancelled),
	}, nil
}

func (c *embeddedClient) SessionUpdate(_ context.Context, notif *schema.SessionNotification) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	switch notif.Update.SessionUpdate {
	case schema.SessionUpdateKindAgentMessageChunk:
		if notif.Update.AgentMessageChunk != nil && notif.Update.AgentMessageChunk.Content != nil {
			c.printContent(*notif.Update.AgentMessageChunk.Content, "Agent")
			fmt.Println()
		}
	case schema.SessionUpdateKindAgentThoughtChunk:
		if notif.Update.AgentThoughtChunk != nil && notif.Update.AgentThoughtChunk.Content != nil {
			fmt.Print("[thought] ")
			c.printContent(*notif.Update.AgentThoughtChunk.Content, "")
			fmt.Println()
		}
	case schema.SessionUpdateKindToolCall:
		if notif.Update.ToolCall != nil {
			fmt.Printf("\n🔧 %s\n", notif.Update.ToolCall.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("  → %s\n", status)
			_ = id
		}
	}
	return nil
}

func (c *embeddedClient) printContent(block schema.ContentBlock, label string) {
	switch {
	case block.Text != nil:
		if label != "" {
			fmt.Printf("\n%s: ", label)
		}
		fmt.Print(block.Text.Text)
	case block.Image != nil:
		fmt.Print("<image>")
	case block.Audio != nil:
		fmt.Print("<audio>")
	case block.Resource_link != nil:
		fmt.Printf("<resource: %s>", block.Resource_link.Uri)
	case block.Resource != nil:
		fmt.Print("<embedded resource>")
	}
}

func (c *embeddedClient) ReadTextFile(_ context.Context, _ *schema.ReadTextFileRequest) (*schema.ReadTextFileResponse, error) {
	return nil, rpc.NewRPCError(rpc.CodeMethodNotFound, "not implemented")
}
func (c *embeddedClient) WriteTextFile(_ context.Context, _ *schema.WriteTextFileRequest) (*schema.WriteTextFileResponse, error) {
	return nil, rpc.NewRPCError(rpc.CodeMethodNotFound, "not implemented")
}
func (c *embeddedClient) CreateTerminal(_ context.Context, _ *schema.CreateTerminalRequest) (*schema.CreateTerminalResponse, error) {
	return nil, rpc.NewRPCError(rpc.CodeMethodNotFound, "not implemented")
}
func (c *embeddedClient) TerminalOutput(_ context.Context, _ *schema.TerminalOutputRequest) (*schema.TerminalOutputResponse, error) {
	return nil, rpc.NewRPCError(rpc.CodeMethodNotFound, "not implemented")
}
func (c *embeddedClient) ReleaseTerminal(_ context.Context, _ *schema.ReleaseTerminalRequest) (*schema.ReleaseTerminalResponse, error) {
	return nil, rpc.NewRPCError(rpc.CodeMethodNotFound, "not implemented")
}
func (c *embeddedClient) WaitForTerminalExit(_ context.Context, _ *schema.WaitForTerminalExitRequest) (*schema.WaitForTerminalExitResponse, error) {
	return nil, rpc.NewRPCError(rpc.CodeMethodNotFound, "not implemented")
}
func (c *embeddedClient) KillTerminal(_ context.Context, _ *schema.KillTerminalRequest) (*schema.KillTerminalResponse, error) {
	return nil, rpc.NewRPCError(rpc.CodeMethodNotFound, "not implemented")
}

func main() {
	flag.Usage = func() {
		fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [flags] -- AGENT_PROGRAM [ARGS...]\n", os.Args[0])
		fmt.Fprintln(flag.CommandLine.Output())
		fmt.Fprintln(flag.CommandLine.Output(), "Spawns an ACP agent and connects an interactive client to it.")
		fmt.Fprintln(flag.CommandLine.Output(), "Commands:")
		fmt.Fprintln(flag.CommandLine.Output(), "  :exit  Exit the session")
		fmt.Fprintln(flag.CommandLine.Output(), "  :cancel  Cancel the current prompt")
	}
	flag.Parse()

	args := flag.Args()
	if len(args) == 0 {
		flag.Usage()
		os.Exit(2)
	}

	// Spawn the agent subprocess.
	subprocess, err := transport.Spawn(args[0], transport.WithArgs(args[1:]...))
	if err != nil {
		log.Fatalf("start agent: %v", err)
	}

	// Connect the client to the agent's stdio.
	impl := &embeddedClient{}
	conn := client.NewClientSideConnection(impl, subprocess.Stdout, subprocess.Stdin)

	// Start the receive loop first so it can read responses.
	go func() {
		conn.ReceiveLoop()
	}()

	protoVers := 1
	initResp, err := conn.Initialize(&schema.InitializeRequest{
		ProtocolVersion:    &protoVers,
		ClientCapabilities: &schema.ClientCapabilities{},
		ClientInfo:         &schema.Implementation{Name: "duet-client", Title: ptrString("Duet Client"), Version: "0.1.0"},
	})
	if err != nil {
		log.Fatalf("initialize: %v", err)
	}
	fmt.Printf("✅ Connected (protocol v%d)\n\n", *initResp.ProtocolVersion)

	session, err := conn.NewSession(&schema.NewSessionRequest{
		Cwd:        cwd(),
		McpServers: nil,
	})
	if err != nil {
		log.Fatalf("new_session: %v", err)
	}
	sessionID := *session.SessionId
	fmt.Printf("📝 Session: %s\n\n", sessionID)

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt)

	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("> ")
	for scanner.Scan() {
		line := scanner.Text()
		select {
		case <-sigChan:
			fmt.Println("\nInterrupted.")
			shutdown(subprocess, conn)
			return
		default:
		}

		if line == "" {
			fmt.Print("> ")
			continue
		}
		if line == ":exit" || line == ":quit" {
			shutdown(subprocess, conn)
			return
		}
		if line == ":cancel" {
			conn.Cancel(sessionID)
			fmt.Print("> ")
			continue
		}

		block := helpers.TextBlock(line)
		if _, err := conn.Prompt(&schema.PromptRequest{
			SessionId: session.SessionId,
			Prompt:    []*schema.ContentBlock{&block},
		}); err != nil {
			log.Printf("prompt failed: %v", err)
		}
		fmt.Print("\n> ")
	}

	conn.Close()
	shutdown(subprocess, conn)
}

func shutdown(subprocess *transport.Subprocess, conn *client.ClientSideConnection) {
	conn.Close()
	subprocess.Close()
}

func cwd() string {
	dir, _ := os.Getwd()
	if dir == "" {
		return "/tmp"
	}
	return dir
}
