package infra

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"
)

// WorktreeLaunchConfig configures a headless Claude agent run inside a fresh
// git worktree. All paths must be absolute. The function spawns claude in the
// background and returns immediately with the PID; the caller is responsible
// for AgentCleanupWorktree when the run finishes (or aborts).
type WorktreeLaunchConfig struct {
	RepoRoot      string            // absolute path to the main repo (git -C <RepoRoot>)
	Branch        string            // e.g. "auto/0115-foo" — created from master
	WorktreePath  string            // absolute path where worktree gets added
	Prompt        string            // text passed to claude -p
	LogPath       string            // file claude stdout/stderr is redirected to
	Env           map[string]string // extra env vars merged on top of os.Environ()
	SkipPerms     bool              // adds --dangerously-skip-permissions
	ResetIfExists bool              // if true, branch + worktree are nuked first
}

// WorktreeLaunchResult is the return shape of AgentLaunchWorktree.
type WorktreeLaunchResult struct {
	PID          int    // claude process id (0 if Error != "")
	Branch       string // echoes cfg.Branch
	WorktreePath string // echoes cfg.WorktreePath
	LogPath      string // echoes cfg.LogPath
	StartedAt    int64  // unix seconds when cmd.Start() returned
	Error        string // empty on success; populated on any failure
}

// AgentLaunchWorktree creates a fresh git worktree on a new branch off master
// and spawns `claude -p <prompt>` headless inside that worktree, redirecting
// stdout+stderr to LogPath. Returns immediately (process keeps running).
//
// If `claude` is not in PATH, falls back to an `echo` stub so tests can run
// without the real binary — the stub still produces a real PID and log file.
//
// Impure: touches the filesystem (worktree), spawns a process, writes a log.
func AgentLaunchWorktree(cfg WorktreeLaunchConfig) WorktreeLaunchResult {
	res := WorktreeLaunchResult{
		Branch:       cfg.Branch,
		WorktreePath: cfg.WorktreePath,
		LogPath:      cfg.LogPath,
	}

	if cfg.RepoRoot == "" || cfg.Branch == "" || cfg.WorktreePath == "" {
		res.Error = "RepoRoot, Branch and WorktreePath are required"
		return res
	}

	// Best-effort cleanup of pre-existing branch/worktree.
	if cfg.ResetIfExists {
		_ = exec.Command("git", "-C", cfg.RepoRoot, "worktree", "remove", "--force", cfg.WorktreePath).Run()
		_ = exec.Command("git", "-C", cfg.RepoRoot, "branch", "-D", cfg.Branch).Run()
		// Best-effort dir cleanup (git worktree remove leaves nothing, but
		// just in case the dir was created out-of-band).
		_ = os.RemoveAll(cfg.WorktreePath)
	}

	// Create the new worktree off master.
	addCmd := exec.Command("git", "-C", cfg.RepoRoot, "worktree", "add", cfg.WorktreePath, "-b", cfg.Branch, "master")
	if out, err := addCmd.CombinedOutput(); err != nil {
		res.Error = fmt.Sprintf("git worktree add failed: %v: %s", err, strings.TrimSpace(string(out)))
		return res
	}

	// Open / truncate log file.
	logFile, err := os.OpenFile(cfg.LogPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
	if err != nil {
		res.Error = fmt.Sprintf("open log %s: %v", cfg.LogPath, err)
		return res
	}

	// Resolve claude binary; fall back to echo stub if not found.
	claudeBin, lookErr := exec.LookPath("claude")
	var args []string
	var bin string
	if lookErr != nil {
		bin = "echo"
		args = []string{"STUB: claude not in PATH, prompt was:", cfg.Prompt}
	} else {
		bin = claudeBin
		if cfg.SkipPerms {
			args = append(args, "--dangerously-skip-permissions")
		}
		args = append(args, "-p", cfg.Prompt)
	}

	cmd := exec.Command(bin, args...)
	cmd.Dir = cfg.WorktreePath
	cmd.Stdout = logFile
	cmd.Stderr = logFile
	// Merge env: os.Environ() base + cfg.Env overrides.
	env := os.Environ()
	for k, v := range cfg.Env {
		env = append(env, k+"="+v)
	}
	cmd.Env = env

	if err := cmd.Start(); err != nil {
		_ = logFile.Close()
		res.Error = fmt.Sprintf("cmd.Start: %v", err)
		return res
	}

	// Release the file handle in this process — child still holds it.
	// (Closing immediately is OK; the kernel keeps the fd open in the child.)
	go func() {
		_ = cmd.Wait()
		_ = logFile.Close()
	}()

	res.PID = cmd.Process.Pid
	res.StartedAt = time.Now().Unix()
	return res
}
