// Package git 是 git CLI 子进程封装。所有方法走 exec.CommandContext，stdout/stderr
// 各自捕获，stderr 摘要在失败时通过 GitError 暴露给调用方。
package git

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"time"
)

// stderrTruncateLimit caps captured stderr to keep audit logs bounded.
const stderrTruncateLimit = 2000

// Runner 是 git 子系统暴露给业务层的全部能力。所有方法接 ctx 与 dir
// （工作目录的绝对路径），不接受用户传入的 path —— 路径在 workspace 包
// 计算后再传入此处，避免命令注入。
type Runner interface {
	Clone(ctx context.Context, dst, remoteURL, branch string, cred *Credential) error
	Fetch(ctx context.Context, dir string, cred *Credential) error
	FetchRemoteBranch(ctx context.Context, dir, branch string, cred *Credential) error
	Push(ctx context.Context, dir, branch string, cred *Credential) error
	Commit(ctx context.Context, dir, message string, addAll bool) (sha string, err error)
	Status(ctx context.Context, dir string) ([]FileStatus, error)
	MergeFFOnly(ctx context.Context, dir string) error
	WorktreeAdd(ctx context.Context, dir, branch, path, startPoint string) error
	WorktreeRemove(ctx context.Context, dir, path string) error
	WorktreeList(ctx context.Context, dir string) ([]Worktree, error)
	ListRemoteBranches(ctx context.Context, dir string, cred *Credential) ([]string, error)
	Checkout(ctx context.Context, dir, branch string) error
	Log(ctx context.Context, dir string, n int) ([]Commit, error)
	Diff(ctx context.Context, dir string, opts DiffOptions) (DiffResult, error)
	// ListTrackedFiles returns the git-tracked files at HEAD (git ls-files),
	// worktree-relative, so the indexer never walks ignored/vendored/untracked paths.
	ListTrackedFiles(ctx context.Context, dir string) ([]string, error)
	// DiffStat returns the `--stat` summary for a single commit (git show --stat),
	// used by auto-doc generation to feed the LLM a bounded change overview.
	DiffStat(ctx context.Context, dir, commitSHA string) (string, error)
}

// FileStatus 表示 `git status --porcelain=v1` 的一行。
type FileStatus struct {
	Path      string // 工作树相对路径
	IndexX    byte   // staged 状态：M/A/D/R/C/U/?/!
	WorktreeY byte   // worktree 状态：同上
}

// Worktree 表示 `git worktree list --porcelain` 的一项。
type Worktree struct {
	Path   string
	Branch string // refs/heads/<branch> 去前缀
	HEAD   string // commit sha
	Bare   bool
}

// Config 控制 Runner 的执行参数；所有 Timeout 都被 ctx 覆盖（取较小值）。
type Config struct {
	Binary       string        // 默认 "git"，测试可换 stub
	TmpDir       string        // 凭据临时文件根目录（${DATA_DIR}/tmp）
	CloneTimeout time.Duration // 默认 30 min
	FetchTimeout time.Duration // 默认 5  min
	PushTimeout  time.Duration // 默认 5  min
	OpsTimeout   time.Duration // 默认 60 s（commit/status/worktree）
}

// DefaultConfig 返回带合理默认值的 Config。Binary="git", TmpDir=os.TempDir() 兜底。
func DefaultConfig() Config {
	return Config{
		Binary:       "git",
		TmpDir:       os.TempDir(),
		CloneTimeout: 30 * time.Minute,
		FetchTimeout: 5 * time.Minute,
		PushTimeout:  5 * time.Minute,
		OpsTimeout:   60 * time.Second,
	}
}

// DefaultRunner 是 Runner 的生产实现。NewDefaultRunner 是构造器。
type DefaultRunner struct{ cfg Config }

// NewDefaultRunner 用给定配置构造 DefaultRunner。tmpDir 必须存在并可写；
// 调用方负责进程启动期清理 tmpDir 残留文件。
func NewDefaultRunner(cfg Config) *DefaultRunner {
	if cfg.Binary == "" {
		cfg.Binary = "git"
	}
	if cfg.TmpDir == "" {
		cfg.TmpDir = os.TempDir()
	}
	if cfg.CloneTimeout == 0 {
		cfg.CloneTimeout = 30 * time.Minute
	}
	if cfg.FetchTimeout == 0 {
		cfg.FetchTimeout = 5 * time.Minute
	}
	if cfg.PushTimeout == 0 {
		cfg.PushTimeout = 5 * time.Minute
	}
	if cfg.OpsTimeout == 0 {
		cfg.OpsTimeout = 60 * time.Second
	}
	return &DefaultRunner{cfg: cfg}
}

// exec 是所有动词的统一执行入口。
//
//	cwd:  绝对路径，作为子进程的工作目录；空字符串表示不切换
//	env:  追加到 os.Environ() 之后的 KEY=VALUE 切片；nil 表示不追加
//	args: git 命令的参数（不含 "git" 本身）
//
// 返回 stdout/stderr 字节切片与错误。错误为 GitError 时携带 stderr 与 exit code。
func (r *DefaultRunner) exec(ctx context.Context, cwd string, env []string, args ...string) (stdout, stderr []byte, err error) {
	// 统一前置 `-c credential.helper=` 清空 helper 链，绕过 Git for Windows
	// 默认 GCM 抢占 PAT 的问题。
	fullArgs := append([]string{"-c", "credential.helper="}, args...)
	cmd := exec.CommandContext(ctx, r.cfg.Binary, fullArgs...)
	if cwd != "" {
		cmd.Dir = cwd
	}
	// base env 总是禁交互；调用方 env 追加在后可覆盖。
	baseEnv := append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GCM_INTERACTIVE=never")
	cmd.Env = append(baseEnv, env...)
	// WaitDelay：超时 kill git 后，孤儿 ssh/git-remote-https 子进程可能持有
	// stderr pipe 写端导致 cmd.Run() 无限阻塞，给一个上限强制收尾。
	cmd.WaitDelay = 10 * time.Second
	var so, se bytes.Buffer
	cmd.Stdout = &so
	cmd.Stderr = &se
	runErr := cmd.Run()
	stdout, stderr = so.Bytes(), se.Bytes()
	if runErr == nil {
		return stdout, stderr, nil
	}
	// 区分超时 / 退出非零 / 二进制找不到
	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
		return stdout, stderr, &GitError{
			Args: args, ExitCode: -1, Stderr: truncate(stderr, stderrTruncateLimit),
			Cause: fmt.Errorf("timeout after %s", deadlineHint(ctx)),
		}
	}
	var ee *exec.ExitError
	if errors.As(runErr, &ee) {
		return stdout, stderr, &GitError{
			Args: args, ExitCode: ee.ExitCode(), Stderr: truncate(stderr, stderrTruncateLimit),
			Cause: runErr,
		}
	}
	return stdout, stderr, &GitError{
		Args: args, ExitCode: -1, Stderr: truncate(stderr, stderrTruncateLimit), Cause: runErr,
	}
}

// withCtx 在 ctx 上挂超时（取 ctx 与 fallback 的较小值）。fallback==0 时不附加超时。
func withCtx(parent context.Context, fallback time.Duration) (context.Context, context.CancelFunc) {
	if fallback == 0 {
		return parent, func() {}
	}
	if d, ok := parent.Deadline(); ok && time.Until(d) < fallback {
		return parent, func() {}
	}
	return context.WithTimeout(parent, fallback)
}

// deadlineHint 给超时错误一个可读时间戳。
func deadlineHint(ctx context.Context) string {
	d, ok := ctx.Deadline()
	if !ok {
		return "n/a"
	}
	return time.Until(d).String()
}

// truncate 裁切 stderr 防爆 audit log。
func truncate(b []byte, limit int) string {
	if len(b) <= limit {
		return string(b)
	}
	return string(b[:limit]) + "...[truncated]"
}

// 让代码在 windows 上也能编译时引用 filepath.Separator（避免 unused import）
var _ = filepath.Separator
var _ = runtime.GOOS
