#!/usr/bin/env bash
# Cut a release on the Mac (ADR 0024, ADR 0125, docs/release-checklist.md): tag -> draft ->
# kits and player builds -> publish. Since ADR 0126 a merged release pull request (dev into
# main, "Release vX.Y.Z") does `start` and the kits on hosted CI (.github/workflows/release.yml);
# the player builds need work/gen, so they are built here and on the Windows PC
# (scripts/release.ps1) with --players-only. Without it, every step runs here as before.
#
#   start TAG    checks: annotated tag vX.Y.Z[-rc.N], clean tree on main, tag on origin/main
#                and pushed, scripts/local-ci green on it; then creates the draft release
#                with the committed header (.github/release-notes-header.md: what the downloads
#                hold, bring your own IPA) above generated notes (.github/release.yml), a
#                pre-release for -rc.N
#   build TAG    the macOS universal and Linux x86_64 kits from a clean checkout of the tag
#                (scripts/local-ci TAG --release, with MAPLETHIEF_VERSION from the tag), and the
#                player builds (ADR 0125): the mac-player workflow on a clean clone of the tag
#                with this checkout's work/gen linked in, and the Linux tarball in the
#                linux-game container (scripts/linux-game player TAG); checks each (lipo,
#                codesign, `--version` reports the tag; Linux in the container), runs the
#                content guard (kit mode for kits, player mode for player builds), writes
#                <file>.sha256, uploads them to the draft; --players-only: the player builds
#                only (the kits come from release.yml)
#   publish TAG  checks that every platform's kit, player build and .sha256 is on the draft,
#                downloads them, verifies them, merges SHA256SUMS (`shasum -a 256 -c` must
#                pass), runs the guard again, uploads SHA256SUMS and publishes the release
#
# Usage: scripts/release start|build|publish TAG [--dry-run] [--replace] [--players-only]
#                                                [--platforms LIST] [--build-arg NAME=VALUE]...
# (the options are described in usage() below, printed by scripts/release --help)
#
# Everything lands in build/release/<TAG>/. --dry-run does every local step and prints the
# GitHub commands instead of running them (the pushed/main checks only warn), so the flow can
# be checked on a local tag. Environment: PYTHON=<interpreter> (3.10+) runs mtport.release.
set -euo pipefail

REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO"

usage() {
    cat >&2 <<'EOF'
Usage: scripts/release start|build|publish TAG [--dry-run] [--replace] [--players-only]
                                               [--platforms LIST] [--build-arg NAME=VALUE]...

  start TAG        check the tag and the tree, run scripts/local-ci on the tag, create the
                   draft release with the notes header and generated notes
  build TAG        build, check, guard and checksum the kits and player builds of this Mac
                   (macOS universal, Linux x86_64) and upload them to the draft
  publish TAG      check every platform's kit and player build is on the draft, write
                   SHA256SUMS, publish
  TAG              vX.Y.Z or vX.Y.Z-rc.N (a pre-release), an annotated tag

  --dry-run        no GitHub calls (they are printed); pushed/main checks only warn;
                   publish uses the kits in build/release/TAG/ instead of downloading
  --replace        build/publish: replace assets already on the draft (gh --clobber)
  --players-only   build: the player builds only; release.yml built and uploaded the kits
  --platforms LIST build: mac,linux (default both); publish: the platforms that must be
                   present, of mac,linux,windows (default all three)
  --build-arg A=B  build: passed to scripts/local-ci / scripts/test-linux for the image
EOF
    exit "${1:-2}"
}

die() { echo "scripts/release: $*" >&2; exit 1; }
step() { echo; echo "== $*"; }

[ "$#" -ge 1 ] || usage
case "$1" in
    start | build | publish) cmd="$1"; shift ;;
    -h | --help) usage 0 ;;
    *) echo "scripts/release: unknown command: $1" >&2; usage ;;
esac
tag=""
dry_run=0
replace=0
players_only=0
platforms=""
build_args=()
while [ "$#" -gt 0 ]; do
    case "$1" in
        --dry-run) dry_run=1; shift ;;
        --replace) replace=1; shift ;;
        --players-only) players_only=1; shift ;;
        --platforms)
            [ "$#" -ge 2 ] || usage
            platforms="$2"
            shift 2
            ;;
        --build-arg)
            [ "$#" -ge 2 ] || usage
            build_args+=(--build-arg "$2")
            shift 2
            ;;
        -h | --help) usage 0 ;;
        -*) echo "scripts/release: unknown option: $1" >&2; usage ;;
        *)
            [ -z "$tag" ] || usage
            tag="$1"
            shift
            ;;
    esac
done
[ -n "$tag" ] || usage

PY="${PYTHON:-python3}"
command -v "$PY" >/dev/null 2>&1 || die "need Python 3.10+, but '$PY' was not found (set PYTHON)"
"$PY" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' 2>/dev/null ||
    die "need Python 3.10+, but '$PY' is older (set PYTHON=/path/to/python3)"
# mtport.release from this checkout; -B: no __pycache__ in tools/.
mtrelease() { PYTHONPATH="$REPO/tools" "$PY" -B -m mtport.release "$@"; }

# The tag: its version and whether it is a pre-release.
parsed="$(mtrelease tag "$tag")" || die "bad tag"
version="$(sed -n 1p <<<"$parsed")"
prerelease=0
[ "$(sed -n 2p <<<"$parsed")" = prerelease ] && prerelease=1
out="build/release/$tag"
mkdir -p "$out"

# gh ARGS...: run gh, or print it with --dry-run.
gh_run() {
    if [ "$dry_run" -eq 1 ]; then
        echo "   dry-run, not run: gh $*"
    else
        echo "+ gh $*"
        gh "$@"
    fi
}
# check_or_warn MESSAGE COMMAND...: fail unless COMMAND succeeds; with --dry-run only warn.
check_or_warn() {
    local message="$1"
    shift
    if "$@"; then
        echo "   ok: $message"
    elif [ "$dry_run" -eq 1 ]; then
        echo "   WARNING (dry-run, would fail): $message"
    else
        die "check failed: $message"
    fi
}

check_tag() {
    git rev-parse -q --verify "refs/tags/$tag" >/dev/null || die "no local tag $tag"
    [ "$(git cat-file -t "refs/tags/$tag")" = tag ] ||
        die "$tag is a lightweight tag; create it with git tag -a $tag -m ..."
    commit="$(git rev-parse "$tag^{commit}")"
    short="$(git rev-parse --short=12 "$commit")"
    echo "   tag $tag ($version$([ "$prerelease" -eq 1 ] && echo ', pre-release')): $short $(git log -1 --format=%s "$commit")"
}
tag_pushed() {
    local remote
    remote="$(git ls-remote --tags origin "refs/tags/$tag" | cut -f1)"
    [ -n "$remote" ] && [ "$remote" = "$(git rev-parse "refs/tags/$tag")" ]
}
on_origin_main() { git merge-base --is-ancestor "$commit" origin/main 2>/dev/null; }
draft_exists() {
    [ "$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)" = true ]
}
check_draft() {
    if [ "$dry_run" -eq 1 ]; then
        echo "   dry-run: not checking that the draft release $tag exists"
    else
        draft_exists || die "no draft release $tag; run scripts/release start $tag first"
        echo "   ok: draft release $tag exists"
    fi
}

cmd_start() {
    step "Checks"
    check_tag
    [ -z "$(git status --porcelain --untracked-files=no)" ] || die "the working tree has changes"
    echo "   ok: clean working tree"
    [ "$dry_run" -eq 1 ] || git fetch --quiet origin main
    check_or_warn "on branch main" test "$(git rev-parse --abbrev-ref HEAD)" = main
    check_or_warn "$tag is on origin/main" on_origin_main
    check_or_warn "$tag is pushed to origin (same tag object)" tag_pushed
    if [ "$dry_run" -eq 0 ] && gh release view "$tag" >/dev/null 2>&1; then
        die "a release $tag exists already"
    fi

    step "scripts/local-ci $tag"
    scripts/local-ci "$tag" ${build_args[@]+"${build_args[@]}"} || die "scripts/local-ci failed on $tag"

    step "Draft release"
    # gh puts the --notes-file text above the generated notes (ADR 0125).
    local args=(release create "$tag" --draft --verify-tag --notes-file .github/release-notes-header.md
        --generate-notes --title "MapleThief $version")
    [ "$prerelease" -eq 1 ] && args+=(--prerelease)
    gh_run "${args[@]}"
    echo
    printf "%s\n" "Next: scripts/release build $tag (this Mac), scripts\\release.ps1 build $tag (Windows PC),"
    echo "      then scripts/release publish $tag."
}

# check_mac_kit KIT: unpack it (a kit or a player build: both have MapleThief.app at the top),
# check the universal binary, the signature and --version.
check_mac_kit() {
    local kit="$1" tmp app bin archs got
    tmp="$(mktemp -d "$REPO/build/release/unpack.XXXXXX")"
    ditto -x -k "$kit" "$tmp"
    app="$tmp/$(basename "$kit" .zip)/MapleThief.app"
    bin="$app/Contents/MacOS/MapleThief"
    archs="$(lipo -archs "$bin")"
    local universal=1
    case " $archs " in *" arm64 "*) ;; *) universal=0 ;; esac
    case " $archs " in *" x86_64 "*) ;; *) universal=0 ;; esac
    if [ "$universal" -eq 0 ]; then
        rm -rf "$tmp"
        die "$kit: not universal (lipo -archs: $archs)"
    fi
    echo "   lipo -archs: $archs"
    codesign --verify --deep --strict "$app" || { rm -rf "$tmp"; die "$kit: codesign --verify failed"; }
    echo "   codesign --verify --deep --strict: ok ($(codesign -dv "$app" 2>&1 | grep -E '^Signature=' || true))"
    got="$("$bin" --version)"
    rm -rf "$tmp"
    [ "$got" = "MapleThief $version" ] || die "$kit: --version printed '$got', expected 'MapleThief $version'"
    echo "   --version: $got"
}

# check_linux_kit KIT: unpack and run --version in the Linux CI container (x86_64).
check_linux_kit() {
    local kit="$1" image got
    image="$(scripts/test-linux --image-name ${build_args[@]+"${build_args[@]}"})"
    got="$(docker run --rm --platform linux/amd64 -v "$(cd "$(dirname "$kit")" && pwd -P):/kits:ro" \
        "$image" bash -c 'set -eu; mkdir /tmp/kit; untar "/kits/$1" /tmp/kit
            file="$(ls /tmp/kit/*/bin/MapleThief)"
            echo "arch $(uname -m)" >&2
            "$file" --version' bash "$(basename "$kit")")"
    [ "$got" = "MapleThief $version" ] || die "$kit: --version printed '$got', expected 'MapleThief $version'"
    echo "   --version (in $image): $got"
}

# build_mac_player: the mac-player workflow (ADR 0125) on a clean clone of the tag, with this
# checkout's work/gen linked in (read only; no bundle: the player build has none). Appends the
# checked zip to `players`.
build_mac_player() {
    local src="$out/player-src" name="MapleThief-$version-macos-universal.zip"
    rm -rf "$src"
    git clone --quiet --shared --no-checkout "$REPO" "$src"
    git -C "$src" -c filter.lfs.process= -c filter.lfs.smudge=cat -c filter.lfs.required=false \
        -c advice.detachedHead=false checkout --quiet "$commit"
    # The same IPA guard as CI and scripts/local-ci (ADR 0100).
    if find "$src" -path "$src/.git" -prune -o -iname '*.ipa' -print | grep -q .; then
        die "IPA guard: an *.ipa is in the checkout of $tag"
    fi
    mkdir -p "$src/work" "$src/build"
    ln -s "$REPO/work/gen" "$src/work/gen"
    ln -s "$REPO/build/_downloads" "$src/build/_downloads"
    echo "-- cmake --workflow --preset mac-player (MAPLETHIEF_VERSION=$version) in $src"
    (cd "$src" && CI=true MAPLETHIEF_VERSION="$version" cmake --workflow --preset mac-player) ||
        die "the mac-player workflow failed on $tag"
    [ -f "$src/build/mac-player/$name" ] || die "the mac-player workflow wrote no $name"
    cp "$src/build/mac-player/$name" "$out/$name"
    rm -rf "$src"
    echo "-- $out/$name ($(wc -c <"$out/$name" | tr -d ' ') bytes)"
    check_mac_kit "$out/$name"
    players+=("$out/$name")
}

# build_linux_player: the Linux player tarball in the linux-game container (glibc 2.39 floor,
# ADR 0065) from a clean export of the tag; --version checked in the CI container.
build_linux_player() {
    local dir="$out/linux-player" name="MapleThief-$version-linux-x86_64.tar.gz"
    rm -rf "$dir"
    MAPLETHIEF_VERSION="$version" scripts/linux-game player "$tag" --arch amd64 --out "$dir" ||
        die "scripts/linux-game player failed on $tag"
    [ -f "$dir/$name" ] || die "scripts/linux-game player wrote no $dir/$name"
    cp "$dir/$name" "$out/$name"
    echo "-- $out/$name ($(wc -c <"$out/$name" | tr -d ' ') bytes)"
    check_linux_kit "$out/$name"
    players+=("$out/$name")
}

cmd_build() {
    [ "$(uname -s)" = Darwin ] || die "build runs on the Mac; on Windows use scripts\\release.ps1 build"
    [ -f work/gen/gen.cmake ] ||
        die "no work/gen/gen.cmake for the player builds: scripts/bootstrap, then python3 -m recomp all"
    local want="${platforms:-mac,linux}" jobs="lint,py-min" kits=() p
    IFS=, read -r -a plist <<<"$want"
    for p in "${plist[@]}"; do
        case "$p" in
            mac | linux) jobs="$jobs,$p" ;;
            *) die "--platforms: build makes mac and linux kits here, not '$p'" ;;
        esac
    done

    step "Checks"
    check_tag
    if [ "$dry_run" -eq 0 ]; then
        tag_pushed || die "$tag is not pushed to origin"
        echo "   ok: $tag is pushed"
    fi
    check_draft

    local ci_dir="build/local-ci/$short-release"
    if [ "$players_only" -eq 1 ]; then
        echo "   --players-only: the kits are release.yml's"
    else
        step "scripts/local-ci $tag --release --only $jobs (MAPLETHIEF_VERSION=$version)"
        # A stale Linux kit of this version must not be mistaken for this run's.
        rm -f build/linux-release/MapleThief-kit-"$version"-*
        MAPLETHIEF_VERSION="$version" scripts/local-ci "$tag" --release --only "$jobs" \
            ${build_args[@]+"${build_args[@]}"} || die "scripts/local-ci --release failed on $tag"
        step "Kits"
    fi
    for p in "${plist[@]}"; do
        [ "$players_only" -eq 0 ] || break
        local name
        case "$p" in
            mac) name="MapleThief-kit-$version-macos-universal.zip" ;;
            linux) name="MapleThief-kit-$version-linux-x86_64.tar.gz" ;;
        esac
        [ -f "$ci_dir/$name" ] || die "scripts/local-ci wrote no $ci_dir/$name"
        cp "$ci_dir/$name" "$out/$name"
        echo "-- $out/$name ($(wc -c <"$out/$name" | tr -d ' ') bytes)"
        case "$p" in
            mac) check_mac_kit "$out/$name" ;;
            linux) check_linux_kit "$out/$name" ;;
        esac
        kits+=("$out/$name")
    done

    step "Player builds (ADR 0125)"
    local players=()
    for p in "${plist[@]}"; do
        case "$p" in
            mac) build_mac_player ;;
            linux) build_linux_player ;;
        esac
    done

    step "Content guard"
    if [ "${#kits[@]}" -gt 0 ]; then
        mtrelease guard "${kits[@]}" || die "the content guard rejected a kit; nothing was uploaded"
    fi
    mtrelease guard --mode player "${players[@]}" ||
        die "the content guard rejected a player build; nothing was uploaded"

    step "Checksums"
    mtrelease sha256 ${kits[@]+"${kits[@]}"} "${players[@]}"

    step "Upload to the draft"
    local files=() k
    for k in ${kits[@]+"${kits[@]}"} "${players[@]}"; do files+=("$k" "$k.sha256"); done
    local args=(release upload "$tag" "${files[@]}")
    [ "$replace" -eq 1 ] && args+=(--clobber)
    gh_run "${args[@]}"
}

cmd_publish() {
    # Per platform its kit and its player build (ADR 0125); kit_names and player_names list
    # the same platforms in the same order.
    local want="${platforms:-mac,linux,windows}" names=() kits=() players=() kit_all player_all p
    local key name
    kit_all="$(mtrelease kits "$version")"
    player_all="$(mtrelease players "$version")"
    IFS=, read -r -a plist <<<"$want"
    for p in "${plist[@]}"; do
        case "$p" in
            mac) key='-macos-' ;;
            linux) key='-linux-' ;;
            windows) key='-windows-' ;;
            *) die "--platforms: unknown platform '$p' (mac, linux, windows)" ;;
        esac
        kits+=("$(grep -- "$key" <<<"$kit_all")")
        players+=("$(grep -- "$key" <<<"$player_all")")
    done
    names=("${kits[@]}" "${players[@]}")

    step "Checks"
    check_tag
    check_draft
    local dir="$out"
    if [ "$dry_run" -eq 0 ]; then
        local assets
        assets="$(gh release view "$tag" --json assets --jq '.assets[].name')"
        for name in "${names[@]}"; do
            for f in "$name" "$name.sha256"; do
                grep -qxF -- "$f" <<<"$assets" || die "the draft has no $f"
            done
        done
        echo "   ok: every expected kit, player build and .sha256 is on the draft"
        if grep -qxF SHA256SUMS <<<"$assets" && [ "$replace" -eq 0 ]; then
            die "the draft has a SHA256SUMS already (--replace to replace it)"
        fi
        dir="$out/download"
        rm -rf "$dir"
        mkdir -p "$dir"
        for name in "${names[@]}"; do
            gh release download "$tag" --dir "$dir" --pattern "$name" --pattern "$name.sha256"
        done
    else
        echo "   dry-run: using the files in $out/ instead of downloading the draft's"
        for name in "${names[@]}"; do
            [ -f "$out/$name" ] && [ -f "$out/$name.sha256" ] ||
                die "no $out/$name or its .sha256 (scripts/release build $tag --dry-run)"
        done
    fi

    step "Verify each file against its .sha256"
    for name in "${names[@]}"; do
        mtrelease check "$dir/$name.sha256" || die "$name does not match $name.sha256"
    done

    step "Content guard"
    local paths=()
    for name in "${kits[@]}"; do paths+=("$dir/$name"); done
    mtrelease guard "${paths[@]}" || die "the content guard rejected a kit"
    paths=()
    for name in "${players[@]}"; do paths+=("$dir/$name"); done
    mtrelease guard --mode player "${paths[@]}" || die "the content guard rejected a player build"

    step "SHA256SUMS"
    local sums=()
    for name in "${names[@]}"; do sums+=("$dir/$name.sha256"); done
    mtrelease sums "$dir/SHA256SUMS" "${sums[@]}"
    (cd "$dir" && shasum -a 256 -c SHA256SUMS) || die "shasum -a 256 -c SHA256SUMS failed"

    step "Publish"
    local args=(release upload "$tag" "$dir/SHA256SUMS")
    [ "$replace" -eq 1 ] && args+=(--clobber)
    gh_run "${args[@]}"
    args=(release edit "$tag" --draft=false)
    [ "$prerelease" -eq 1 ] && args+=(--prerelease)
    gh_run "${args[@]}"
    [ "$dry_run" -eq 1 ] || gh release view "$tag" --json url,isDraft,isPrerelease
}

"cmd_$cmd"
