#!/usr/bin/env bash

# Bringing an already-installed machine up to what this checkout expects.
#
# `./install` is additive: it copies files over `/`, links dotfiles, and
# installs packages. It has no way to say "remove that file", "disable that
# unit", "that symlink now points at the wrong place". So a machine installed
# in March keeps March's mistakes forever, and the only thing that ever fixes
# them is the person who happens to read a commit message.
#
# A migration is one shell script that performs one such repair, exactly once,
# on machines that need it.
#
#   migrations/<unix-timestamp>.sh
#
# The name is the commit timestamp of HEAD when it was authored, so glob order
# over fixed-width epoch seconds IS chronological order -- no sequence numbers
# to collide on across branches. `panama-dev-migration` stamps them.
#
# State is one empty marker file per migration under
# $XDG_STATE_HOME/panama/migrations. Present means applied. There is no
# database and no version integer, because the failure mode of a version
# integer is that one bad migration strands every later one behind it.
#
# The rules a migration must follow are in the template that
# `panama-dev-migration` writes, and they are worth repeating here because
# this runner cannot enforce them:
#
#   * Safe to run twice. The marker only records that it succeeded once.
#   * Tolerant of the repair already being correct -- a user may have fixed it
#     by hand, or a later `./install` may have overwritten it back.
#   * Root work goes through `panama-sudo --reason "..."`, never bare sudo,
#     so the prompt names the repair. See bin/panama-sudo.
#
# The marker is written ONLY on success, so a migration that fails stays
# pending and is retried at the next login. That is deliberate: a repair that
# could not complete has not happened, and recording it as done would hide it
# forever.
#
# This mirrors config/dot/quickshell/config/Migrations.qml, which does the same
# job for the settings JSON and documents the same reasoning. That one handles
# renamed preference keys; this one handles everything else.

set -uo pipefail

PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
MIGRATIONS_DIR="$PANAMA_PATH/migrations"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama/migrations"

export PANAMA_PATH

info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
ok()   { printf '\033[1;32m✓\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
err()  { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; }

# Every migration this checkout ships, oldest first. Empty is a valid state.
all_migrations() {
    [[ -d "$MIGRATIONS_DIR" ]] || return 0
    local file
    for file in "$MIGRATIONS_DIR"/*.sh; do
        [[ -e "$file" ]] || continue
        basename "$file"
    done | sort
}

pending_migrations() {
    local name
    while read -r name; do
        [[ -n "$name" ]] || continue
        [[ -e "$STATE_DIR/$name" ]] || printf '%s\n' "$name"
    done < <(all_migrations)
}

run_one() {
    local name="$1" file="$MIGRATIONS_DIR/$1"
    info "$name"
    # A subshell with its own strictness: a migration that forgets `set -e` is
    # still stopped by its first failing command, and one that sets shell
    # options cannot leak them into the next migration.
    if bash -euo pipefail "$file"; then
        mkdir -p "$STATE_DIR"
        : >"$STATE_DIR/$name"
        ok "$name applied"
        return 0
    fi
    err "$name failed and will be retried at the next login"
    return 1
}

cmd_run() {
    local pending
    pending="$(pending_migrations)"
    if [[ -z "$pending" ]]; then
        ok "Nothing to migrate; this machine matches the checkout."
        return 0
    fi

    local count failed=0 name
    count="$(grep -c . <<<"$pending")"
    info "$count migration(s) to apply"
    while read -r name; do
        [[ -n "$name" ]] || continue
        # Stop at the first failure rather than continuing. Migrations are
        # ordered, and a later one may assume an earlier one landed; running
        # it anyway turns one stuck repair into an unpredictable machine.
        if ! run_one "$name"; then
            failed=1
            break
        fi
    done <<<"$pending"

    if (( failed )); then
        warn "Re-running 'panama migrate' is safe and will retry from the failure."
        return 1
    fi
    ok "This machine now matches the checkout."
}

# The check the login notifier runs. Exit 0 means work is waiting, so it reads
# as `if panama-migrate --pending; then notify; fi`.
cmd_pending() {
    local pending
    pending="$(pending_migrations)"
    [[ -n "$pending" ]] || return 1
    grep -c . <<<"$pending"
}

cmd_list() {
    local name
    while read -r name; do
        [[ -n "$name" ]] || continue
        if [[ -e "$STATE_DIR/$name" ]]; then
            printf 'applied  %s\n' "$name"
        else
            printf 'pending  %s\n' "$name"
        fi
    done < <(all_migrations)
}

# Re-run one that already succeeded. For developing a migration, and for the
# rare case where a repair was undone by something else.
cmd_force() {
    local name="${1:-}"
    [[ -n "$name" ]] || { err "force needs a migration name"; return 2; }
    [[ -e "$MIGRATIONS_DIR/$name" ]] || { err "no such migration: $name"; return 2; }
    rm -f "$STATE_DIR/$name"
    run_one "$name"
}

# Mark everything applied without running it. This is what a fresh install
# does: the machine was just built from this checkout, so every repair those
# migrations describe is already true of it, and running them would apply
# fixes for versions it never had.
cmd_baseline() {
    mkdir -p "$STATE_DIR"
    local name count=0
    while read -r name; do
        [[ -n "$name" ]] || continue
        [[ -e "$STATE_DIR/$name" ]] && continue
        : >"$STATE_DIR/$name"
        count=$(( count + 1 ))
    done < <(all_migrations)
    ok "Marked $count migration(s) as already applied."
}

case "${1:-run}" in
    run)        shift || true; cmd_run "$@" ;;
    --pending)  shift || true; cmd_pending "$@" ;;
    --list|list) shift || true; cmd_list "$@" ;;
    --force)    shift || true; cmd_force "$@" ;;
    --baseline) shift || true; cmd_baseline "$@" ;;
    -h|--help)
        cat <<'USAGE'
usage: panama-migrate [run|--pending|--list|--force <name>|--baseline]

  run         Apply every pending migration, oldest first (default)
  --pending   Exit 0 and print the count when work is waiting, else exit 1
  --list      Show every migration and whether it has been applied
  --force     Re-run one migration that already succeeded
  --baseline  Mark everything applied without running it (fresh installs)
USAGE
        ;;
    *) err "unknown argument: $1"; exit 2 ;;
esac
