#!/usr/bin/env bash
# pre-commit: staged-only gofmt + golangci-lint checks. engineering.md §2.1.
#
# **bash 3.2 compatible** (carried from the F3-#3c fix): the default macOS
# bash is 3.2 (Apple's GPLv3 policy keeps it there); avoid bash 4+ syntax
# like mapfile / declare -A and use only bash 3.2 constructs, which also run
# fine on Linux bash 4+/5+.
set -euo pipefail

# Short-circuit in CI environments
if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then exit 0; fi
# Skip silently when tools are missing (do not hold docs-only commits hostage
# to a Go toolchain)
command -v gofmt >/dev/null 2>&1 || exit 0

# bash 3.2 compatible: fill the array with a while-read loop instead of mapfile -t
staged=()
while IFS= read -r line; do
    [ -n "$line" ] && staged+=("$line")
done < <(git diff --cached --name-only --diff-filter=ACM -- '*.go')
[ ${#staged[@]} -eq 0 ] && exit 0

# 1) gofmt formatting check
unformatted=$(gofmt -l "${staged[@]}")
if [ -n "$unformatted" ]; then
    echo "✗ gofmt 未通过:"
    echo "$unformatted"
    echo ""
    # shellcheck disable=SC2086
    echo "fix: gofmt -w $(echo $unformatted | tr '\n' ' ') && git add $(echo $unformatted | tr '\n' ' ')"
    exit 1
fi

# 2) golangci-lint static checks (only for packages touched by staged files)
#
# Group by go.mod boundary: package paths in independent submodules
# (spike/, benchmarks/, ... with their own go.mod) cannot be fed to the main
# module's golangci-lint (it reports "main module does not contain package")
# -- each group must run with cwd at its own module root. Algorithm: for
# every staged .go file walk upward to the nearest go.mod to determine its
# module root, then run packages sharing a root together under that root.
command -v golangci-lint >/dev/null 2>&1 || exit 0

# Find the nearest ancestor directory containing go.mod (the module root)
module_root() {
    local d="$1"
    while [ "$d" != "." ] && [ "$d" != "/" ]; do
        if [ -f "$d/go.mod" ]; then echo "$d"; return; fi
        d=$(dirname "$d")
    done
    echo "."  # fallback: the repository root
}

# bash 3.2 compatible: instead of a declare -A associative array, use a
# "<root>\t<rel> line stream + sort -u, then group by root" scheme. First
# collect the (root, rel) pairs into a tab-separated line array.
pairs=()
for f in "${staged[@]}"; do
    dir=$(dirname "$f")
    root=$(module_root "$dir")
    # Package path relative to the module root
    if [ "$root" = "." ]; then
        rel="./$dir"
    elif [ "$dir" = "$root" ]; then
        rel="."  # the file sits directly in the module root
    else
        rel="./${dir#"$root"/}"
    fi
    pairs+=("$root"$'\t'"$rel")
done

# Sort and dedupe by root, then loop grouping by root
sorted=()
while IFS= read -r line; do
    [ -n "$line" ] && sorted+=("$line")
done < <(printf '%s\n' "${pairs[@]}" | sort -u)

# Group by root: when a new root appears, flush the previous group first
current_root=""
current_rels=()

run_group() {
    local root="$1"
    shift
    local rels=("$@")
    [ ${#rels[@]} -eq 0 ] && return 0
    # Capture output: when a whole package sits behind a non-default build
    # tag (e.g. //go:build wangshu_p3), the default lint reports "build
    # constraints exclude all Go files" -- not a real lint error (the
    # package just is not part of the default build), so let it through.
    # Real lint issues still fail.
    local out
    out=$(cd "$root" && golangci-lint run "${rels[@]}" 2>&1) && return 0
    # It failed: filter out the "build constraints exclude all" false-positive
    # lines and see whether real errors remain
    local real
    real=$(printf '%s\n' "$out" | grep -v "build constraints exclude all Go files" | grep -iE "level=error|\.go:[0-9]+:" || true)
    if [ -z "$real" ]; then
        # Only build-constraint false positives (package behind another
        # build tag) -> let it through
        return 0
    fi
    echo "$out"
    echo ""
    echo "✗ golangci-lint 未通过(module: $root)"
    return 1
}

for line in "${sorted[@]}"; do
    root="${line%%$'\t'*}"
    rel="${line#*$'\t'}"
    if [ "$root" != "$current_root" ]; then
        # Root changed: run the previous group (if any)
        if [ -n "$current_root" ]; then
            run_group "$current_root" "${current_rels[@]}" || exit 1
        fi
        current_root="$root"
        current_rels=()
    fi
    current_rels+=("$rel")
done
# Run the final group
if [ -n "$current_root" ]; then
    run_group "$current_root" "${current_rels[@]}" || exit 1
fi
