package agent

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/pelletier/go-toml/v2"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/runtimeconfig"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/internal/core/rewindcaps"
)

// RestoreRecoveryPointDetached asks codex-app-server to restore a checkpoint without attaching to an active turn.
func (d *CodexAppServerDriver) RestoreRecoveryPointDetached(ctx context.Context, cfg drivercontract.LaunchConfig, strategy string, recoveryRef string) error {
	if _, err := decodeCodexRecoveryRefForRestore(strategy, recoveryRef); err != nil {
		return err
	}

	_, _, cleanup, wait, err := d.startTransport(ctx, cfg, io.Discard, nil)
	if err != nil {
		return err
	}
	defer func() {
		cleanup()
		logIgnoredCleanupError("failed waiting for detached codex recovery process cleanup", wait())
	}()

	return d.RestoreRecoveryPoint(ctx, strategy, recoveryRef)
}

// PrepareRecoveryPoint captures the current completed-turn boundary as a recovery point.
func (d *CodexAppServerDriver) PrepareRecoveryPoint() (string, string, bool, error) {
	d.mu.Lock()
	threadID := strings.TrimSpace(d.threadID)
	fallbackTurnCount := d.completedTurnCount
	d.mu.Unlock()

	if threadID == "" {
		return "", "", true, fmt.Errorf("codex thread is not initialized")
	}

	turnCount := fallbackTurnCount
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if detailed, err := d.readThread(ctx, threadID, true); err == nil {
		d.updateThreadSnapshot(detailed.Thread)
		turnCount = d.completedTurnsFromThread(detailed.Thread)
	}

	refBytes, err := rewindcaps.EncodeCodexAppServerRecoveryRef(threadID, turnCount)
	if err != nil {
		return "", "", true, fmt.Errorf("encode codex recovery ref: %w", err)
	}
	return codexAppServerRecoveryStrategy, refBytes, true, nil
}

// RestoreRecoveryPoint restores a previously captured recovery point.
func (d *CodexAppServerDriver) RestoreRecoveryPoint(ctx context.Context, strategy string, recoveryRef string) error {
	ref, err := decodeCodexRecoveryRefForRestore(strategy, recoveryRef)
	if err != nil {
		return err
	}

	var currentTurnCount int
	var threadStatus appServerThreadStatus
	var activeTurnID string
	var currentThread appServerThread
	// A fresh/detached app-server can read a persisted thread without loading it,
	// but mutating endpoints such as thread/rollback only accept a live thread in
	// Codex's ThreadManager. Do not use currentThreadState's startup-compatibility
	// result here: an empty primary id, or an authoritative notLoaded snapshot,
	// must be resumed before recovery can mutate the requested boundary.
	d.mu.Lock()
	loaded := strings.TrimSpace(d.threadID) == strings.TrimSpace(ref.ThreadID) &&
		strings.TrimSpace(d.threadID) != "" &&
		normalizeThreadStatus(d.threadStatus).Type != "notLoaded"
	d.mu.Unlock()

	if !loaded {
		resumed, resumeErr := d.resumeThreadForRecovery(ctx, d.currentLaunchConfig(), ref.ThreadID)
		if resumeErr != nil {
			return resumeErr
		}
		threadStatus = resumed.Thread.Status
		activeTurnID = d.currentActiveTurnID()
		currentTurnCount = d.completedTurnsFromThread(resumed.Thread)
		currentThread = resumed.Thread
	} else {
		readResult, readErr := d.readThread(ctx, ref.ThreadID, false)
		if readErr != nil {
			return readErr
		}
		d.updateThreadStatus(readResult.Thread.ID, readResult.Thread.Status)
		detailed, detailErr := d.readThread(ctx, ref.ThreadID, true)
		if detailErr != nil {
			return detailErr
		}
		d.updateThreadSnapshot(detailed.Thread)
		threadStatus, activeTurnID, _ = d.currentThreadState(ref.ThreadID)
		currentTurnCount = d.completedTurnsFromThread(detailed.Thread)
		currentThread = detailed.Thread
	}

	// Codex persists an interrupted turn asynchronously. A recent successful
	// InterruptActiveRun means both current<target and current==target can be a
	// stale history snapshot: the terminal interrupted turn may materialize one
	// read later and must then be included in the rollback diff. Never report a
	// no-op until that exact interrupt boundary is visible and inactive.
	if materialized, snapshot, waited, waitErr := d.waitForActiveInterruptTurnForRecovery(
		ctx, ref.ThreadID, ref.TurnCount, currentThread,
	); waitErr != nil {
		return waitErr
	} else if waited {
		// Once the exact interrupted turn is authoritative and terminal, the
		// snapshot's wait-only responsibility is complete. Clear it before the
		// rollback mutation: an ambiguous rollback may actually succeed, and a
		// retry must judge the new authoritative count instead of waiting forever
		// for the intentionally removed interrupted turn.
		if !d.clearActiveInterruptSnapshotIfCurrent(snapshot) {
			return fmt.Errorf("codex active interrupt snapshot for thread %s was superseded during recovery", ref.ThreadID)
		}
		currentThread = materialized
		threadStatus = materialized.Status
		activeTurnID = ""
		currentTurnCount = d.completedTurnsFromThread(materialized)
	}

	if currentTurnCount < ref.TurnCount {
		return &recoveryTargetAheadError{CurrentTurnCount: currentTurnCount, TargetTurnCount: ref.TurnCount}
	}
	if normalizeThreadStatus(threadStatus).Type == "active" {
		beforeCompletedTurnCount := currentTurnCount
		interruptedTurnID, intErr := d.interruptActiveThreadWithTarget(ctx, ref.ThreadID, activeTurnID)
		if intErr != nil {
			slog.Warn("interrupt before rollback failed",
				"driver", "codex-app-server",
				"thread_id", ref.ThreadID,
				"turn_id", activeTurnID,
				"error", intErr)
		}
		detailed, detailErr := d.readThread(ctx, ref.ThreadID, true)
		if detailErr != nil {
			return detailErr
		}
		d.updateThreadSnapshot(detailed.Thread)
		currentThread = detailed.Thread
		threadStatus, activeTurnID, _ = d.currentThreadState(ref.ThreadID)
		currentTurnCount = d.completedTurnsFromThread(detailed.Thread)
		if normalizeThreadStatus(threadStatus).Type == "active" {
			if strings.TrimSpace(activeTurnID) == "" {
				return fmt.Errorf("codex thread %s remains active after interrupt and active turn id is unknown", ref.ThreadID)
			}
			return fmt.Errorf("codex thread %s remains active after interrupt (turn %s)", ref.ThreadID, activeTurnID)
		}
		d.rememberActiveInterrupt(ref.ThreadID, beforeCompletedTurnCount, interruptedTurnID)
		materialized, snapshot, waited, waitErr := d.waitForActiveInterruptTurnForRecovery(
			ctx, ref.ThreadID, ref.TurnCount, currentThread,
		)
		if waitErr != nil {
			return waitErr
		}
		if !waited {
			return fmt.Errorf("codex interrupted turn for thread %s was not tracked for recovery", ref.ThreadID)
		}
		if !d.clearActiveInterruptSnapshotIfCurrent(snapshot) {
			return fmt.Errorf("codex active interrupt snapshot for thread %s was superseded during recovery", ref.ThreadID)
		}
		currentThread = materialized
		threadStatus = materialized.Status
		activeTurnID = ""
		currentTurnCount = d.completedTurnsFromThread(materialized)
	}
	if currentTurnCount < ref.TurnCount {
		return &recoveryTargetAheadError{CurrentTurnCount: currentTurnCount, TargetTurnCount: ref.TurnCount}
	}
	if currentTurnCount == ref.TurnCount {
		return nil
	}

	if err := d.rollbackThread(ctx, ref.ThreadID, currentTurnCount-ref.TurnCount); err != nil {
		return err
	}
	return nil
}

func decodeCodexRecoveryRefForRestore(strategy string, recoveryRef string) (rewindcaps.CodexAppServerRecoveryRef, error) {
	if strings.TrimSpace(strategy) != codexAppServerRecoveryStrategy {
		return rewindcaps.CodexAppServerRecoveryRef{}, fmt.Errorf("unsupported recovery strategy %q", strategy)
	}

	ref, err := rewindcaps.DecodeCodexAppServerRecoveryRef(recoveryRef)
	if err != nil {
		return rewindcaps.CodexAppServerRecoveryRef{}, fmt.Errorf("decode codex recovery ref: %w", err)
	}
	if strings.TrimSpace(ref.ThreadID) == "" {
		return rewindcaps.CodexAppServerRecoveryRef{}, fmt.Errorf("codex recovery ref is missing thread id")
	}
	if ref.TurnCount < 0 {
		return rewindcaps.CodexAppServerRecoveryRef{}, fmt.Errorf("codex recovery ref has invalid turn count")
	}
	return ref, nil
}

// ---------------------------------------------------------------------------
// Transport
// ---------------------------------------------------------------------------

func (d *CodexAppServerDriver) startTransport(ctx context.Context, cfg drivercontract.LaunchConfig, translatedWriter io.Writer, translatedConsumed <-chan struct{}) (*exec.Cmd, io.WriteCloser, func(), func() error, error) {
	binary := strings.TrimSpace(cfg.Binary)
	if binary == "" {
		binary = "codex"
	}

	args, err := codexAppServerArgs(cfg)
	if err != nil {
		return nil, nil, nil, nil, err
	}
	env := runtimeconfig.RuntimeEnvOverrides(cfg.Env, cfg.MCPs)
	args, env = applyCodexProviderConfig(args, env, cfg.Provider)

	cmd := runtimeconfig.RuntimeCommandContext(ctx, runtimeconfig.TypeCodex, binary, args)
	cmd.Dir = cfg.WorkDir

	// Set per-server bearer, extra HTTP header, and stdio env vars without
	// leaking secrets into runtime argv/config flags.
	env = addCodexMCPEnv(env, cfg.MCPs)
	cmd.Env = buildRuntimeEnvWithProvider(env, cfg.Provider)
	stderrWriter := &prefixWriter{prefix: fmt.Sprintf("[codex %s] ", cfg.AgentID)}
	cmd.Stderr = stderrWriter
	configureRuntimeCommand(cmd)

	stdoutPipe, err := cmd.StdoutPipe()
	if err != nil {
		return nil, nil, nil, nil, fmt.Errorf("codex: get stdout pipe: %w", err)
	}
	stdinPipe, err := cmd.StdinPipe()
	if err != nil {
		logIgnoredCleanupError("failed to close codex stdout pipe after stdin setup failure", stdoutPipe.Close())
		return nil, nil, nil, nil, fmt.Errorf("codex: get stdin pipe: %w", err)
	}

	generation, translatedLines, translatedOutput := d.installTransportGeneration(cfg, stdinPipe)

	// stderr callbacks are generation-bound just like stdout frames. A late
	// configuration error from an old child must not enter the replacement
	// generation's translated output queue.
	stderrWriter.onLine = func(line string) {
		d.handleCodexAppServerStderrLineForGeneration(generation, line)
	}
	translatedOutput.attachWriter(translatedWriter)

	if err := cmd.Start(); err != nil {
		d.closeTranslatedLinesForGeneration(generation, translatedLines)
		logIgnoredCleanupError("failed to close codex stdout pipe after start failure", stdoutPipe.Close())
		logIgnoredCleanupError("failed to close codex stdin pipe after start failure", stdinPipe.Close())
		return nil, nil, nil, nil, fmt.Errorf("codex: start: %w", err)
	}

	transportFailure := &codexAppServerTransportFailure{
		abort: func() {
			terminateRuntimeCommand(cmd)
		},
	}
	d.mu.Lock()
	if d.launchGeneration != generation {
		d.mu.Unlock()
		transportFailure.beginShutdown()
		logIgnoredCleanupError("failed to close superseded codex stdout pipe", stdoutPipe.Close())
		logIgnoredCleanupError("failed to close superseded codex stdin pipe", stdinPipe.Close())
		transportFailure.terminate()
		go func() {
			logIgnoredCleanupError("failed waiting for superseded codex process", cmd.Wait())
		}()
		return nil, nil, nil, nil, fmt.Errorf("codex app-server launch was superseded before transport attach")
	}
	d.transportFailure = transportFailure
	d.mu.Unlock()
	translatedOutput.setFatalHandler(func(outputErr error) {
		go d.failTransport(generation, transportFailure, outputErr.Error())
	})

	go d.flushTranslatedOutput(translatedOutput, translatedWriter)
	readLoopDone := make(chan struct{})
	go func() {
		defer close(readLoopDone)
		d.readLoopForGeneration(stdoutPipe, generation, transportFailure)
	}()

	var cleanupOnce sync.Once
	cleanup := func() {
		cleanupOnce.Do(func() {
			transportFailure.beginShutdown()
			translatedOutput.cancel()
			logIgnoredCleanupError("failed to close codex stdin pipe during cleanup", d.closeRawStdinForGeneration(generation, stdinPipe))
			logIgnoredCleanupError("failed to close codex stdout pipe during cleanup", stdoutPipe.Close())
			transportFailure.terminate()
		})
	}
	wait := func() error {
		return waitForCodexAppServerTransport(transportFailure, readLoopDone, translatedOutput, translatedConsumed, cmd.Wait)
	}

	if _, err := d.request(ctx, "initialize", map[string]any{
		"clientInfo": map[string]any{
			"name":    "agentteam",
			"version": "0.0.0",
		},
		"capabilities": map[string]any{
			"experimentalApi": true,
		},
	}); err != nil {
		cleanup()
		go func() {
			logIgnoredCleanupError("failed waiting for codex process after initialization failure", wait())
		}()
		return nil, nil, nil, nil, err
	}
	// Bind the initialized notification to the exact generation. A concurrent
	// replacement after initialize must not let this old launch write its final
	// handshake frame into the new child.
	if err := d.notifyForGeneration(generation, stdinPipe, "initialized", map[string]any{}); err != nil {
		cleanup()
		go func() {
			logIgnoredCleanupError("failed waiting for codex process after initialized notification failure", wait())
		}()
		return nil, nil, nil, nil, err
	}

	stdin := &lockedDriverWriter{driver: d, generation: generation, writer: stdinPipe}
	return cmd, stdin, cleanup, wait, nil
}

// waitForCodexAppServerTransport preserves the same ordering as Codex TUI's
// event agentteam: consume the complete ordered stdout stream, then publish every
// admitted translated event, and only then expose process exit to Process.
// Otherwise cmd.Wait can win the scheduler race against a final turn/completed
// frame and make AgentTeam fail/restart the wake before its terminal event is seen.
// Explicit cleanup closes the translated writer, so a missing consumer still
// releases the flusher and this wait.
func waitForCodexAppServerTransport(
	failure *codexAppServerTransportFailure,
	readLoopDone <-chan struct{},
	output *codexTranslatedOutput,
	translatedConsumed <-chan struct{},
	wait func() error,
) error {
	if readLoopDone != nil {
		<-readLoopDone
	}
	output.waitFlushed()
	if translatedConsumed != nil {
		select {
		case <-translatedConsumed:
		case <-failure.shutdownSignal():
			// Explicit shutdown is allowed to abandon Process consumption. Cleanup
			// already canceled the writer, so this also preserves no-consumer
			// termination without weakening unexpected-exit ordering.
		}
	}
	return failure.wait(wait)
}

// installTransportGeneration performs the only production app-server state
// swap. It intentionally aborts old output before requesting the generation
// write fence, so a full lossless output queue cannot deadlock replacement.
func (d *CodexAppServerDriver) installTransportGeneration(
	cfg drivercontract.LaunchConfig,
	stdinPipe io.WriteCloser,
) (uint64, chan string, *codexTranslatedOutput) {
	d.transportResetMu.Lock()
	defer d.transportResetMu.Unlock()

	d.mu.Lock()
	outputToAbort := d.translatedOutput
	d.mu.Unlock()
	if outputToAbort != nil {
		outputToAbort.abort()
	}

	d.transportGenerationMu.Lock()
	d.tokenUsageMu.Lock()
	d.mu.Lock()
	retiredTransport := d.snapshotTransportForRetirementLocked()
	d.resetAppServerStateForLaunchLocked(cfg, stdinPipe)
	generation := d.launchGeneration
	translatedLines := d.translatedLines
	translatedOutput := d.translatedOutput
	d.mu.Unlock()
	d.tokenUsageMu.Unlock()
	retiredTransport.retire("codex app-server transport was superseded by a newer launch")
	d.transportGenerationMu.Unlock()

	return generation, translatedLines, translatedOutput
}

const (
	codexAppServerSandboxModeWorkspaceWrite = "workspace-write"
	codexAppServerSandboxModeFullAccess     = "danger-full-access"

	codexAppServerSandboxPolicyTypeWorkspaceWrite = "workspaceWrite"
	codexAppServerSandboxPolicyTypeFullAccess     = "dangerFullAccess"
)

func codexAppServerSandboxMode(cfg drivercontract.LaunchConfig) (string, error) {
	mode, err := cfg.EffectivePermissionMode()
	if err != nil {
		return "", err
	}
	switch mode {
	case types.AgentPermissionModeWorkspaceWrite:
		return codexAppServerSandboxModeWorkspaceWrite, nil
	case types.AgentPermissionModeFullAccess:
		return codexAppServerSandboxModeFullAccess, nil
	default:
		return "", fmt.Errorf("unsupported agent permission mode %q", mode)
	}
}

func codexAppServerSandboxPolicy(cfg drivercontract.LaunchConfig) (map[string]any, error) {
	mode, err := cfg.EffectivePermissionMode()
	if err != nil {
		return nil, err
	}
	switch mode {
	case types.AgentPermissionModeWorkspaceWrite:
		workDir := strings.TrimSpace(cfg.WorkDir)
		if workDir == "" {
			return nil, fmt.Errorf("workspace-write permission mode requires an agent workdir")
		}
		return map[string]any{
			"type":          codexAppServerSandboxPolicyTypeWorkspaceWrite,
			"writableRoots": []string{workDir},
		}, nil
	case types.AgentPermissionModeFullAccess:
		return map[string]any{"type": codexAppServerSandboxPolicyTypeFullAccess}, nil
	default:
		return nil, fmt.Errorf("unsupported agent permission mode %q", mode)
	}
}

func codexReasoningSummary(env map[string]string) string {
	if summary := codexReasoningSummaryFromEnv(env); summary != "" {
		return summary
	}
	if summary := codexReasoningSummaryFromConfig(env); summary != "" {
		return summary
	}
	return "auto"
}

func codexReasoningSummaryFromEnv(env map[string]string) string {
	return normalizeCodexReasoningSummary(env["AGENTTEAM_CODEX_REASONING_SUMMARY"])
}

func codexReasoningSummaryFromConfig(env map[string]string) string {
	path := codexConfigPath(env)
	if path == "" {
		return ""
	}

	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}

	var config struct {
		ModelReasoningSummary string `toml:"model_reasoning_summary"`
	}
	if err := toml.Unmarshal(data, &config); err != nil {
		return ""
	}
	return normalizeCodexReasoningSummary(config.ModelReasoningSummary)
}

func codexConfigPath(env map[string]string) string {
	if codexHome := strings.TrimSpace(env["CODEX_HOME"]); codexHome != "" {
		return filepath.Join(codexHome, "config.toml")
	}
	if codexHome := strings.TrimSpace(os.Getenv("CODEX_HOME")); codexHome != "" {
		return filepath.Join(codexHome, "config.toml")
	}

	home := strings.TrimSpace(env["HOME"])
	if home == "" {
		home, _ = os.UserHomeDir()
	}
	if home == "" {
		return ""
	}
	return filepath.Join(home, ".codex", "config.toml")
}

func normalizeCodexReasoningSummary(value string) string {
	switch strings.TrimSpace(value) {
	case "auto", "concise", "detailed", "none":
		return strings.TrimSpace(value)
	default:
		return ""
	}
}

func codexShowRawReasoningFromEnv(env map[string]string) bool {
	switch strings.ToLower(strings.TrimSpace(env["AGENTTEAM_CODEX_SHOW_RAW_REASONING"])) {
	case "1", "true", "yes", "on":
		return true
	default:
		return false
	}
}

func codexAppServerArgs(cfg drivercontract.LaunchConfig) ([]string, error) {
	sandboxMode, err := codexAppServerSandboxMode(cfg)
	if err != nil {
		return nil, err
	}

	args := []string{
		"app-server",
		"--listen",
		"stdio://",
		"-c",
		fmt.Sprintf("sandbox_mode=%q", sandboxMode),
	}
	if sandboxMode == codexAppServerSandboxModeFullAccess {
		args = append([]string{"--dangerously-bypass-approvals-and-sandbox"}, args...)
	}

	if strings.TrimSpace(cfg.SmallModel) != "" {
		args = append(args, "-c", fmt.Sprintf("model_small=%q", strings.TrimSpace(cfg.SmallModel)))
	}
	if cfg.DisableNativeTools {
		args = append(args, codexNativeToolDisableArgs()...)
	}

	// Configure MCP servers via CLI flags.
	for _, m := range cfg.MCPs {
		switch m.EffectiveTransport() {
		case types.MCPTransportStdio:
			if strings.TrimSpace(m.Command) == "" {
				continue
			}
			args = append(args, "-c", fmt.Sprintf("mcp_servers.%s.command=%q", m.Name, strings.TrimSpace(m.Command)))
			if len(m.Args) > 0 {
				args = append(args, "-c", fmt.Sprintf("mcp_servers.%s.args=%s", m.Name, codexTOMLStringArray(m.Args)))
			}
			if strings.TrimSpace(m.CWD) != "" {
				args = append(args, "-c", fmt.Sprintf("mcp_servers.%s.cwd=%q", m.Name, strings.TrimSpace(m.CWD)))
			}
			for _, key := range sortedMapKeys(m.Env) {
				envVar := mcpStdioEnvVarForServer(m.Name, key)
				args = append(args, "-c", fmt.Sprintf("mcp_servers.%s.env.%s=%q", m.Name, codexTOMLMapKey(key), "${"+envVar+"}"))
			}
			args = appendCodexMCPCommonArgs(args, m.Name, false)
		default:
			if strings.TrimSpace(m.URL) == "" {
				continue
			}
			args = append(args, "-c", fmt.Sprintf("mcp_servers.%s.url=%q", m.Name, strings.TrimSpace(m.URL)))
			if token := extractBearerToken(m.Headers); token != "" {
				args = append(args, "-c", fmt.Sprintf("mcp_servers.%s.bearer_token_env_var=%q", m.Name, mcpAuthTokenEnvVarForServer(m.Name)))
			}
			if envHeaders := mcpEnvHTTPHeadersForServer(m.Name, m.Headers); len(envHeaders) > 0 {
				args = append(args, "-c", fmt.Sprintf("mcp_servers.%s.env_http_headers=%s", m.Name, tomlInlineStringMap(envHeaders)))
			}
			args = appendCodexMCPCommonArgs(args, m.Name, m.Required || m.Name == "agentteam")
		}
	}

	return args, nil
}

func codexNativeToolDisableArgs() []string {
	features := []string{
		"apps", "browser_use", "code_mode", "code_mode_host", "computer_use",
		"goals", "hooks", "image_generation", "memories", "multi_agent",
		"plugins", "remote_plugin", "shell_tool", "tool_suggest", "unified_exec",
	}
	args := make([]string, 0, len(features)*2+2)
	for _, feature := range features {
		args = append(args, "-c", fmt.Sprintf("features.%s=false", feature))
	}
	return append(args, "-c", `web_search="disabled"`)
}

func addCodexMCPEnv(env map[string]string, mcps []types.NamedMCPConfig) map[string]string {
	for _, m := range mcps {
		switch m.EffectiveTransport() {
		case types.MCPTransportHTTP:
			env = applyMCPHeaderEnv(env, []types.NamedMCPConfig{m})
		case types.MCPTransportStdio:
			if len(m.Env) == 0 {
				continue
			}
			if env == nil {
				env = make(map[string]string)
			}
			for key, value := range m.Env {
				if strings.TrimSpace(value) == "" {
					continue
				}
				env[mcpStdioEnvVarForServer(m.Name, key)] = value
			}
		}
	}
	return env
}

func appendCodexMCPCommonArgs(args []string, name string, required bool) []string {
	return append(args,
		"-c", fmt.Sprintf("mcp_servers.%s.startup_timeout_sec=30", name),
		"-c", fmt.Sprintf("mcp_servers.%s.tool_timeout_sec=300", name),
		"-c", fmt.Sprintf("mcp_servers.%s.enabled=true", name),
		"-c", fmt.Sprintf("mcp_servers.%s.required=%t", name, required),
	)
}

func codexTOMLStringArray(values []string) string {
	items := make([]string, 0, len(values))
	for _, value := range values {
		items = append(items, strconv.Quote(value))
	}
	return "[" + strings.Join(items, ", ") + "]"
}

func codexTOMLMapKey(key string) string {
	return strconv.Quote(key)
}

func sortedMapKeys(values map[string]string) []string {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

// ---------------------------------------------------------------------------
// Thread management
// ---------------------------------------------------------------------------
