package sandboxv2

import (
	"context"
	"fmt"
	"log"
	"path"
	pathpkg "path/filepath"
	"strings"

	"github.com/yaoapp/yao/agent/sandbox/v2/types"
	infra "github.com/yaoapp/yao/sandbox/v2"
	"github.com/yaoapp/yao/tai/workspace"
)

const onceMarkerDir = ".yao/prepare"

// RunPrepareSteps executes a list of PrepareStep actions on the given Computer.
// file/copy/marker operations use computer.Workplace() (gRPC volume, cross-platform).
// exec operations use shell via Computer.Exec.
// assistantDir is the absolute host path to the assistant source directory;
// copy steps with a relative src resolve against it (host → workspace push).
func RunPrepareSteps(ctx context.Context, steps []types.PrepareStep, computer infra.Computer, assistantID, configHash, assistantDir string) error {
	if len(steps) == 0 {
		return nil
	}

	var ws workspace.FS
	if computer != nil {
		ws = computer.Workplace()
	}

	markerDir := onceMarkerDir
	if assistantID != "" {
		markerDir = onceMarkerDir + "/" + assistantID
	}
	markerPath := markerDir + "/done"

	skipOnce := false
	if configHash != "" && ws != nil {
		if data, err := ws.ReadFile(markerPath); err == nil {
			if strings.TrimSpace(string(data)) == configHash {
				skipOnce = true
			}
		}
	}

	for i, step := range steps {
		if step.Once && skipOnce {
			continue
		}

		var err error
		switch step.Action {
		case "file":
			err = runFileStep(ws, step)
		case "copy":
			err = runCopyStep(ws, step, assistantDir)
		case "exec":
			err = runExecStep(ctx, computer, step)
		case "process":
			log.Printf("[sandbox/v2] prepare step %d: action=process (reserved, skipping)", i)
		default:
			err = fmt.Errorf("unknown prepare action %q", step.Action)
		}

		if err != nil {
			if step.IgnoreError {
				log.Printf("[sandbox/v2] prepare step %d (%s): ignored error: %v", i, step.Action, err)
				continue
			}
			return fmt.Errorf("prepare step %d (%s): %w", i, step.Action, err)
		}
	}

	if configHash != "" && ws != nil {
		ws.MkdirAll(markerDir, 0755)
		ws.WriteFile(markerPath, []byte(configHash), 0644)
	}

	return nil
}

// ---------------------------------------------------------------------------
// Step runners
// ---------------------------------------------------------------------------

func runFileStep(ws workspace.FS, step types.PrepareStep) error {
	if step.Path == "" {
		return fmt.Errorf("file step requires path")
	}
	if ws == nil {
		return fmt.Errorf("file step requires workspace")
	}

	step.Path = expandTilde(step.Path)
	dir := path.Dir(step.Path)
	if dir != "." && dir != "/" {
		if err := ws.MkdirAll(dir, 0755); err != nil {
			return fmt.Errorf("mkdir %s: %w", dir, err)
		}
	}

	if err := ws.WriteFile(step.Path, step.Content, 0644); err != nil {
		return fmt.Errorf("write file %s: %w", step.Path, err)
	}
	return nil
}

// runCopyStep copies files into the workspace using ws.Copy which supports
// the "local:///" URI scheme for host-to-workspace transfers.
//
// src resolution:
//   - Already a host URI ("local:///..." or "tmp:///...") → used as-is
//   - Relative path + assistantDir provided → resolved to "local:///<assistantDir>/<src>"
//   - Relative path without assistantDir → treated as workspace-internal path
func runCopyStep(ws workspace.FS, step types.PrepareStep, assistantDir string) error {
	if step.Src == "" || step.Dst == "" {
		return fmt.Errorf("copy step requires src and dst")
	}
	if ws == nil {
		return fmt.Errorf("copy step requires workspace")
	}

	src := step.Src
	if !isHostURI(src) && assistantDir != "" {
		src = "local:///" + pathpkg.Join(assistantDir, src)
	}

	dst := expandTilde(step.Dst)
	if _, err := ws.Copy(src, dst); err != nil {
		return fmt.Errorf("copy %s -> %s: %w", src, dst, err)
	}
	return nil
}

func isHostURI(s string) bool {
	return strings.HasPrefix(s, "local:///") || strings.HasPrefix(s, "tmp:///")
}

// expandTilde replaces a leading "~/" with the empty string so the path
// becomes relative to the workspace root (which is HOME inside the sandbox).
// Paths without "~/" are returned unchanged.
func expandTilde(p string) string {
	if strings.HasPrefix(p, "~/") {
		return p[2:]
	}
	if p == "~" {
		return "."
	}
	return p
}

func runExecStep(ctx context.Context, computer infra.Computer, step types.PrepareStep) error {
	if step.Cmd == "" {
		return fmt.Errorf("exec step requires cmd")
	}

	kind := shellFromSystem(computer)
	script := step.Cmd
	if step.Background {
		if kind == shellSh {
			script = fmt.Sprintf("nohup %s > /dev/null 2>&1 &", step.Cmd)
		} else {
			script = fmt.Sprintf("Start-Process -NoNewWindow -FilePath 'cmd.exe' -ArgumentList '/C %s'", step.Cmd)
		}
	}

	workDir := computer.GetWorkDir()
	if workDir == "" {
		workDir = "/"
		if isWindowsComputer(computer) {
			workDir = `C:\`
		}
	}

	result, err := computer.Exec(ctx, shellWrap(kind, script),
		infra.WithWorkDir(workDir),
		infra.WithEnv(map[string]string{"HOME": workDir}),
	)
	if err != nil {
		return err
	}
	label := "exec"
	if step.Background {
		label = "exec(background)"
	}
	return checkResult(result, label)
}

func isWindowsComputer(computer infra.Computer) bool {
	return strings.EqualFold(computer.ComputerInfo().System.OS, "windows")
}

// checkResult inspects ExecResult for errors.
func checkResult(result *infra.ExecResult, label string) error {
	if result.Error != "" {
		return fmt.Errorf("%s: %s", label, result.Error)
	}
	if result.ExitCode != 0 {
		stderr := result.Stderr
		if len(stderr) > 200 {
			stderr = stderr[:200] + "..."
		}
		return fmt.Errorf("%s: exit %d: %s", label, result.ExitCode, stderr)
	}
	return nil
}
