#!/bin/bash
#
#  This file is part of the OmniDiff code diffing tool.
#
#  Copyright (C) 2026 Marko Ivankovic
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as published
#  by the Free Software Foundation, version 3 of the License.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# Two jobs: format what is being committed (cargo fmt, ruff format), then regenerate
# src/test/data/diffs.csv when the commit touches the fixture corpus.
#
# Nothing unstaged is ever staged: a file with further unstaged changes is reported and left
# alone. rustfmt cannot format one file without the modules it declares, so `cargo fmt --all`
# runs and any other file it touches is listed, not staged.
#
# Enable with `make install-hooks`; skip once with `git commit --no-verify`.

set -euo pipefail
cd "$(git rev-parse --show-toplevel)"

# ── 1. Formatting ───────────────────────────────────────────────────────────────────────────────

# Staged files, and the subset of them that carry *no* further unstaged changes - the only files
# this hook will ever re-stage.
staged="$(git diff --cached --name-only --diff-filter=ACMR)"
unstaged_too="$(git diff --name-only)"
safe_to_restage() {
    local file="$1"
    ! grep -qxF "$file" <<<"$unstaged_too"
}

staged_rust="$(grep -E '\.rs$' <<<"$staged" || true)"
if [ -n "$staged_rust" ]; then
    # The cheap path: the crate is already formatted, which is almost every commit.
    if ! cargo fmt --all --check >/dev/null 2>&1; then
        echo "pre-commit: running cargo fmt --all"
        before="$(git diff --name-only; git diff --cached --name-only)"
        cargo fmt --all
        restaged=()
        while IFS= read -r file; do
            [ -n "$file" ] || continue
            if safe_to_restage "$file" && git diff --quiet -- "$file"; then
                continue  # untouched by the formatter
            fi
            if safe_to_restage "$file"; then
                git add -- "$file"
                restaged+=("$file")
            fi
        done <<<"$staged_rust"
        [ ${#restaged[@]} -eq 0 ] || echo "pre-commit: reformatted and staged ${restaged[*]}"
        # Anything else the formatter reached: not staged by this hook, but say so out loud.
        collateral="$(git diff --name-only | grep -E '\.rs$' | grep -vxF "$before" || true)"
        if [ -n "$collateral" ]; then
            echo "pre-commit: cargo fmt also reformatted files this commit does not stage:" >&2
            sed 's/^/    /' <<<"$collateral" >&2
            echo "  (rustfmt formats the modules a file declares; they are left for you to review)" >&2
        fi
    fi
fi

staged_python="$(grep -E '\.py$' <<<"$staged" || true)"
if [ -n "$staged_python" ]; then
    if ! command -v ruff >/dev/null 2>&1; then
        echo "pre-commit: ruff not on PATH - skipping Python formatting (CI still checks it)" >&2
    else
        formattable=()
        skipped=()
        while IFS= read -r file; do
            [ -n "$file" ] || continue
            if safe_to_restage "$file"; then
                formattable+=("$file")
            else
                skipped+=("$file")
            fi
        done <<<"$staged_python"
        if [ ${#formattable[@]} -gt 0 ]; then
            ruff format --quiet "${formattable[@]}"
            for file in "${formattable[@]}"; do
                git diff --quiet -- "$file" || {
                    git add -- "$file"
                    echo "pre-commit: reformatted and staged $file"
                }
            done
        fi
        if [ ${#skipped[@]} -gt 0 ]; then
            echo "pre-commit: not formatting ${skipped[*]} - staged with further unstaged changes" >&2
        fi
    fi
fi

# ── 2. Fixture inventory ────────────────────────────────────────────────────────────────────────

# What the inventory is derived from, and therefore what has to trigger it. `sample.csv` is in the
# list because the inventory joins its provenance and comment columns per fixture, so editing a
# comment there changes the CSV just as surely as adding a fixture does.
CORPUS=("src/test/data/diffs" "src/test/data/sample.csv")
INVENTORY="src/test/data/diffs.csv"

# ACDMR: added, copied, deleted, modified, renamed - every way a fixture can change. `--cached`,
# so this asks what is being committed rather than what happens to be lying around.
staged_fixture_changes="$(git diff --cached --name-only --diff-filter=ACDMR -- "${CORPUS[@]}")"
if [ -z "$staged_fixture_changes" ]; then
    exit 0
fi

# The hook regenerates from the *working tree*, but the commit records the *index*. Those are the
# same thing only when every fixture change is staged. When they differ, the CSV written here would
# describe a corpus different from the one being committed - a wrong inventory that looks right,
# which is precisely the failure this hook exists to prevent. So it stops rather than guessing.
unstaged="$(git diff --name-only -- "${CORPUS[@]}")"
untracked="$(git ls-files --others --exclude-standard -- "${CORPUS[@]}")"
if [ -n "$unstaged$untracked" ]; then
    echo "pre-commit: this commit changes the corpus, but some of its files are not staged:" >&2
    # shellcheck disable=SC2001  # one sed is clearer here than a read loop
    echo "$unstaged$untracked" | sed 's/^/    /' >&2
    cat >&2 <<'EOF'

The inventory is regenerated from the working tree, so committing now would record a
src/test/data/diffs.csv describing a corpus different from the one in this commit.

Either stage them too:
    git add src/test/data/diffs src/test/data/sample.csv
or commit the fixtures separately from the rest of your work.

To skip this check deliberately, commit with --no-verify and run `make diff-inventory` yourself
afterwards.
EOF
    exit 1
fi

echo "pre-commit: fixtures changed, regenerating $INVENTORY"
# The first run on a clean checkout compiles the fat-LTO release binary: expect minutes.
if ! make --quiet diff-inventory; then
    echo "pre-commit: diff_inventory failed - see the error above" >&2
    echo "  (commit with --no-verify to bypass, then fix the inventory separately)" >&2
    exit 1
fi

git add "$INVENTORY"
echo "pre-commit: staged $INVENTORY"
