package git

import (
	"context"
	"fmt"
	"strings"
)

// diffMaxBytes caps the raw unified-diff stdout to keep MCP payloads / memory
// bounded. Larger diffs are truncated and flagged via DiffResult.Truncated.
const diffMaxBytes = 1 << 20 // 1 MiB

// DiffOptions controls `git diff` argument building.
//
//   - Ref/Against both empty => working-tree vs HEAD (the default).
//   - Ref set, Against empty => `git diff <Ref>` (working-tree vs Ref).
//   - Ref and Against both set => `git diff <Ref>..<Against>` (two refs).
//   - Staged => add `--cached` (index vs HEAD); mutually composes with refs.
//   - Paths => restrict to the given pathspecs (passed after `--`).
//   - ContextLines => `-U<n>` when > 0 (git default 3 otherwise).
type DiffOptions struct {
	Ref          string
	Against      string
	Staged       bool
	Paths        []string
	ContextLines int
}

// DiffResult carries the raw unified-diff text. The viewer parses it; the git
// layer never parses the hunks. Truncated reports whether stdout exceeded the
// 1 MiB cap and was cut.
type DiffResult struct {
	Diff      string
	Truncated bool
}

// Diff returns the unified diff of dir per opts. The output is the raw `git
// diff --no-color` text (capped at 1 MiB). On a clean tree the diff is empty.
func (r *DefaultRunner) Diff(ctx context.Context, dir string, opts DiffOptions) (DiffResult, error) {
	// 安全护栏：ref/against 来自 MCP/HTTP 入参，可能未经分支校验。以 '-' 开头的值会被
	// git 当作选项解析（如 --output=、-O<file>），故一律拒绝，杜绝参数注入。
	if strings.HasPrefix(opts.Ref, "-") || strings.HasPrefix(opts.Against, "-") {
		return DiffResult{}, fmt.Errorf("git diff: ref/against must not begin with '-': %q %q", opts.Ref, opts.Against)
	}
	ctx, cancel := withCtx(ctx, r.cfg.OpsTimeout)
	defer cancel()
	args := buildDiffArgs(opts)
	out, _, err := r.exec(ctx, dir, nil, args...)
	if err != nil {
		return DiffResult{}, err
	}
	res := DiffResult{}
	if len(out) > diffMaxBytes {
		out = out[:diffMaxBytes]
		res.Truncated = true
	}
	res.Diff = string(out)
	return res, nil
}

// buildDiffArgs assembles the `git diff` argument slice from opts. Refs are
// never user-supplied paths (workspace package validates branches), so they are
// passed positionally; pathspecs go after the `--` separator to disambiguate.
func buildDiffArgs(opts DiffOptions) []string {
	args := []string{"diff", "--no-color"}
	if opts.Staged {
		args = append(args, "--cached")
	}
	if opts.ContextLines > 0 {
		args = append(args, "-U"+itoa(opts.ContextLines))
	}
	switch {
	case opts.Ref != "" && opts.Against != "":
		args = append(args, opts.Ref+".."+opts.Against)
	case opts.Ref != "":
		args = append(args, opts.Ref)
	}
	if len(opts.Paths) > 0 {
		args = append(args, "--")
		args = append(args, opts.Paths...)
	}
	return args
}

// itoa is a tiny strconv.Itoa avoiding an import just for one call site.
func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	neg := n < 0
	if neg {
		n = -n
	}
	var buf [20]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	if neg {
		i--
		buf[i] = '-'
	}
	return string(buf[i:])
}
