package precommit

import (
	"os"
	"path"
	"path/filepath"
	"runtime"
	"strings"
)

// configNames lists accepted pre-commit config filenames, in priority order.
var configNames = []string{".pre-commit-config.yaml", ".pre-commit-config.yml"}

// ConfigFile returns the path to the pre-commit config in root, and whether one exists.
func ConfigFile(root string) (string, bool) {
	for _, name := range configNames {
		p := filepath.Join(root, name)
		if info, err := os.Stat(p); err == nil && !info.IsDir() {
			return p, true
		}
	}
	return "", false
}

// ToolVersion returns the installed pre-commit version (e.g. "3.7.0") and whether
// the tool is available.
func ToolVersion(r CommandRunner) (string, bool) {
	// Read stdout only: if pre-commit ever prints a warning to stderr, it must
	// not leak into the parsed version token.
	out, err := r.RunStdout("", "pre-commit", "--version")
	if err != nil {
		return "", false
	}
	fields := strings.Fields(strings.TrimSpace(out))
	if len(fields) >= 2 {
		return fields[1], true
	}
	if len(fields) == 1 {
		// Output had only the program name with no version token. Treat the
		// tool as installed with an unknown version rather than mislabeling the
		// program name as a version string.
		return "", true
	}
	return "", false
}

// hookSignature is the marker the pre-commit tool writes into the hook file it
// generates. Anchoring on this avoids treating a foreign/custom hook that merely
// mentions "pre-commit" as one managed by the tool.
const hookSignature = "generated by pre-commit"

// HookType identifies a git hook managed by pre-commit.
type HookType string

const (
	// HookTypePreCommit runs before a commit is created.
	HookTypePreCommit HookType = "pre-commit"
	// HookTypePrePush runs before commits are pushed.
	HookTypePrePush HookType = "pre-push"
)

var requiredHookTypes = []HookType{HookTypePreCommit, HookTypePrePush}

// HookInstalled reports whether the repository's pre-commit git hook exists and was
// generated by the pre-commit tool. root is the repository root; r is used to
// resolve the real hooks path so git worktrees (where .git is a file) work.
func HookInstalled(r CommandRunner, root string) bool {
	return HookTypeInstalled(r, root, HookTypePreCommit)
}

// HookTypeInstalled reports whether hookType exists and is managed by pre-commit.
func HookTypeInstalled(r CommandRunner, root string, hookType HookType) bool {
	if !isRequiredHookType(hookType) {
		return false
	}
	hook := hookPath(r, root, hookType)
	info, err := os.Stat(hook)
	if err != nil || info.IsDir() {
		return false
	}
	if runtime.GOOS != "windows" && info.Mode().Perm()&0o100 == 0 {
		return false
	}
	data, err := os.ReadFile(hook)
	if err != nil {
		return false
	}
	content := string(data)
	return strings.Contains(content, hookSignature) &&
		strings.Contains(content, "--hook-type="+string(hookType))
}

func isRequiredHookType(hookType HookType) bool {
	for _, required := range requiredHookTypes {
		if hookType == required {
			return true
		}
	}
	return false
}

// hookPath resolves a managed hook path, honoring git worktrees.
func hookPath(r CommandRunner, root string, hookType HookType) string {
	gitPath := path.Join("hooks", string(hookType))
	out, err := r.Run(root, "git", "rev-parse", "--git-path", gitPath)
	if err == nil {
		if p := strings.TrimSpace(out); p != "" {
			if !filepath.IsAbs(p) {
				p = filepath.Join(root, p)
			}
			// git (notably under git-bash on Windows) may return forward
			// slashes; normalize to the OS separator and remove any "..".
			return filepath.Clean(p)
		}
	}
	return filepath.Join(root, ".git", "hooks", string(hookType))
}