#!/bin/bash
# Compatible with the Bash 3.2 supplied by macOS. No Homebrew/Python required.
set -euo pipefail
umask 077

usage() {
    cat <<'EOF'
Usage: /bin/bash restore-migration.sh PACKAGE [--plan|--apply]

PACKAGE contains filesystem/<original absolute path without leading slash>.
The sibling restore-migration-paths.txt lists the exact destinations.
Default: --plan (read-only preview). --apply prompts before each copy.
Run as your normal macOS user, not sudo. Protected copies request sudo.
EOF
}
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
ask() {
    local answer
    printf '%s [y/N] ' "$*" >/dev/tty
    IFS= read -r answer </dev/tty || return 1
    case "$answer" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac
}
safe_components() {
    # Do not follow an existing symlink in a destination or source ancestry.
    local path="$1"
    while [ "$path" != / ]; do
        [ ! -L "$path" ] || fail "Symlink in copy path: $path"
        path=$(/usr/bin/dirname "$path")
    done
}
check_services() {
    local label
    for label in homebrew.mxcl.httpd homebrew.mxcl.php@8.3 homebrew.mxcl.postgresql@14 \
        com.kinsley.megatron-follow-up-email com.kinsley.duckdns com.kinsley.acme-renew; do
        if /bin/launchctl print "gui/$UID/$label" >/dev/null 2>&1; then
            fail "Unload $label before restoring. Its configuration/data must not be changed while loaded."
        fi
    done
    # Also catch independently started server processes, including system services.
    if /usr/bin/pgrep -x 'httpd|php-fpm|postgres' >/dev/null 2>&1; then
        fail 'Apache, PHP-FPM or PostgreSQL is running. Stop it before restoring.'
    fi
}
run_copy_command() {
    if [ "$protected" = yes ]; then /usr/bin/sudo "$@"; else "$@"; fi
}

if [ "${1:-}" = --help ] || [ "${1:-}" = -h ]; then usage; exit 0; fi
[ "$#" -ge 1 ] && [ "$#" -le 2 ] || { usage; exit 1; }
mode="${2:---plan}"
case "$mode" in --plan|--apply) ;; *) fail "Unknown mode: $mode" ;; esac
[ "$(/usr/bin/uname -s)" = Darwin ] || fail 'This script requires macOS.'
[ "$(/usr/bin/uname -m)" = x86_64 ] || fail 'This package targets an Intel Mac.'
[ "$EUID" -ne 0 ] || fail 'Run as your normal account; do not launch this script with sudo.'
expected_user=kinsleypetit-homme
[ "$(/usr/bin/id -un)" = "$expected_user" ] || fail "Create/log into the macOS short-name account $expected_user. The project has hardcoded home paths."
[ "$HOME" = "/Users/$expected_user" ] || fail "Unexpected home directory: $HOME"
script_dir=$(cd "$(/usr/bin/dirname "$0")" && pwd -P)
paths_file="$script_dir/restore-migration-paths.txt"
[ -f "$paths_file" ] || fail "Missing destination list: $paths_file"
[ -d "$1" ] || fail "Package directory does not exist: $1"
package=$(cd "$1" && pwd -P)
[ -d "$package/filesystem" ] || fail "Missing $package/filesystem; this is not a migration package."
safe_components "$package/filesystem"

destinations=()
missing=0
printf 'Package: %s\nMode: %s\n\n' "$package" "$mode"
if [ ! -s "$package/FILES_COMPLETE" ] || [ ! -f "$package/CAPTURED-PATHS.txt" ] || [ ! -s "$package/FILES.sha256" ]; then
    printf 'MISSING: completed full-file capture marker/path inventory. This may be a database-only package.\n'
    missing=$((missing + 1))
elif ! /usr/bin/cmp -s "$paths_file" "$package/CAPTURED-PATHS.txt"; then
    fail 'Captured file destinations differ from this toolkit. Use the matching toolkit or recapture.'
fi
while IFS= read -r dest || [ -n "$dest" ]; do
    case "$dest" in ''|'#'*) continue ;; esac
    case "$dest" in *'/../'*|*'/./'*|*'//'*) fail "Invalid destination: $dest" ;; esac
    case "$dest" in
        /usr/local/*|/Library/Fonts|'/Library/Application Support/'*|/Users/kinsleypetit-homme/*) ;;
        *) fail "Destination outside the migration scope: $dest" ;;
    esac
    case "$package/" in "$dest/"*) fail "Backup is inside restore destination: $dest" ;; esac
    source="$package/filesystem$dest"
    safe_components "$source"
    safe_components "$dest"
    destinations+=("$dest")
    if [ ! -e "$source" ]; then
        printf 'MISSING BACKUP: %s\n' "$source"
        missing=$((missing + 1))
    elif [ -e "$dest" ]; then
        printf 'MERGE/REPLACE (will ask): %s\n' "$dest"
        if { [ -d "$source" ] && [ ! -d "$dest" ]; } || { [ ! -d "$source" ] && [ -d "$dest" ]; }; then
            fail "Source/destination types differ: $dest"
        fi
    else
        printf 'CREATE (will ask): %s\n' "$dest"
    fi
done < "$paths_file"
[ "${#destinations[@]}" -gt 0 ] || fail 'Empty destination list.'

printf '\nDatabase archives (import is a separate step):\n'
for name in appsmith.dump misper.dump postgres-globals.sql; do
    if [ -s "$package/databases/$name" ]; then
        printf '  PRESENT: %s\n' "$name"
    else
        printf '  MISSING: %s\n' "$name"
        missing=$((missing + 1))
    fi
done
printf '\nRequired installations (presence only; not an activation/version test):\n'
for executable in /usr/local/opt/php@8.3/bin/php /usr/local/opt/postgresql@14/bin/pg_restore \
    /usr/local/opt/httpd/bin/httpd /usr/local/bin/exiftool /usr/local/bin/gs \
    /usr/local/opt/python@3.11/bin/python3.11 /usr/local/opt/python@3.14/bin/python3.14 \
    /usr/local/bin/acme.sh /usr/local/bin/bash /Applications/BCStudio.app/Contents/MacOS/BCStudio_Console; do
    if [ -x "$executable" ]; then printf '  FOUND: %s\n' "$executable";
    else printf '  INSTALL: %s\n' "$executable"; fi
done
for app in '/Applications/Adobe Photoshop 2026/Adobe Photoshop 2026.app' \
    '/Applications/Adobe Illustrator 2026/Adobe Illustrator.app'; do
    if [ -d "$app" ]; then printf '  FOUND: %s\n' "$app";
    else printf '  INSTALL: %s\n' "$app"; fi
done
printf '\nMissing required package items: %s\n' "$missing"
if [ "$mode" = --plan ]; then
    printf 'Preview only. No files copied, directories created, or services changed.\n'
    [ "$missing" -eq 0 ] || exit 2
    exit 0
fi
[ "$missing" -eq 0 ] || fail 'Incomplete package; repair the backup before applying.'
[ -t 0 ] || fail '--apply requires an interactive Terminal.'
check_services
# Copying server config into an uninitialized cluster would obstruct initdb.
[ -f /usr/local/var/postgresql@14/PG_VERSION ] || fail 'Install and initialize PostgreSQL 14 first, then stop it. Do not create its data directory by copying configuration alone.'
[ "$(cat /usr/local/var/postgresql@14/PG_VERSION)" = 14 ] || fail 'Target PostgreSQL cluster is not version 14.'

printf '\nCopies merge directories and replace matching files. Destination-only files remain.\n'
printf 'Install application/runtime dependencies first. Database dumps are not imported by this script.\n'
printf 'LaunchAgents will be disabled in launchd before copying so they stay off across logins.\n'
printf 'Copied Python environments still require matching base interpreters and validation.\n'
ask 'Continue with interactive file restoration?' || exit 1
log=$(/usr/bin/mktemp "$HOME/megatron-restore-$(/bin/date +%Y%m%d-%H%M%S).XXXXXX")
printf 'Restore log: %s\n' "$log"
exec > >(/usr/bin/tee -a "$log") 2>&1
trap 'printf "Restore failed at line %s. Review the log; already copied files remain.\n" "$LINENO" >&2' ERR
printf 'Started: %s\nPackage: %s\n' "$(/bin/date -u)" "$package"
printf 'Checking saved full-file backup checksums (this can take several minutes)...\n'
(cd "$package" && /usr/bin/shasum -a 256 -c FILES.sha256 > "$log.backup-checksums" 2>&1) || fail "Backup integrity check failed; inspect $log.backup-checksums"
copied=0
skipped=0
for dest in "${destinations[@]}"; do
    source="$package/filesystem$dest"
    safe_components "$dest"
    protected=no
    case "$dest" in /Users/kinsleypetit-homme/*) ;; *) protected=yes ;; esac
    if [ -e "$dest" ]; then
        if { [ -d "$source" ] && [ ! -d "$dest" ]; } || { [ ! -d "$source" ] && [ -d "$dest" ]; }; then
            fail "Source/destination types differ: $dest"
        fi
        if ! ask "Merge/replace existing $dest?"; then
            printf 'SKIPPED: %s\n' "$dest"; skipped=$((skipped + 1)); continue
        fi
    else
        if ! ask "Restore $dest?"; then
            printf 'SKIPPED: %s\n' "$dest"; skipped=$((skipped + 1)); continue
        fi
    fi
    if [ -d "$source" ]; then folder="$dest"; else folder=$(/usr/bin/dirname "$dest"); fi
    ancestor="$folder"
    while [ ! -d "$ancestor" ]; do ancestor=$(/usr/bin/dirname "$ancestor"); done
    required_kb=$(/usr/bin/du -sk "$source" | /usr/bin/awk '{print $1}')
    available_kb=$(/bin/df -Pk "$ancestor" | /usr/bin/awk 'END {print $4}')
    # Conservative: reserve the full item even when some destination files exist.
    [ "$available_kb" -ge "$required_kb" ] || fail "Insufficient free space for $dest (need up to ${required_kb} KiB; available ${available_kb} KiB)."
    if [ ! -d "$folder" ]; then
        printf 'Missing destination folder (including any missing parents): %s\n' "$folder"
        if ! ask "May I create $folder and its missing parents?"; then
            printf 'SKIPPED: %s\n' "$dest"; skipped=$((skipped + 1)); continue
        fi
        if [ "$protected" = yes ]; then
            # Protected parent directories must remain traversable by the service user.
            /usr/bin/sudo /bin/sh -c 'umask 022; /bin/mkdir -p "$1"' sh "$folder"
        else
            /bin/mkdir -p "$folder"
        fi
    fi
    check_services
    case "$dest" in
        /Users/kinsleypetit-homme/Library/LaunchAgents/*.plist)
            label=$(/usr/libexec/PlistBuddy -c 'Print :Label' "$source")
            case "$label" in
                homebrew.mxcl.httpd|homebrew.mxcl.php@8.3|homebrew.mxcl.postgresql@14|com.kinsley.duckdns|com.kinsley.acme-renew|com.kinsley.megatron-follow-up-email) ;;
                *) fail "Unexpected LaunchAgent label: $label" ;;
            esac
            ask "Keep $label disabled until setup is complete? Required to copy this job." || {
                printf 'SKIPPED: %s\n' "$dest"; skipped=$((skipped + 1)); continue
            }
            /bin/launchctl disable "gui/$UID/$label"
            printf 'DISABLED: %s (re-enable deliberately after validation)\n' "$label"
            ;;
    esac
    printf 'COPY: %s\n' "$dest"
    run_copy_command /usr/bin/ditto --rsrc --extattr --acl "$source" "$dest"
    # These locations must be writable by the logged-in service user, not root.
    case "$dest" in
        /usr/local/var/www/megatron|/usr/local/etc/php/8.3|/usr/local/etc/httpd)
            /usr/bin/sudo /usr/sbin/chown -R "$UID:$(/usr/bin/id -g)" "$dest" ;;
        /usr/local/var/postgresql@14/*|/usr/local/etc/pdflib/licensekeys.txt|/usr/local/lib/php/pecl/20230831/php_pdflib.so)
            /usr/bin/sudo /usr/sbin/chown "$UID:$(/usr/bin/id -g)" "$dest" ;;
    esac
    printf 'VERIFY file contents and symlink targets: %s\n' "$dest"
    if [ -d "$source" ]; then
        differences=$(run_copy_command /usr/bin/rsync -rlcni "$source/" "$dest/")
    else
        differences=$(run_copy_command /usr/bin/rsync -rlcni "$source" "$(/usr/bin/dirname "$dest")/")
    fi
    [ -z "$differences" ] || fail "Copy verification differs at $dest: $differences"
    printf 'VERIFIED: %s\n' "$dest"
    copied=$((copied + 1))
done
printf '\nCopied and content-verified: %s; skipped: %s. Log: %s\n' "$copied" "$skipped" "$log"
printf 'Metadata was copied with ditto; content verification does not certify ACLs/resource forks.\n'
printf 'Next: restore database roles/data; validate PHP/PDFlib, Python, licenses, fonts and Adobe automation.\n'
printf 'Restored LaunchAgents remain disabled. Enable/load them deliberately after setup.\n'
printf 'Enable email scheduling only after acceptance tests.\n'
[ "$skipped" -eq 0 ] || exit 2
