package bridge

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/daniel/cursor-adapter/internal/workspace"
)

// Bridge 定義與 Cursor CLI 的整合介面。
type Bridge interface {
	Execute(ctx context.Context, prompt string, model string, sessionKey string) (<-chan string, <-chan error)
	ExecuteSync(ctx context.Context, prompt string, model string, sessionKey string) (string, error)
	ListModels(ctx context.Context) ([]string, error)
	CheckHealth(ctx context.Context) error
}

// NewBridge 建立 Bridge。chatOnly=true 會讓每個子程序都跑在空的 temp
// workspace、並且用 env overrides 把 HOME / CURSOR_CONFIG_DIR 導到那個
// temp dir，讓 Cursor agent 讀不到任何真實專案檔案或全域 rules。
func NewBridge(cursorPath string, logger *slog.Logger, useACP bool, chatOnly bool, maxConcurrent int, timeout time.Duration) Bridge {
	if useACP {
		return NewACPBridge(cursorPath, logger, chatOnly, maxConcurrent, timeout)
	}
	return NewCLIBridge(cursorPath, chatOnly, maxConcurrent, timeout)
}

// --- CLI Bridge ---

type CLIBridge struct {
	cursorPath string
	semaphore  chan struct{}
	timeout    time.Duration
	chatOnly   bool
}

func buildCLICommandArgs(prompt, model, workspaceDir string, stream, chatOnly bool) []string {
	args := []string{"--print", "--mode", "ask"}
	if chatOnly {
		args = append(args, "--trust")
	}
	if workspaceDir != "" {
		args = append(args, "--workspace", workspaceDir)
	}
	if model != "" {
		args = append(args, "--model", model)
	}
	if stream {
		args = append(args, "--stream-partial-output", "--output-format", "stream-json")
	} else {
		args = append(args, "--output-format", "text")
	}
	args = append(args, prompt)
	return args
}

func NewCLIBridge(cursorPath string, chatOnly bool, maxConcurrent int, timeout time.Duration) *CLIBridge {
	if maxConcurrent <= 0 {
		maxConcurrent = 1
	}
	return &CLIBridge{
		cursorPath: cursorPath,
		semaphore:  make(chan struct{}, maxConcurrent),
		timeout:    timeout,
		chatOnly:   chatOnly,
	}
}

// prepareWorkspace returns (workspaceDir, envOverrides, cleanup). When
// chatOnly is enabled, workspaceDir is a fresh temp dir and cleanup removes
// it. Otherwise workspaceDir falls back to the adapter's cwd with no
// cleanup.
func (b *CLIBridge) prepareWorkspace() (string, map[string]string, func()) {
	if !b.chatOnly {
		ws, _ := os.Getwd()
		return ws, nil, func() {}
	}
	dir, env, err := workspace.ChatOnly("")
	if err != nil {
		slog.Warn("chat-only workspace setup failed, falling back to cwd", "err", err)
		ws, _ := os.Getwd()
		return ws, nil, func() {}
	}
	return dir, env, func() { _ = os.RemoveAll(dir) }
}

func (b *CLIBridge) Execute(ctx context.Context, prompt string, model string, sessionKey string) (<-chan string, <-chan error) {
	outputChan := make(chan string, 64)
	errChan := make(chan error, 1)

	go func() {
		defer close(outputChan)
		defer close(errChan)

		select {
		case b.semaphore <- struct{}{}:
			defer func() { <-b.semaphore }()
		case <-ctx.Done():
			errChan <- ctx.Err()
			return
		}

		execCtx, cancel := context.WithTimeout(ctx, b.timeout)
		defer cancel()

		ws, envOverrides, cleanup := b.prepareWorkspace()
		defer cleanup()
		cmd := exec.CommandContext(execCtx, b.cursorPath, buildCLICommandArgs(prompt, model, ws, true, b.chatOnly)...)
		cmd.Dir = ws
		cmd.Env = workspace.MergeEnv(os.Environ(), envOverrides)

		stdoutPipe, err := cmd.StdoutPipe()
		if err != nil {
			errChan <- fmt.Errorf("stdout pipe: %w", err)
			return
		}
		defer stdoutPipe.Close()

		if err := cmd.Start(); err != nil {
			errChan <- fmt.Errorf("start command: %w", err)
			return
		}
		scanner := bufio.NewScanner(stdoutPipe)
		for scanner.Scan() {
			line := scanner.Text()
			line = strings.TrimSpace(line)
			if line == "" {
				continue
			}
			outputChan <- line
		}

		if err := scanner.Err(); err != nil {
			errChan <- fmt.Errorf("stdout scanner: %w", err)
		}

		if err := cmd.Wait(); err != nil {
			errChan <- err
		}
	}()

	return outputChan, errChan
}

func (b *CLIBridge) ExecuteSync(ctx context.Context, prompt string, model string, sessionKey string) (string, error) {
	select {
	case b.semaphore <- struct{}{}:
		defer func() { <-b.semaphore }()
	case <-ctx.Done():
		return "", ctx.Err()
	}

	execCtx, cancel := context.WithTimeout(ctx, b.timeout)
	defer cancel()

	ws, envOverrides, cleanup := b.prepareWorkspace()
	defer cleanup()
	cmd := exec.CommandContext(execCtx, b.cursorPath, buildCLICommandArgs(prompt, model, ws, false, b.chatOnly)...)
	cmd.Dir = ws
	cmd.Env = workspace.MergeEnv(os.Environ(), envOverrides)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("run command: %w (stderr: %s)", err, strings.TrimSpace(stderr.String()))
	}
	return strings.TrimSpace(stdout.String()), nil
}

func (b *CLIBridge) ListModels(ctx context.Context) ([]string, error) {
	select {
	case b.semaphore <- struct{}{}:
		defer func() { <-b.semaphore }()
	case <-ctx.Done():
		return nil, ctx.Err()
	}

	cmd := exec.CommandContext(ctx, b.cursorPath, "models")
	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("list models: %w", err)
	}
	return parseModelsOutput(string(output)), nil
}

func (b *CLIBridge) CheckHealth(ctx context.Context) error {
	cmd := exec.CommandContext(ctx, b.cursorPath, "--version")
	_, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("health check: %w", err)
	}
	return nil
}

var ansiEscapeRe = regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z]`)

func parseModelsOutput(output string) []string {
	clean := ansiEscapeRe.ReplaceAllString(output, "")
	var models []string
	for _, line := range strings.Split(clean, "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		if strings.Contains(line, "Loading") || strings.Contains(line, "Available") ||
			strings.Contains(line, "Tip:") || strings.Contains(line, "Error") {
			continue
		}
		id := line
		if idx := strings.Index(line, " - "); idx > 0 {
			id = line[:idx]
		}
		id = strings.TrimSpace(id)
		if id != "" {
			models = append(models, id)
		}
	}
	return models
}

// --- ACP Bridge (per-request 完整流程，參考 cursor-api-proxy) ---

type ACPBridge struct {
	cursorPath string
	logger     *slog.Logger
	timeout    time.Duration
	chatOnly   bool
	workers    []*acpWorker
	nextWorker atomic.Uint32
	sessionsMu sync.Mutex
	sessions   map[string]acpSessionHandle
	sessionTTL time.Duration
}

type acpSessionHandle struct {
	WorkerIndex int
	SessionID   string
	Model       string
	Generation  uint64
	LastUsedAt  time.Time
}

func NewACPBridge(cursorPath string, logger *slog.Logger, chatOnly bool, maxConcurrent int, timeout time.Duration) *ACPBridge {
	if maxConcurrent <= 0 {
		maxConcurrent = 1
	}
	bridge := &ACPBridge{
		cursorPath: cursorPath,
		logger:     logger,
		timeout:    timeout,
		chatOnly:   chatOnly,
		sessions:   make(map[string]acpSessionHandle),
		sessionTTL: 30 * time.Minute,
	}
	for i := 0; i < maxConcurrent; i++ {
		bridge.workers = append(bridge.workers, newACPWorker(cursorPath, logger, chatOnly, timeout))
	}
	return bridge
}

func buildACPCommandArgs(workspace, model string) []string {
	args := []string{"--workspace", workspace}
	if model != "" && model != "auto" && model != "default" {
		args = append(args, "--model", model)
	}
	args = append(args, "acp")
	return args
}

// acpMessage 定義 ACP JSON-RPC message 格式。
type acpMessage struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      *int            `json:"id,omitempty"`
	Method  string          `json:"method,omitempty"`
	Params  json.RawMessage `json:"params,omitempty"`
	Result  json.RawMessage `json:"result,omitempty"`
	Error   *acpError       `json:"error,omitempty"`
}

type acpError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

type acpSession struct {
	SessionID string `json:"sessionId"`
}

type acpResponse struct {
	result json.RawMessage
	err    error
}

type acpWorker struct {
	cursorPath string
	logger     *slog.Logger
	timeout    time.Duration
	chatOnly   bool

	reqMu sync.Mutex

	writeMu sync.Mutex
	stateMu sync.Mutex

	workspace    string
	envOverrides map[string]string
	currentModel string
	cmd          *exec.Cmd
	stdin        io.WriteCloser
	pending      map[int]chan acpResponse
	nextID       int
	readerErr    error
	readerDone   chan struct{}
	activeSink   func(string)
	generation   atomic.Uint64
}

func newACPWorker(cursorPath string, logger *slog.Logger, chatOnly bool, timeout time.Duration) *acpWorker {
	return &acpWorker{
		cursorPath: cursorPath,
		logger:     logger,
		timeout:    timeout,
		chatOnly:   chatOnly,
	}
}

func (b *ACPBridge) Execute(ctx context.Context, prompt string, model string, sessionKey string) (<-chan string, <-chan error) {
	outputChan := make(chan string, 64)
	errChan := make(chan error, 1)

	go func() {
		defer close(outputChan)
		defer close(errChan)

		worker, sessionID := b.resolveSession(sessionKey, model)
		finalSessionID, err := worker.run(ctx, prompt, model, sessionID, func(text string) {
			if text == "" {
				return
			}
			select {
			case outputChan <- text:
			case <-ctx.Done():
			}
		})
		if err == nil {
			b.storeSession(sessionKey, model, worker, finalSessionID)
		}
		if err != nil {
			errChan <- err
		}
	}()

	return outputChan, errChan
}

func (b *ACPBridge) ExecuteSync(ctx context.Context, prompt string, model string, sessionKey string) (string, error) {
	var content strings.Builder
	worker, sessionID := b.resolveSession(sessionKey, model)
	finalSessionID, err := worker.run(ctx, prompt, model, sessionID, func(text string) {
		content.WriteString(text)
	})
	if err != nil {
		return "", err
	}
	b.storeSession(sessionKey, model, worker, finalSessionID)
	return strings.TrimSpace(content.String()), nil
}

func (b *ACPBridge) pickWorker() *acpWorker {
	if len(b.workers) == 0 {
		return newACPWorker(b.cursorPath, b.logger, b.chatOnly, b.timeout)
	}
	idx := int(b.nextWorker.Add(1)-1) % len(b.workers)
	return b.workers[idx]
}

func (b *ACPBridge) resolveSession(sessionKey, model string) (*acpWorker, string) {
	normalizedModel := normalizeModel(model)
	if sessionKey == "" {
		return b.pickWorker(), ""
	}

	b.sessionsMu.Lock()
	defer b.sessionsMu.Unlock()
	b.cleanupExpiredSessionsLocked()

	handle, ok := b.sessions[sessionKey]
	if !ok {
		return b.pickWorker(), ""
	}
	if handle.Model != normalizedModel {
		delete(b.sessions, sessionKey)
		return b.workers[handle.WorkerIndex], ""
	}
	if handle.WorkerIndex < 0 || handle.WorkerIndex >= len(b.workers) {
		delete(b.sessions, sessionKey)
		return b.pickWorker(), ""
	}
	worker := b.workers[handle.WorkerIndex]
	if worker.Generation() != handle.Generation {
		delete(b.sessions, sessionKey)
		return worker, ""
	}

	handle.LastUsedAt = time.Now()
	b.sessions[sessionKey] = handle
	return worker, handle.SessionID
}

func (b *ACPBridge) storeSession(sessionKey, model string, worker *acpWorker, sessionID string) {
	if sessionKey == "" || sessionID == "" {
		return
	}
	workerIndex := -1
	for i, candidate := range b.workers {
		if candidate == worker {
			workerIndex = i
			break
		}
	}
	if workerIndex == -1 {
		return
	}

	b.sessionsMu.Lock()
	defer b.sessionsMu.Unlock()
	b.cleanupExpiredSessionsLocked()
	b.sessions[sessionKey] = acpSessionHandle{
		WorkerIndex: workerIndex,
		SessionID:   sessionID,
		Model:       normalizeModel(model),
		Generation:  worker.Generation(),
		LastUsedAt:  time.Now(),
	}
}

func (b *ACPBridge) cleanupExpiredSessionsLocked() {
	if b.sessionTTL <= 0 {
		return
	}
	cutoff := time.Now().Add(-b.sessionTTL)
	for key, handle := range b.sessions {
		if handle.LastUsedAt.Before(cutoff) {
			delete(b.sessions, key)
		}
	}
}

func (b *ACPBridge) ListModels(ctx context.Context) ([]string, error) {
	cmd := exec.CommandContext(ctx, b.cursorPath, "models")
	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("list models: %w", err)
	}
	return parseModelsOutput(string(output)), nil
}

func (b *ACPBridge) CheckHealth(ctx context.Context) error {
	cmd := exec.CommandContext(ctx, b.cursorPath, "--version")
	_, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("health check: %w", err)
	}
	return nil
}

// sendACP 送 JSON-RPC request。
func sendACP(stdin io.Writer, id int, method string, params interface{}) error {
	msg := acpMessage{
		JSONRPC: "2.0",
		ID:      &id,
		Method:  method,
	}
	if params != nil {
		data, _ := json.Marshal(params)
		msg.Params = data
	}

	data, _ := json.Marshal(msg)
	_, err := fmt.Fprintf(stdin, "%s\n", data)
	return err
}

func respondACP(stdin io.Writer, id int, result interface{}) error {
	msg := map[string]interface{}{
		"jsonrpc": "2.0",
		"id":      id,
		"result":  result,
	}
	data, _ := json.Marshal(msg)
	_, err := fmt.Fprintf(stdin, "%s\n", data)
	return err
}

func extractACPText(params json.RawMessage) string {
	var update map[string]interface{}
	_ = json.Unmarshal(params, &update)

	updateMap, _ := update["update"].(map[string]interface{})
	sessionUpdate, _ := updateMap["sessionUpdate"].(string)
	if sessionUpdate != "" && !strings.HasPrefix(sessionUpdate, "agent_message") {
		return ""
	}
	content, _ := updateMap["content"].(interface{})

	switch v := content.(type) {
	case map[string]interface{}:
		if t, ok := v["text"].(string); ok {
			return t
		}
	case []interface{}:
		var parts []string
		for _, item := range v {
			if m, ok := item.(map[string]interface{}); ok {
				if nested, ok := m["content"].(map[string]interface{}); ok {
					if t, ok := nested["text"].(string); ok {
						parts = append(parts, t)
						continue
					}
				}
				if t, ok := m["text"].(string); ok {
					parts = append(parts, t)
				}
			}
		}
		return strings.Join(parts, "")
	case string:
		return v
	}
	return ""
}

func (w *acpWorker) Generation() uint64 {
	return w.generation.Load()
}

func normalizeModel(m string) string {
	m = strings.TrimSpace(m)
	if m == "" || m == "default" {
		return "auto"
	}
	return m
}

func (w *acpWorker) run(ctx context.Context, prompt string, model string, sessionID string, sink func(string)) (string, error) {
	w.reqMu.Lock()
	defer w.reqMu.Unlock()

	t0 := time.Now()
	wantModel := normalizeModel(model)

	if w.cmd != nil && w.currentModel != wantModel {
		slog.Debug("acp: model changed, restarting worker", "from", w.currentModel, "to", wantModel)
		w.resetLocked()
		sessionID = ""
	}

	if err := w.ensureStartedLocked(ctx, wantModel); err != nil {
		return "", err
	}
	slog.Debug("acp: ensureStarted", "model", wantModel, "elapsed", time.Since(t0))

	newSession := sessionID == ""
	if newSession {
		var err error
		sessionID, err = w.createSessionLocked(ctx)
		if err != nil {
			w.resetLocked()
			return "", err
		}
		slog.Debug("acp: session created", "elapsed", time.Since(t0))
	}

	w.setActiveSinkLocked(sink)
	defer w.setActiveSinkLocked(nil)

	slog.Debug("acp: sending prompt", "newSession", newSession, "elapsed", time.Since(t0))
	if _, err := w.sendRequestLocked(ctx, "session/prompt", map[string]interface{}{
		"sessionId": sessionID,
		"prompt": []interface{}{map[string]interface{}{
			"type": "text",
			"text": prompt,
		}},
	}); err != nil {
		w.resetLocked()
		return "", err
	}
	slog.Debug("acp: prompt complete", "elapsed", time.Since(t0))

	return sessionID, nil
}

func (w *acpWorker) ensureStartedLocked(ctx context.Context, model string) error {
	if w.cmd != nil {
		return nil
	}

	if w.workspace == "" {
		var (
			dir string
			env map[string]string
			err error
		)
		if w.chatOnly {
			dir, env, err = workspace.ChatOnly("")
			if err != nil {
				return fmt.Errorf("chat-only workspace: %w", err)
			}
		} else {
			dir, err = os.MkdirTemp("", "cursor-acp-worker-*")
			if err != nil {
				return fmt.Errorf("temp workspace: %w", err)
			}
		}
		w.workspace = dir
		w.envOverrides = env
	}

	w.currentModel = model
	cmd := exec.Command(w.cursorPath, buildACPCommandArgs(w.workspace, model)...)
	cmd.Dir = w.workspace
	cmd.Env = workspace.MergeEnv(os.Environ(), w.envOverrides)

	stdin, err := cmd.StdinPipe()
	if err != nil {
		return fmt.Errorf("stdin pipe: %w", err)
	}

	stdoutPipe, err := cmd.StdoutPipe()
	if err != nil {
		_ = stdin.Close()
		return fmt.Errorf("stdout pipe: %w", err)
	}

	if err := cmd.Start(); err != nil {
		_ = stdin.Close()
		_ = stdoutPipe.Close()
		return fmt.Errorf("start acp: %w", err)
	}

	w.cmd = cmd
	w.stdin = stdin
	w.pending = make(map[int]chan acpResponse)
	w.nextID = 1
	w.readerDone = make(chan struct{})
	w.readerErr = nil
	w.generation.Add(1)
	go w.readLoop(stdoutPipe)

	if _, err := w.sendRequestLocked(ctx, "initialize", map[string]interface{}{
		"protocolVersion": 1,
		"clientCapabilities": map[string]interface{}{
			"promptCapabilities": map[string]interface{}{"text": true},
			"fs":                 map[string]interface{}{"readTextFile": false, "writeTextFile": false},
			"terminal":           false,
		},
		"clientInfo": map[string]interface{}{
			"name":    "cursor-adapter",
			"version": "0.2.0",
		},
	}); err != nil {
		return fmt.Errorf("initialize: %w", err)
	}

	if _, err := w.sendRequestLocked(ctx, "authenticate", map[string]interface{}{
		"methodId": "cursor_login",
	}); err != nil {
		return fmt.Errorf("authenticate: %w", err)
	}

	return nil
}

func (w *acpWorker) createSessionLocked(ctx context.Context) (string, error) {
	resp, err := w.sendRequestLocked(ctx, "session/new", map[string]interface{}{
		"cwd":        w.workspace,
		"mcpServers": []interface{}{},
	})
	if err != nil {
		return "", fmt.Errorf("session/new: %w", err)
	}

	var session acpSession
	if err := json.Unmarshal(resp, &session); err != nil || session.SessionID == "" {
		return "", fmt.Errorf("session/new invalid response: %s", string(resp))
	}
	return session.SessionID, nil
}

func (w *acpWorker) setConfigLocked(ctx context.Context, sessionID, configID string, value interface{}) error {
	_, err := w.sendRequestLocked(ctx, "session/set_config_option", map[string]interface{}{
		"sessionId": sessionID,
		"configId":  configID,
		"value":     value,
	})
	if err != nil {
		return fmt.Errorf("session/set_config_option(%s=%v): %w", configID, value, err)
	}
	return nil
}

func (w *acpWorker) sendRequestLocked(ctx context.Context, method string, params interface{}) (json.RawMessage, error) {
	if w.stdin == nil {
		return nil, fmt.Errorf("acp stdin unavailable")
	}

	id := w.nextID
	w.nextID++
	respCh := make(chan acpResponse, 1)

	w.stateMu.Lock()
	w.pending[id] = respCh
	readerDone := w.readerDone
	w.stateMu.Unlock()

	if err := w.writeJSONRPCLocked(id, method, params); err != nil {
		w.removePending(id)
		return nil, err
	}

	timer := time.NewTimer(w.timeout)
	defer timer.Stop()

	select {
	case resp := <-respCh:
		return resp.result, resp.err
	case <-ctx.Done():
		w.removePending(id)
		return nil, ctx.Err()
	case <-readerDone:
		return nil, w.getReaderErr()
	case <-timer.C:
		w.removePending(id)
		return nil, fmt.Errorf("acp %s timed out after %s", method, w.timeout)
	}
}

func (w *acpWorker) writeJSONRPCLocked(id int, method string, params interface{}) error {
	w.writeMu.Lock()
	defer w.writeMu.Unlock()
	return sendACP(w.stdin, id, method, params)
}

func (w *acpWorker) readLoop(stdout io.ReadCloser) {
	defer stdout.Close()

	scanner := bufio.NewScanner(stdout)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}

		var msg acpMessage
		if err := json.Unmarshal([]byte(line), &msg); err != nil {
			continue
		}

		if msg.ID != nil && (msg.Result != nil || msg.Error != nil) {
			w.deliverResponse(*msg.ID, msg)
			continue
		}

		w.handleACPNotification(msg)
	}

	err := scanner.Err()
	w.stateMu.Lock()
	w.readerErr = err
	done := w.readerDone
	pending := w.pending
	w.pending = make(map[int]chan acpResponse)
	w.stateMu.Unlock()

	for _, ch := range pending {
		ch <- acpResponse{err: w.getReaderErr()}
		close(ch)
	}

	if w.cmd != nil {
		_ = w.cmd.Wait()
	}
	if done != nil {
		close(done)
	}
}

func (w *acpWorker) deliverResponse(id int, msg acpMessage) {
	w.stateMu.Lock()
	ch := w.pending[id]
	delete(w.pending, id)
	w.stateMu.Unlock()
	if ch == nil {
		return
	}

	resp := acpResponse{result: msg.Result}
	if msg.Error != nil {
		resp.err = fmt.Errorf("%s", msg.Error.Message)
	}
	ch <- resp
	close(ch)
}

func (w *acpWorker) handleACPNotification(msg acpMessage) bool {
	switch msg.Method {
	case "session/update":
		text := extractACPText(msg.Params)
		if text != "" {
			w.stateMu.Lock()
			sink := w.activeSink
			w.stateMu.Unlock()
			if sink != nil {
				sink(text)
			}
		}
		return true
	case "session/request_permission":
		if msg.ID != nil {
			w.writeMu.Lock()
			_ = respondACP(w.stdin, *msg.ID, map[string]interface{}{
				"outcome": map[string]interface{}{
					"outcome":  "selected",
					"optionId": "reject-once",
				},
			})
			w.writeMu.Unlock()
		}
		return true
	}

	if msg.ID != nil && strings.HasPrefix(msg.Method, "cursor/") {
		var params map[string]interface{}
		_ = json.Unmarshal(msg.Params, &params)

		w.writeMu.Lock()
		defer w.writeMu.Unlock()

		switch msg.Method {
		case "cursor/ask_question":
			selectedID := ""
			if options, ok := params["options"].([]interface{}); ok && len(options) > 0 {
				if first, ok := options[0].(map[string]interface{}); ok {
					if id, ok := first["id"].(string); ok {
						selectedID = id
					}
				}
			}
			_ = respondACP(w.stdin, *msg.ID, map[string]interface{}{"selectedId": selectedID})
		case "cursor/create_plan":
			_ = respondACP(w.stdin, *msg.ID, map[string]interface{}{"approved": true})
		default:
			_ = respondACP(w.stdin, *msg.ID, map[string]interface{}{})
		}
		return true
	}

	return false
}

func (w *acpWorker) setActiveSinkLocked(sink func(string)) {
	w.stateMu.Lock()
	w.activeSink = sink
	w.stateMu.Unlock()
}

func (w *acpWorker) removePending(id int) {
	w.stateMu.Lock()
	delete(w.pending, id)
	w.stateMu.Unlock()
}

func (w *acpWorker) getReaderErr() error {
	w.stateMu.Lock()
	defer w.stateMu.Unlock()
	if w.readerErr != nil {
		return w.readerErr
	}
	return fmt.Errorf("acp process exited")
}

func (w *acpWorker) resetLocked() {
	w.stateMu.Lock()
	cmd := w.cmd
	stdin := w.stdin
	done := w.readerDone
	w.cmd = nil
	w.stdin = nil
	w.pending = make(map[int]chan acpResponse)
	w.nextID = 1
	w.readerDone = nil
	w.readerErr = nil
	w.activeSink = nil
	w.stateMu.Unlock()

	w.generation.Add(1)

	if stdin != nil {
		_ = stdin.Close()
	}
	if cmd != nil && cmd.Process != nil {
		_ = cmd.Process.Kill()
	}
	if done != nil {
		select {
		case <-done:
		case <-time.After(2 * time.Second):
		}
	}

	if w.workspace != "" {
		_ = os.RemoveAll(w.workspace)
		w.workspace = ""
	}
	w.envOverrides = nil
}
