#!/usr/bin/env bash
# Restore ~/.claude/settings.json when a rogue writer guts it, and keep the
# restore source fresh. Driven by claude-settings-heal.path (systemd --user),
# deliberately independent of settings.json: its own hooks are gone exactly
# when healing is needed.
set -euo pipefail

SETTINGS="${CLAUDE_SETTINGS:-$HOME/.claude/settings.json}"
KNOWN_GOOD="${CLAUDE_SETTINGS_KNOWN_GOOD:-$SETTINGS.known-good}"
LOG="${CLAUDE_SETTINGS_HEAL_LOG:-$HOME/.claude/settings-heal.log}"

log() { printf '%s %s\n' "$(date -Is)" "$*" >>"$LOG"; }

# healthy <candidate> [reference]
#   0  structurally sound, and carries every hook command the reference has
#   2  structurally sound but missing hook commands the reference has
#   1  unusable: unparseable, not an object, or no hooks at all
healthy() {
    python3 - "$1" "${2-}" <<'PY'
import json
import os
import re
import sys

SCRIPT = re.compile(r"(/[^\s'\"]+\.(?:sh|mjs|js|py))")


def load(path):
    with open(path, encoding="utf-8") as handle:
        return json.load(handle)


def hook_commands(payload):
    """Every command string, or None when the hooks block is not a well-formed one."""
    found = set()
    hooks = payload.get("hooks")
    if not isinstance(hooks, dict) or not hooks:
        return None
    for groups in hooks.values():
        if not isinstance(groups, list) or not groups:
            return None
        for group in groups:
            if not isinstance(group, dict):
                return None
            entries = group.get("hooks")
            if not isinstance(entries, list) or not entries:
                return None
            for hook in entries:
                if not isinstance(hook, dict):
                    return None
                command = hook.get("command")
                if not isinstance(command, str) or not command.strip():
                    return None
                # A hook whose script is gone runs nothing; the file would still parse.
                if any(not os.path.exists(path) for path in SCRIPT.findall(command)):
                    return None
                found.add(command)
    return found


try:
    payload = load(sys.argv[1])
except (OSError, json.JSONDecodeError):
    sys.exit(1)
if not isinstance(payload, dict):
    sys.exit(1)
present = hook_commands(payload)
if present is None:
    sys.exit(1)

reference = sys.argv[2] if len(sys.argv) > 2 else ""
if not reference:
    sys.exit(0)
try:
    expected = hook_commands(load(reference))
except (OSError, json.JSONDecodeError):
    sys.exit(0)
if expected is None:
    sys.exit(0)
missing = expected - present
if missing:
    sys.stdout.write(", ".join(sorted(missing)))
    sys.exit(2)
sys.exit(0)
PY
}

replace_atomically() {
    local source="$1" target="$2" temp
    temp="$(mktemp "$target.XXXXXX")"
    cp "$source" "$temp"
    mv -f "$temp" "$target"
}

missing="$(healthy "$SETTINGS" "$KNOWN_GOOD")" && status=0 || status=$?
case "$status" in
0)
    if ! cmp -s "$SETTINGS" "$KNOWN_GOOD"; then
        replace_atomically "$SETTINGS" "$KNOWN_GOOD"
        log "refreshed known-good from healthy settings.json"
    fi
    exit 0
    ;;
2)
    # Promoting this would make the loss canonical; restoring it would revert a deliberate
    # removal. Neither file is touched -- known-good stays the older, complete restore point.
    log "REFUSED to promote settings.json: known-good has hooks it dropped ($missing);" \
        "confirm a deliberate removal with: cp $SETTINGS $KNOWN_GOOD"
    exit 0
    ;;
esac

if [[ ! -s "$KNOWN_GOOD" ]]; then
    log "FATAL settings.json unhealthy but no known-good at $KNOWN_GOOD; left as-is"
    exit 1
fi
if ! healthy "$KNOWN_GOOD"; then
    log "FATAL known-good at $KNOWN_GOOD is itself unhealthy; settings.json left as-is"
    exit 1
fi

replace_atomically "$KNOWN_GOOD" "$SETTINGS"
log "RESTORED settings.json from known-good (was gutted)"
