#!/usr/bin/env bash

# Versioned snapshots of Panama's durable settings stores.
#
# New snapshots are a small envelope which records both data and absence for
# the schema store and Home favourites. Settings-only snapshots written by an
# older Panama remain restorable; because they carry no Home metadata, they
# deliberately leave current Home state alone.

set -euo pipefail

config_root="${XDG_CONFIG_HOME:-$HOME/.config}"
state_root="${XDG_STATE_HOME:-$HOME/.local/state}"
settings="$config_root/panama/settings.json"
home="$state_root/panama/panama-home.json"
backup_dir="$state_root/panama/backups"
keep=15
home_json_filter='type == "object"
    and (.initialized | type == "boolean")
    and (.favorites | type == "array")
    and all(.favorites[];
        type == "object"
        and (.id | type == "string" and test("^light\\.[a-z0-9_]+$"))
        and (.alias | type == "string"))
    and ((.initialized == true) or (.favorites | length == 0))
    and ((.favorites | map(.id) | unique | length) == (.favorites | length))'

fail() {
    printf '%s\n' "$1" >&2
    exit 1
}

store_present() {
    [[ -e "$1" || -L "$1" ]]
}

store_valid() {
    local path="$1"
    [[ -f "$path" && ! -L "$path" && -r "$path" ]] \
        && jq -e 'type == "object"' "$path" >/dev/null 2>&1
}

home_store_valid() {
    local path="$1"
    store_valid "$path" && jq -e "$home_json_filter" "$path" >/dev/null 2>&1
}

validate_store() {
    local path="$1"
    local label="$2"
    store_present "$path" || return 1
    [[ ! -L "$path" ]] || fail "$label is a symbolic link and cannot be backed up safely."
    [[ -f "$path" && -r "$path" ]] || fail "$label is not a readable file."
    jq -e 'type == "object"' "$path" >/dev/null 2>&1 \
        || fail "$label is not valid settings JSON."
}

validate_home_store() {
    validate_store "$1" "$2"
    jq -e "$home_json_filter" "$1" >/dev/null 2>&1 \
        || fail "$2 does not contain valid Home favourites."
}

# The live HomePreferences store still lives in Quickshell's private state
# directory. SettingsBackup passes its public values as one argv element so the
# canonical Panama state file is current before save. No value is evaluated or
# interpolated into a command.
write_live_home() {
    local json="$1"
    local directory
    local temp

    jq -e "$home_json_filter" <<<"$json" >/dev/null 2>&1 \
        || fail "The live Home state is not valid."
    [[ ! -L "$home" ]] \
        || fail "The current Home state file is a symbolic link and cannot be replaced safely."
    directory="$(dirname "$home")"
    mkdir -p "$directory"
    temp="$(mktemp "$directory/.home-save.XXXXXX")"
    chmod 600 "$temp"
    jq '.' <<<"$json" >"$temp"
    mv -- "$temp" "$home"
}

next_snapshot_path() {
    local stamp
    local candidate
    while true; do
        stamp="$(date +%Y%m%d-%H%M%S%3N)"
        candidate="$backup_dir/settings-$stamp.json"
        if [[ ! -e "$candidate" && ! -L "$candidate" ]]; then
            snapshot_stamp="$stamp"
            snapshot_path="$candidate"
            return
        fi
        sleep 0.002
    done
}

prune_snapshots() {
    local -a entries=()
    local index
    local path
    mapfile -d '' entries < <(
        find "$backup_dir" -maxdepth 1 -type f \
            -name 'settings-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].json' \
            -printf '%T@ %p\0' | sort -zrn
    )
    for ((index = keep; index < ${#entries[@]}; index++)); do
        path="${entries[$index]#* }"
        rm -f -- "$path"
    done
}

# Writes the current stores after callers have decided whether invalid or
# absent state should be fatal. The destination is created in the backup
# directory and renamed into place, so list/restore never observe half a JSON
# document.
write_snapshot() {
    local desktop_present=false
    local home_present=false
    local temp

    store_present "$settings" && desktop_present=true
    store_present "$home" && home_present=true
    [[ "$desktop_present" == true || "$home_present" == true ]] \
        || return 1

    mkdir -p "$backup_dir"
    next_snapshot_path
    temp="$(mktemp "$backup_dir/.settings-snapshot.XXXXXX")"
    chmod 600 "$temp"

    if [[ "$desktop_present" == true && "$home_present" == true ]]; then
        jq -n --slurpfile desktop "$settings" --slurpfile home "$home" '{
            version: 2,
            desktop: { present: true, data: $desktop[0] },
            home: { present: true, data: $home[0] }
        }' >"$temp"
    elif [[ "$desktop_present" == true ]]; then
        jq -n --slurpfile desktop "$settings" '{
            version: 2,
            desktop: { present: true, data: $desktop[0] },
            home: { present: false }
        }' >"$temp"
    else
        jq -n --slurpfile home "$home" '{
            version: 2,
            desktop: { present: false },
            home: { present: true, data: $home[0] }
        }' >"$temp"
    fi

    mv -- "$temp" "$snapshot_path"
    prune_snapshots
}

snapshot_source() {
    local name="$1"
    local candidate="$backup_dir/$name"
    local canonical_dir
    local canonical_file

    [[ "$name" =~ ^settings-[0-9]{8}-[0-9]{9}\.json$ ]] \
        || fail "Not a snapshot name."
    [[ -f "$candidate" && ! -L "$candidate" && -r "$candidate" ]] \
        || fail "That snapshot is missing."

    canonical_dir="$(realpath -e -- "$backup_dir")"
    canonical_file="$(realpath -e -- "$candidate")"
    [[ "$canonical_file" == "$canonical_dir/"* ]] \
        || fail "That snapshot is outside the backup directory."

    printf '%s\n' "$candidate"
}

validate_snapshot() {
    local source_file="$1"

    jq -e 'type == "object"' "$source_file" >/dev/null 2>&1 \
        || fail "That snapshot is not valid JSON."

    if jq -e 'has("version")' "$source_file" >/dev/null 2>&1; then
        jq -e '
            .version == 2
            and (.desktop | type == "object")
            and (.desktop.present | type == "boolean")
            and ((.desktop.present == false) or (.desktop.data | type == "object"))
            and (.home | type == "object")
            and (.home.present | type == "boolean")
            and ((.home.present == false) or (.home.data | type == "object"))
        ' "$source_file" >/dev/null 2>&1 \
            || fail "That snapshot uses an unsupported format."
        if jq -e '.home.present' "$source_file" >/dev/null 2>&1; then
            jq -e ".home.data | $home_json_filter" "$source_file" >/dev/null 2>&1 \
                || fail "That snapshot contains invalid Home favourites."
        fi
        snapshot_format="versioned"
    else
        snapshot_format="legacy"
    fi
}

stage_json() {
    local source_file="$1"
    local filter="$2"
    local directory="$3"
    local template="$4"
    local staged

    mkdir -p "$directory"
    staged="$(mktemp "$directory/$template.XXXXXX")"
    chmod 600 "$staged"
    jq -e "$filter" "$source_file" >"$staged"
    printf '%s\n' "$staged"
}

backup_current_file() {
    local target="$1"
    local directory="$2"
    local template="$3"

    if ! store_present "$target"; then
        printf '\n'
        return
    fi
    [[ -f "$target" && ! -L "$target" ]] \
        || fail "A settings target is not a regular file."
    local rollback
    rollback="$(mktemp "$directory/$template.XXXXXX")"
    chmod 600 "$rollback"
    cp -- "$target" "$rollback"
    printf '%s\n' "$rollback"
}

restore_target() {
    local target="$1"
    local present="$2"
    local staged="$3"

    if [[ "$present" == true ]]; then
        mv -- "$staged" "$target"
    else
        rm -f -- "$target"
    fi
}

case "${1:-list}" in
    save)
        desktop_present=false
        home_present=false
        if store_present "$settings"; then
            validate_store "$settings" "The current settings file"
            desktop_present=true
        fi
        if [[ $# -ge 2 ]]; then
            write_live_home "$2"
        fi
        if store_present "$home"; then
            validate_home_store "$home" "The current Home state file"
            home_present=true
        fi
        [[ "$desktop_present" == true || "$home_present" == true ]] \
            || fail "No Panama settings exist to back up."

        write_snapshot
        printf '{"saved":"settings-%s.json"}\n' "$snapshot_stamp"
        ;;

    list)
        mkdir -p "$backup_dir"
        first=true
        printf '['
        while IFS= read -r -d '' entry; do
            file="${entry#* }"
            name="$(basename -- "$file")"
            raw="${name#settings-}"
            raw="${raw%.json}"
            pretty="${raw:0:4}-${raw:4:2}-${raw:6:2} ${raw:9:2}:${raw:11:2}:${raw:13:2}"
            if jq -e '.version == 2' "$file" >/dev/null 2>&1; then
                keys="$(jq -r 'if .desktop.present then (.desktop.data | keys | length) else 0 end' "$file" 2>/dev/null || printf 0)"
            else
                keys="$(jq -r 'keys | length' "$file" 2>/dev/null || printf 0)"
            fi
            [[ "$first" == true ]] || printf ','
            first=false
            printf '{"name":"%s","when":"%s","keys":%s}' "$name" "$pretty" "$keys"
        done < <(
            find "$backup_dir" -maxdepth 1 -type f \
                -name 'settings-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].json' \
                -printf '%T@ %p\0' | sort -zrn
        )
        printf ']\n'
        ;;

    restore)
        name="${2:-}"
        [[ -n "$name" ]] || fail "Which snapshot?"
        source_file="$(snapshot_source "$name")"
        validate_snapshot "$source_file"

        desktop_present=true
        home_action="preserve"
        if [[ "$snapshot_format" == "versioned" ]]; then
            desktop_present="$(jq -r '.desktop.present' "$source_file")"
            home_action="$(jq -r 'if .home.present then "present" else "absent" end' "$source_file")"
            if [[ "$desktop_present" == true ]]; then
                desktop_stage="$(stage_json "$source_file" '.desktop.data' "$(dirname "$settings")" '.settings-restore')"
            else
                mkdir -p "$(dirname "$settings")"
                desktop_stage=""
            fi
            if [[ "$home_action" == "present" ]]; then
                home_stage="$(stage_json "$source_file" '.home.data' "$(dirname "$home")" '.home-restore')"
            else
                mkdir -p "$(dirname "$home")"
                home_stage=""
            fi
        else
            desktop_stage="$(stage_json "$source_file" '.' "$(dirname "$settings")" '.settings-restore')"
            mkdir -p "$(dirname "$home")"
            home_stage=""
        fi

        # Preserve the replaced state as an undo snapshot only when every
        # existing store is valid. A corrupt store must not prevent recovery,
        # but it is not useful as a future restore point either.
        if { ! store_present "$settings" || store_valid "$settings"; } \
            && { ! store_present "$home" || home_store_valid "$home"; }; then
            write_snapshot >/dev/null 2>&1 || true
        fi

        settings_dir="$(dirname "$settings")"
        home_dir="$(dirname "$home")"
        settings_rollback="$(backup_current_file "$settings" "$settings_dir" '.settings-rollback')"
        home_rollback="$(backup_current_file "$home" "$home_dir" '.home-rollback')"
        rollback_needed=true

        rollback() {
            if [[ "$settings_rollback" != "" ]]; then
                mv -f -- "$settings_rollback" "$settings"
            else
                rm -f -- "$settings"
            fi
            if [[ "$home_action" != "preserve" ]]; then
                if [[ "$home_rollback" != "" ]]; then
                    mv -f -- "$home_rollback" "$home"
                else
                    rm -f -- "$home"
                fi
            fi
        }

        cleanup_restore() {
            local status=$?
            if [[ "$rollback_needed" == true ]]; then
                rollback || true
            fi
            [[ "${desktop_stage:-}" == "" ]] || rm -f -- "$desktop_stage"
            [[ "${home_stage:-}" == "" ]] || rm -f -- "$home_stage"
            [[ "$settings_rollback" == "" ]] || rm -f -- "$settings_rollback"
            [[ "$home_rollback" == "" ]] || rm -f -- "$home_rollback"
            exit "$status"
        }
        trap cleanup_restore EXIT

        restore_target "$settings" "$desktop_present" "$desktop_stage"
        if [[ "$home_action" != "preserve" ]]; then
            restore_target "$home" "$([[ "$home_action" == "present" ]] && printf true || printf false)" "$home_stage"
        fi

        rollback_needed=false
        trap - EXIT
        [[ "$settings_rollback" == "" ]] || rm -f -- "$settings_rollback"
        [[ "$home_rollback" == "" ]] || rm -f -- "$home_rollback"
        if [[ "$home_action" == "present" ]]; then
            jq -cn --arg restored "$name" --slurpfile home "$home" \
                '{restored: $restored, home: {present: true, data: $home[0]}}'
        elif [[ "$home_action" == "absent" ]]; then
            jq -cn --arg restored "$name" \
                '{restored: $restored, home: {present: false}}'
        else
            jq -cn --arg restored "$name" \
                '{restored: $restored, home: {preserve: true}}'
        fi
        ;;

    *)
        fail "usage: panama-settings-backup [save|list|restore <name>]"
        ;;
esac
