#!/bin/sh
# script/update-policy: replace this repo's vendored policy files with
# the canonical ones from the prompts repo, commit them and push.
# Identical in all repos. Our own extension to
# scripts-to-rule-them-all.
#
# Usage: script/update-policy [full commit hash of prompts]
# Without a hash it uses the head of the prompts main branch.
#
# The script writes nothing until every file has been fetched and
# merged. A repo's own entries are kept:
#   - .gitignore, .editorconfig, .dockerignore: the lines below the
#     canonical file's last line
#   - .golangci.yml: the entries the repo added to the deny list of the
#     test-support rule
set -eu

ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
PROMPTS_URL="https://git.eeqj.de/sneak/prompts"

# Paths in the prompts repo. A file this repo does not carry is skipped.
VENDORED_FILES="
.dockerignore
.editorconfig
.gitea/workflows/check.yml
.gitignore
.golangci.yml
.prettierrc
prompts/API_POLICIES.md
prompts/CODE_STYLEGUIDE.md
prompts/CODE_STYLEGUIDE_GO.md
prompts/CODE_STYLEGUIDE_JS.md
prompts/CODE_STYLEGUIDE_PYTHON.md
prompts/EXISTING_REPO_CHECKLIST.md
prompts/GO_HTTP_SERVER_CONVENTIONS.md
prompts/LLM_PROSE_TELLS.md
prompts/NEW_REPO_CHECKLIST.md
prompts/REPO_POLICIES.md
"

die() {
    echo "update-policy: $*" >&2
    exit 1
}

# local_path <path in prompts>: print where this repo keeps the file, or
# nothing if git does not track it here. A prompts/NAME.md lands at
# docs/NAME.md or NAME.md.
local_path() {
    name="${1#prompts/}"
    if [ "$name" != "$1" ] && [ -n "$(git ls-files -- "docs/$name")" ]; then
        echo "docs/$name"
    elif [ -n "$(git ls-files -- "$name")" ]; then
        echo "$name"
    fi
}

# keep_lines_below_last <fetched> <local>: append to the fetched file
# whatever follows its last line in the local file.
keep_lines_below_last() {
    last="$(tail -n 1 "$1")"
    number="$(grep -n -F -x -- "$last" "$2" | head -n 1 | cut -d: -f1)"
    if [ -z "$last" ] || [ -z "$number" ]; then
        die "$2 lacks this line; add it and put the repo's own entries below it: $last"
    fi
    tail -n "+$((number + 1))" "$2" >> "$1"
}

# keep_deny_entries <fetched> <local>: add to the deny list of the
# test-support rule in the fetched file the entries only the local file
# has. An entry is a "- pkg:" line, the lines under it and any comment
# above it.
keep_deny_entries() {
    awk '
        function indent(line) { return match(line, /[^ ]/) - 1 }
        function print_local_entries(i) {
            for (i = 1; i <= count; i++)
                if (!(pkg[i] in fetched_pkg))
                    printf "%s", entry[i]
        }

        FNR == 1 { in_rule = 0; in_list = 0 }

        in_list && /[^ ]/ && indent($0) <= list_indent {
            in_list = 0
            if (FILENAME == ARGV[2]) print_local_entries()
        }

        in_list {
            is_entry = ($1 == "-" && $2 == "pkg:")
            if (FILENAME == ARGV[2]) {
                if (is_entry) fetched_pkg[$3]
                print
            } else if (is_entry) {
                count++
                pkg[count] = $3
                entry[count] = comment $0 "\n"
                comment = ""
            } else if (/^ *#/) {
                comment = comment $0 "\n"
            } else if (count) {
                entry[count] = entry[count] $0 "\n"
            }
            next
        }

        /^ *test-support:/ { in_rule = 1 }
        in_rule && /^ *deny:/ {
            in_rule = 0
            in_list = 1
            list_indent = indent($0)
            if (FILENAME == ARGV[2]) found = 1
        }
        FILENAME == ARGV[2] { print }

        END {
            if (in_list) print_local_entries()
            exit !found
        }
    ' "$2" "$1" > "$1.merged" ||
        die "the fetched $2 has no deny list in its test-support rule"
    mv "$1.merged" "$1"
}

main() {
    cd "$ROOT"

    [ "$#" -le 1 ] || die "usage: script/update-policy [full commit hash]"

    project="$(script/projectname)"
    if [ "$project" = "prompts" ]; then
        die "this repo is the source of the policy files"
    fi

    commit="${1:-}"
    if [ -z "$commit" ]; then
        head_line="$(git ls-remote "$PROMPTS_URL.git" refs/heads/main)"
        commit="${head_line%%[[:space:]]*}"
    fi
    printf '%s\n' "$commit" | grep -Eqx '[0-9a-f]{40}' ||
        die "not a full commit hash: '$commit'"

    tmp="$(mktemp -d)"
    trap 'rm -rf "$tmp"' EXIT

    carried=""
    for file in $VENDORED_FILES; do
        path="$(local_path "$file")"
        [ -n "$path" ] || continue
        if [ -n "$(git status --porcelain -- "$path")" ]; then
            die "$path has uncommitted changes"
        fi
        mkdir -p "$tmp/$(dirname "$path")"
        curl --fail --silent --show-error --max-time 60 \
            --output "$tmp/$path" \
            "$PROMPTS_URL/raw/commit/$commit/$file" ||
            die "cannot fetch $file at $commit"
        carried="$carried $path"
    done
    [ -n "$carried" ] || die "this repo carries none of the vendored files"

    for path in $carried; do
        case "$path" in
            .gitignore | .editorconfig | .dockerignore)
                keep_lines_below_last "$tmp/$path" "$path"
                ;;
            .golangci.yml)
                keep_deny_entries "$tmp/$path" "$path"
                ;;
        esac
    done

    for path in $carried; do
        cp "$tmp/$path" "$path"
        git add -- "$path"
    done

    if git diff --cached --quiet -- $carried; then
        echo "update-policy: already at prompts $commit, nothing to commit"
        exit 0
    fi

    git commit -m "Update policy files from prompts $commit" -- $carried
    git push origin HEAD ||
        die "push failed; the commit exists locally, so fix that and run git push"
}

main "$@"
