package agent

import (
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/google/uuid"

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

type piPreparedLaunch struct {
	Args             []string
	Env              []string
	WorkDir          string
	ShareDir         string
	BridgeFile       string
	MCPConfigFile    string
	SystemPromptFile string
	SessionDir       string
	AgentDir         string
	ModelsFile       string
	LaunchDir        string
	Cleanup          func()
}

func (prepared *piPreparedLaunch) addCleanup(cleanup func()) {
	if prepared == nil || cleanup == nil {
		return
	}
	previous := prepared.Cleanup
	prepared.Cleanup = func() {
		cleanup()
		if previous != nil {
			previous()
		}
	}
}

func preparePiLaunch(cfg drivercontract.LaunchConfig, sessionID string) (piPreparedLaunch, error) {
	workDir, err := filepath.Abs(strings.TrimSpace(cfg.WorkDir))
	if err != nil || strings.TrimSpace(cfg.WorkDir) == "" {
		return piPreparedLaunch{}, fmt.Errorf("pi: valid workdir is required")
	}
	workDir = filepath.Clean(workDir)
	if err := os.MkdirAll(workDir, 0o755); err != nil {
		return piPreparedLaunch{}, fmt.Errorf("pi: create workdir: %w", err)
	}

	env := runtimeconfig.RuntimeEnvOverrides(cfg.Env, cfg.MCPs)
	if env == nil {
		env = make(map[string]string)
	}
	env["FORCE_COLOR"] = "0"
	env["NO_COLOR"] = "1"

	prepared := piPreparedLaunch{
		Args:     []string{"--mode", "rpc"},
		WorkDir:  workDir,
		ShareDir: filepath.Join(workDir, ".pi-agentteam"),
		Cleanup:  func() {},
	}
	success := false
	defer func() {
		if !success {
			prepared.Cleanup()
		}
	}()

	// Session dir under the stable per-agent runtime state root so restart
	// resume finds the same session file.
	sessionDir := strings.TrimSpace(piSessionDir(cfg))
	if sessionDir == "" {
		sessionDir = filepath.Join(prepared.ShareDir, "sessions")
	}
	if strings.TrimSpace(cfg.RuntimeStateDir) == "" {
		if err := ensurePiDirectory(prepared.ShareDir, 0o700); err != nil {
			return piPreparedLaunch{}, fmt.Errorf("pi: create session share dir: %w", err)
		}
	}
	if err := ensurePiSessionDirectory(sessionDir); err != nil {
		return piPreparedLaunch{}, fmt.Errorf("pi: create session dir: %w", err)
	}
	prepared.SessionDir = sessionDir
	prepared.Args = append(prepared.Args, "--session-dir", sessionDir)
	if strings.TrimSpace(sessionID) != "" {
		prepared.Args = append(prepared.Args, "--session-id", sessionID)
	}

	if err := configurePiAgentHome(&prepared, cfg, env); err != nil {
		return piPreparedLaunch{}, err
	}

	piAddIdentityArgs(&prepared, cfg, sessionID)
	if err := piAddProviderConfig(&prepared, cfg, env); err != nil {
		return piPreparedLaunch{}, err
	}

	// Permission mode: pi has no yolo flag; full access is the only supported
	// mode and pi's built-in tools already run with the caller's permissions.
	permissionMode, err := cfg.EffectivePermissionMode()
	if err != nil {
		return piPreparedLaunch{}, err
	}
	switch permissionMode {
	case types.AgentPermissionModeFullAccess:
		// no flag needed
	default:
		return piPreparedLaunch{}, fmt.Errorf("pi: unsupported permission mode %q", permissionMode)
	}

	// Native tools: when AgentTeam wants a restricted tool set, pass --no-tools
	// so only the MCP bridge tool set remains.
	if cfg.DisableNativeTools {
		prepared.Args = append(prepared.Args, "--no-tools")
	}

	// System prompt assembly: pi accepts --append-system-prompt. When the
	// carrier/instructions are non-trivial, materialise them to a file and
	// pass the file path so the prompt survives shell quoting.
	if err := piAddSystemPrompt(&prepared, cfg); err != nil {
		return piPreparedLaunch{}, err
	}

	// MCP bridge + servers env.
	if err := piAddMCPAndSkills(&prepared, cfg, workDir, env); err != nil {
		return piPreparedLaunch{}, err
	}

	prepared.Env = buildRuntimeEnvWithProvider(env, cfg.Provider)
	if prepared.AgentDir != "" {
		prepared.Env = replacePiEnv(prepared.Env, piAgentDirEnv, prepared.AgentDir)
	}
	success = true
	return prepared, nil
}

const piAgentDirEnv = "PI_CODING_AGENT_DIR"

var piEnvKeyPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

// configurePiAgentHome assigns the stable per-agent PI_CODING_AGENT_DIR and
// seeds it only for runtime_default. Inline launches own their complete
// provider/auth configuration and must not inherit unrelated native credentials.
func configurePiAgentHome(prepared *piPreparedLaunch, cfg drivercontract.LaunchConfig, env map[string]string) error {
	if prepared == nil {
		return fmt.Errorf("pi: prepared launch is nil")
	}
	agentDir, err := piAgentHome(cfg, prepared.ShareDir)
	if err != nil {
		return err
	}
	if !providerUpstreamConfigured(cfg.Provider) {
		if err := inheritPiRuntimeDefault(agentDir, env); err != nil {
			return err
		}
	}
	prepared.AgentDir = agentDir
	env[piAgentDirEnv] = agentDir
	return nil
}

func piAddProviderConfig(prepared *piPreparedLaunch, cfg drivercontract.LaunchConfig, env map[string]string) error {
	prov := cfg.Provider
	if prov == nil || strings.TrimSpace(prov.BaseURL) == "" {
		return nil
	}
	root := prepared.AgentDir
	if root == "" {
		var err error
		root, err = ensurePiLaunchConfigDir(prepared)
		if err != nil {
			return err
		}
	}
	providerName := piProviderName(prov)
	if providerName == "" {
		return fmt.Errorf("pi: inline provider requires a provider id or recognizable base URL")
	}
	providerConfig, err := piModelsProviderConfig(prov, cfg.Model)
	if err != nil {
		return err
	}
	catalog := map[string]any{"providers": map[string]any{
		providerName: providerConfig,
	}}
	data, err := json.MarshalIndent(catalog, "", "  ")
	if err != nil {
		return fmt.Errorf("pi: encode models catalog: %w", err)
	}
	modelsPath := filepath.Join(root, "models.json")
	if err := writePiModelsAtomic(modelsPath, data); err != nil {
		return err
	}
	prepared.AgentDir = root
	prepared.ModelsFile = modelsPath
	env[piAgentDirEnv] = root
	return nil
}

const (
	piAgentHomeName         = "pi"
	piRuntimeDefaultMaxFile = 32 << 20
)

var piRuntimeDefaultFiles = []string{
	"settings.json",
	"auth.json",
	"models.json",
	"models-store.json",
}

// piAgentHome returns the stable, private Pi configuration root for one
// AgentTeam-managed agent. RuntimeStateDir is supplied by the dispatcher and is
// already scoped to the agent identity (not the ephemeral launch workdir).
func piAgentHome(cfg drivercontract.LaunchConfig, fallbackRoot string) (string, error) {
	root := strings.TrimSpace(cfg.RuntimeStateDir)
	if root == "" {
		root = strings.TrimSpace(fallbackRoot)
	}
	if root == "" {
		return "", fmt.Errorf("pi: runtime state directory is required for a persistent agent home")
	}
	absolute, err := filepath.Abs(root)
	if err != nil {
		return "", fmt.Errorf("pi: resolve runtime state directory: %w", err)
	}
	homeName := piAgentHomeName
	if strings.TrimSpace(cfg.RuntimeStateDir) == "" {
		homeName = "agent"
	}
	home := filepath.Join(filepath.Clean(absolute), homeName)
	if err := ensurePiSessionDirectory(home); err != nil {
		return "", fmt.Errorf("pi: create persistent agent home: %w", err)
	}
	return home, nil
}

// inheritPiRuntimeDefault seeds a fresh per-agent home with the native Pi
// configuration. Pi's settings/auth/catalog files are all local state; copying
// them once gives runtime_default the same provider/model selection while
// allowing subsequent /login and catalog updates to remain agent-local.
//
// The source honors an explicitly configured PI_CODING_AGENT_DIR before the
// process environment, which makes custom Pi installations work as expected.
// Existing destination files are never overwritten: a restart must preserve
// state written by the already-running agent.
func inheritPiRuntimeDefault(agentDir string, env map[string]string) error {
	agentDir = filepath.Clean(strings.TrimSpace(agentDir))
	if agentDir == "" {
		return fmt.Errorf("pi: persistent agent home is empty")
	}
	source := strings.TrimSpace(env[piAgentDirEnv])
	if source == "" {
		source = strings.TrimSpace(os.Getenv(piAgentDirEnv))
	}
	if source == "" {
		home, err := os.UserHomeDir()
		if err != nil {
			return fmt.Errorf("pi: resolve native runtime home: %w", err)
		}
		source = filepath.Join(home, ".pi", "agent")
	}
	if source == "" {
		return nil
	}
	source, err := filepath.Abs(source)
	if err != nil {
		return fmt.Errorf("pi: resolve runtime default home: %w", err)
	}
	source = filepath.Clean(source)
	if source == agentDir {
		return nil
	}

	info, err := os.Lstat(source)
	if os.IsNotExist(err) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("pi: inspect runtime default home: %w", err)
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
		return fmt.Errorf("pi: runtime default home %q is not a directory", source)
	}
	for _, name := range piRuntimeDefaultFiles {
		if err := copyPiRuntimeDefaultFile(filepath.Join(source, name), filepath.Join(agentDir, name)); err != nil {
			return err
		}
	}
	return nil
}

func copyPiRuntimeDefaultFile(source, destination string) error {
	info, err := os.Lstat(source)
	if os.IsNotExist(err) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("pi: inspect runtime default file %q: %w", source, err)
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
		return fmt.Errorf("pi: runtime default file %q is not a safe regular file", source)
	}
	if info.Size() > piRuntimeDefaultMaxFile {
		return fmt.Errorf("pi: runtime default file %q exceeds %d bytes", source, piRuntimeDefaultMaxFile)
	}

	if existing, err := os.Lstat(destination); err == nil {
		if existing.Mode()&os.ModeSymlink != 0 || !existing.Mode().IsRegular() {
			return fmt.Errorf("pi: destination %q is not a safe regular file", destination)
		}
		if err := chmodPiPath(destination, 0o600, false); err != nil {
			return fmt.Errorf("pi: secure destination %q: %w", destination, err)
		}
		return nil
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("pi: inspect destination %q: %w", destination, err)
	}

	data, err := os.ReadFile(source)
	if err != nil {
		return fmt.Errorf("pi: read runtime default file %q: %w", source, err)
	}
	if err := writePiModelsAtomic(destination, data); err != nil {
		return fmt.Errorf("pi: copy runtime default file %q: %w", source, err)
	}
	return nil
}

func piModelsProviderConfig(prov *drivercontract.ProviderConfig, model string) (map[string]any, error) {
	if prov == nil {
		return nil, fmt.Errorf("pi: inline provider is nil")
	}
	if envKey := strings.TrimSpace(prov.EnvKey); envKey != "" && !piEnvKeyPattern.MatchString(envKey) {
		return nil, fmt.Errorf("pi: inline provider environment key %q is invalid", envKey)
	}
	api, err := piProviderAPI(prov)
	if err != nil {
		return nil, err
	}
	baseURL := strings.TrimSpace(prov.BaseURL)
	if api == "anthropic-messages" {
		// Pi uses the Anthropic SDK, which appends /v1/messages to baseUrl.
		// ProviderConfig carries the protocol root (for example /maas/v1), so
		// normalize it the same way as Claude Code rather than producing
		// /maas/v1/v1/messages.
		baseURL = claudeAnthropicBaseURL(prov)
	}
	config := map[string]any{
		"baseUrl": baseURL,
		"api":     api,
	}
	if name := strings.TrimSpace(prov.Name); name != "" {
		config["name"] = name
	}
	if key := strings.TrimSpace(prov.APIKey); key != "" {
		config["apiKey"] = key
	} else if envKey := strings.TrimSpace(prov.EnvKey); envKey != "" {
		// Pi's models.json value resolver treats "$NAME" as an environment
		// variable reference. A bare NAME is a literal API key, so preserve
		// the explicit reference marker when materializing an EnvKey.
		config["apiKey"] = "$" + envKey
	}
	if len(prov.Headers) > 0 {
		headers := make(map[string]string, len(prov.Headers))
		for key, value := range prov.Headers {
			headers[key] = value
		}
		config["headers"] = headers
	}
	// Pi's OpenAI Chat Completions adapter auto-detects `store` support
	// from the URL and otherwise sends `store:false`. A number of
	// OpenAI-compatible MaaS gateways reject that field entirely. Scope
	// the compatibility override to this provider and this API;
	// Responses and Anthropic have different request contracts.
	if api == "openai-completions" {
		config["compat"] = map[string]any{"supportsStore": false}
	}
	if model = strings.TrimSpace(model); model != "" {
		modelConfig := map[string]any{
			"id":  model,
			"api": api,
		}
		if piUsesAgentTeamMaaSAnthropic(prov) {
			// AgentTeam-hosted Pi launches use the same Anthropic Messages boundary as
			// Claude Code. The hosted catalog currently exposes thinking models;
			// mark the custom model accordingly so Pi preserves and replays native
			// thinking blocks across tool-call turns.
			modelConfig["reasoning"] = true
		}
		config["models"] = []any{modelConfig}
	}
	return config, nil
}

func piProviderAPI(prov *drivercontract.ProviderConfig) (string, error) {
	// A AgentTeam MaaS capability is an internal multi-protocol endpoint. Pi's
	// native Anthropic Messages adapter matches that boundary end-to-end and
	// avoids reconstructing model-specific Chat Completions compatibility in
	// the AgentTeam driver. This also keeps tool calls and thinking replay in one
	// protocol instead of translating them through OpenAI-shaped messages.
	if piUsesAgentTeamMaaSAnthropic(prov) {
		return "anthropic-messages", nil
	}
	var selected string
	if prov != nil {
		for _, wireAPI := range prov.WireAPIs {
			switch strings.ToLower(strings.TrimSpace(wireAPI)) {
			case provider.WireAPIResponses:
				if selected != "" && selected != "openai-responses" {
					return "", fmt.Errorf("pi: inline provider has conflicting wire APIs %q and %q", selected, wireAPI)
				}
				selected = "openai-responses"
			case provider.WireAIOpenAI:
				if selected != "" && selected != "openai-completions" {
					return "", fmt.Errorf("pi: inline provider has conflicting wire APIs %q and %q", selected, wireAPI)
				}
				selected = "openai-completions"
			case provider.WireAPIAnthropic:
				if selected != "" && selected != "anthropic-messages" {
					return "", fmt.Errorf("pi: inline provider has conflicting wire APIs %q and %q", selected, wireAPI)
				}
				selected = "anthropic-messages"
			case "":
				continue
			default:
				return "", fmt.Errorf("pi: inline provider has unsupported wire API %q", wireAPI)
			}
		}
		if selected != "" {
			return selected, nil
		}
		baseURL := strings.ToLower(strings.TrimSpace(prov.BaseURL))
		if strings.Contains(baseURL, "anthropic") {
			return "anthropic-messages", nil
		}
		if strings.Contains(baseURL, "responses") {
			return "openai-responses", nil
		}
	}
	return "openai-completions", nil
}

func replacePiEnv(env []string, key, value string) []string {
	prefix := key + "="
	filtered := make([]string, 0, len(env)+1)
	for _, entry := range env {
		if !strings.HasPrefix(entry, prefix) {
			filtered = append(filtered, entry)
		}
	}
	return append(filtered, prefix+value)
}

func writePiModelsAtomic(path string, data []byte) error {
	dir := filepath.Dir(path)
	if err := ensurePiDirectory(dir, 0o700); err != nil {
		return fmt.Errorf("pi: secure models directory: %w", err)
	}
	if info, err := os.Lstat(path); err == nil {
		if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
			return fmt.Errorf("pi: models path %q is not a safe regular file", path)
		}
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("pi: inspect models path: %w", err)
	}
	temp, err := os.CreateTemp(dir, ".models-*.tmp")
	if err != nil {
		return fmt.Errorf("pi: create models temp: %w", err)
	}
	tempPath := temp.Name()
	cleanup := func() {
		_ = temp.Close()
		_ = os.Remove(tempPath)
	}
	if err := temp.Chmod(0o600); err != nil {
		cleanup()
		return fmt.Errorf("pi: secure models temp: %w", err)
	}
	if _, err := temp.Write(data); err != nil {
		cleanup()
		return fmt.Errorf("pi: write models temp: %w", err)
	}
	if err := temp.Sync(); err != nil {
		cleanup()
		return fmt.Errorf("pi: fsync models temp: %w", err)
	}
	if err := temp.Close(); err != nil {
		_ = os.Remove(tempPath)
		return fmt.Errorf("pi: close models temp: %w", err)
	}
	if err := os.Rename(tempPath, path); err != nil {
		_ = os.Remove(tempPath)
		return fmt.Errorf("pi: atomically install models: %w", err)
	}
	if err := syncPiDirectory(dir); err != nil {
		return fmt.Errorf("pi: fsync models directory: %w", err)
	}
	info, err := os.Lstat(path)
	if err != nil {
		return fmt.Errorf("pi: inspect installed models: %w", err)
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
		return fmt.Errorf("pi: installed models path %q is not a safe regular file", path)
	}
	if err := chmodPiPath(path, 0o600, false); err != nil {
		return fmt.Errorf("pi: secure installed models: %w", err)
	}
	return nil
}

// piAddIdentityArgs appends session name, model, thinking, and provider flags.
func piAddIdentityArgs(prepared *piPreparedLaunch, cfg drivercontract.LaunchConfig, sessionID string) {
	// Name the session for the TUI / listing.
	if name := strings.TrimSpace(cfg.AgentID); name != "" {
		prepared.Args = append(prepared.Args, "--name", name)
	}
	// Model & reasoning.
	if model := strings.TrimSpace(cfg.Model); model != "" {
		prepared.Args = append(prepared.Args, "--model", model)
	}
	if thinking := piValidateThinkingLevel(cfg.ReasoningEffort); thinking != "" {
		prepared.Args = append(prepared.Args, "--thinking", thinking)
	}
	// Provider injection.
	if cfg.Provider != nil {
		if provider := piProviderName(cfg.Provider); provider != "" {
			prepared.Args = append(prepared.Args, "--provider", provider)
		}
	}
}

// piAddSystemPrompt materialises the AgentTeam contract carrier and base/developer
// instructions as a pi --append-system-prompt file.
func piAddSystemPrompt(prepared *piPreparedLaunch, cfg drivercontract.LaunchConfig) error {
	systemPrompt := piSystemPrompt(cfg)
	if strings.TrimSpace(systemPrompt) == "" {
		return nil
	}
	root, err := ensurePiLaunchConfigDir(prepared)
	if err != nil {
		return err
	}
	prepared.SystemPromptFile = filepath.Join(root, "system.md")
	if err := writePiModelsAtomic(prepared.SystemPromptFile, []byte(systemPrompt)); err != nil {
		return fmt.Errorf("pi: write system prompt: %w", err)
	}
	prepared.Args = append(prepared.Args, "--append-system-prompt", prepared.SystemPromptFile)
	return nil
}

// piAddMCPAndSkills wires the MCP bridge extension and the CHANNEL9_MCP_SERVERS
// env, and materialises AgentTeam skills into pi's workdir skill location.
func piAddMCPAndSkills(prepared *piPreparedLaunch, cfg drivercontract.LaunchConfig, workDir string, env map[string]string) error {
	launchDir, err := ensurePiLaunchConfigDir(prepared)
	if err != nil {
		return fmt.Errorf("pi: create launch auxiliary directory: %w", err)
	}
	bridgePath, err := writePiMCPBridge(launchDir)
	if err != nil {
		return fmt.Errorf("pi: write mcp bridge: %w", err)
	}
	prepared.BridgeFile = bridgePath
	prepared.Args = append(prepared.Args, "-e", bridgePath)
	if len(cfg.MCPs) > 0 {
		mcpServersJSON, err := encodePiMCPServersEnv(cfg.MCPs)
		if err != nil {
			return err
		}
		env["CHANNEL9_MCP_SERVERS"] = mcpServersJSON
	}
	if err := materializePiSkills(cfg, workDir); err != nil {
		return fmt.Errorf("pi: materialize skills: %w", err)
	}
	return nil
}

// piSessionDir returns the per-agent stable session directory for pi.
func piSessionDir(cfg drivercontract.LaunchConfig) string {
	stateDir := strings.TrimSpace(cfg.RuntimeStateDir)
	if stateDir == "" {
		return ""
	}
	return filepath.Join(stateDir, ".pi-sessions")
}

// piSystemPrompt assembles the system prompt passed to pi via
// --append-system-prompt. It layers the AgentTeam contract carrier and base/
// developer instructions that pi should treat as durable context rather than
// a turn input.
func piSystemPrompt(cfg drivercontract.LaunchConfig) string {
	if cfg.CompiledPrompt == nil || !cfg.CompiledPrompt.HasLaunchCarrier() {
		return ""
	}
	return cfg.CompiledPrompt.LaunchSurface("systemPrompt")
}

// piInitialPrompt composes the first user-visible turn prompt for a fresh or
// resumed session.
func piInitialPrompt(cfg drivercontract.LaunchConfig, resumed bool) string {
	if cfg.CompiledPrompt == nil || !cfg.CompiledPrompt.HasLaunchCarrier() {
		return ""
	}
	return cfg.CompiledPrompt.InitialPrompt(resumed)
}

func resolvePiLaunchSession(cfg drivercontract.LaunchConfig) (string, bool, error) {
	sessionID := strings.TrimSpace(cfg.SessionID)
	if sessionID == "" {
		return uuid.NewString(), false, nil
	}
	if _, err := uuid.Parse(sessionID); err != nil {
		return "", false, fmt.Errorf("pi: invalid session id %q", sessionID)
	}
	path, err := piSessionFilePath(cfg, sessionID)
	if err != nil {
		// No durable session file yet: this is a fresh session with a
		// deterministic id (allows resume to attach on a later launch).
		return sessionID, false, nil
	}
	if info, err := os.Lstat(path); err == nil {
		if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
			if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
				return "", false, fmt.Errorf("pi: session path %q is not a safe file", path)
			}
		}
	}
	return sessionID, true, nil
}

func ensurePiLaunchConfigDir(prepared *piPreparedLaunch) (string, error) {
	switch {
	case prepared == nil:
		return "", fmt.Errorf("pi: launch config is nil")
	case prepared.LaunchDir != "":
		return prepared.LaunchDir, nil
	}
	// Prompt/bridge files are launch-scoped and must never live in the
	// persistent PI_CODING_AGENT_DIR. Use an OS temporary directory rather
	// than a random child under the workdir, which may itself be ephemeral or
	// shared with a project checkout.
	root, err := os.MkdirTemp("", "agentteam-pi-launch-*")
	if err != nil {
		return "", fmt.Errorf("pi: create launch config directory: %w", err)
	}
	if err := chmodPiPath(root, 0o700, true); err != nil {
		_ = os.RemoveAll(root)
		return "", fmt.Errorf("pi: secure launch config directory: %w", err)
	}
	prepared.LaunchDir = root
	prepared.addCleanup(func() {
		if err := os.RemoveAll(root); err != nil {
			slog.Warn("pi: remove launch config directory", "path", root, "error", err)
		}
	})
	return root, nil
}

func ensurePiDirectory(path string, mode os.FileMode) error {
	if strings.TrimSpace(path) == "" {
		return fmt.Errorf("directory path is empty")
	}
	info, err := os.Lstat(path)
	if os.IsNotExist(err) {
		if err := os.Mkdir(path, mode); err != nil {
			return err
		}
		info, err = os.Lstat(path)
	}
	if err != nil {
		return err
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
		return fmt.Errorf("path %q is not a safe directory", path)
	}
	return chmodPiPath(path, mode, true)
}

// ensurePiSessionDirectory is allowed to follow the daemon's trusted
// RuntimeStateDir ancestors (which may themselves be a user-visible symlink),
// but never accepts a symlink or non-directory at the controlled session leaf.
func ensurePiSessionDirectory(path string) error {
	if err := os.MkdirAll(path, 0o700); err != nil {
		return err
	}
	info, err := os.Lstat(path)
	if err != nil {
		return err
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
		return fmt.Errorf("path %q is not a safe directory", path)
	}
	return chmodPiPath(path, 0o700, true)
}

// materializePiSkills copies AgentTeam skills into pi's workdir skill location.
// Pi discovers skills from `<workdir>/.pi/agent/skills` (CONFIG_DIR_NAME is
// ".pi"). This mirrors the Codex/OpenCode `.agents/skills` target.
func materializePiSkills(cfg drivercontract.LaunchConfig, workDir string) error {
	if strings.TrimSpace(cfg.SkillDir) == "" && len(cfg.ManagedSkillDirs) == 0 {
		return nil
	}
	// Skill materialisation is handled by the Process layer via
	// MaterializeRuntimeSkillsFromSources with the runtimeconfig skill target;
	// the driver only needs the workdir to exist (it does).
	return nil
}

// piProviderName maps a AgentTeam provider config to a pi --provider value.
func piProviderName(prov *drivercontract.ProviderConfig) string {
	if prov == nil {
		return ""
	}
	id := strings.Trim(strings.TrimSpace(prov.ID), "/")
	if id != "" {
		return id
	}
	switch {
	case strings.Contains(strings.ToLower(prov.BaseURL), "anthropic"):
		return "anthropic"
	case strings.Contains(strings.ToLower(prov.BaseURL), "openai"):
		return "openai"
	default:
		return ""
	}
}

func piUsesAgentTeamMaaSAnthropic(prov *drivercontract.ProviderConfig) bool {
	return prov != nil && strings.TrimSpace(prov.CredentialKind) == LLMCredentialKindMaaSCapability
}

// piValidateThinkingLevel normalizes and validates a pi --thinking value.
func piValidateThinkingLevel(level string) string {
	trimmed := strings.ToLower(strings.TrimSpace(level))
	switch trimmed {
	case "", "off", "minimal", "low", "medium", "high", "xhigh", "max":
		return trimmed
	default:
		return ""
	}
}

func encodePiMCPServersEnv(mcps []types.NamedMCPConfig) (string, error) {
	type wireServer struct {
		Name    string            `json:"name"`
		URL     string            `json:"url"`
		Headers map[string]string `json:"headers,omitempty"`
	}
	wires := make([]wireServer, 0, len(mcps))
	for _, m := range mcps {
		if strings.TrimSpace(m.Name) == "" {
			continue
		}
		url := strings.TrimSpace(m.URL)
		if url == "" {
			continue
		}
		wires = append(wires, wireServer{
			Name:    strings.TrimSpace(m.Name),
			URL:     url,
			Headers: m.Headers,
		})
	}
	data, err := json.Marshal(wires)
	if err != nil {
		return "", fmt.Errorf("pi: encode MCP servers: %w", err)
	}
	return string(data), nil
}

var errPiSessionNotFound = errors.New("pi: session not found")
