#!/bin/sh
# sync-memory.sh -- MIRROR the REPO memory store into the machine-local
# HARNESS PROFILE memory store. Repo is the source of truth.
#
# WHY: There are TWO memory stores on every machine:
#   REPO store    : <root>/.claude/memory/                 (git-tracked, synced via Gitea -- SOURCE OF TRUTH)
#   PROFILE store : $HOME/.claude/projects/<slug>/memory/  (machine-local, NOT in git;
#                                                            the harness auto-injects THIS into the prompt)
# They drift. This script keeps the auto-injected PROFILE store in lock step
# with the synced REPO store. Memories must be authored into the REPO store
# (`.claude/memory/`) to persist; PROFILE-only files are treated as stale and
# removed.
#
# MIRROR MODE -- REPO IS AUTHORITATIVE:
#   * file in REPO but not in PROFILE   -> copy REPO -> PROFILE
#   * file in PROFILE but not in REPO   -> DELETE from PROFILE (repo absence = intentional removal)
#   * file in BOTH, identical           -> nothing
#   * file in BOTH, content differs     -> overwrite PROFILE with REPO (repo wins)
#   * never modifies the REPO store.    MEMORY.md is excluded from sync.
#
# SAFETY: if the REPO store contains fewer than 5 *.md files (excluding
# MEMORY.md), the script ABORTS before deleting anything from PROFILE. This
# guards against a corrupted or empty repo wiping the profile store.
#
# Idempotent and safe to run repeatedly on every machine (Windows Git Bash,
# macOS, Linux). ASCII output only. No hardcoded drive paths.
#
# Flags:
#   --dry-run   show what WOULD be copied/deleted/overwritten; change nothing.
#
# Exit: 0 normally. Non-zero only on a hard setup error (no repo, no memory
# dir, or the safety threshold trips).

set -u

DRY_RUN=0
for arg in "$@"; do
    case "$arg" in
        --dry-run) DRY_RUN=1 ;;
        -h|--help)
            echo "Usage: sync-memory.sh [--dry-run]"
            echo "  Mirror the repo memory store into the harness-profile memory store."
            echo "  Repo is the source of truth; profile-only files are removed and"
            echo "  divergent files are overwritten with the repo version."
            echo "  --dry-run   print planned ops; change nothing."
            exit 0
            ;;
        *)
            echo "[ERROR] unknown argument: $arg" >&2
            echo "Usage: sync-memory.sh [--dry-run]" >&2
            exit 2
            ;;
    esac
done

# --- Resolve CLAUDETOOLS_ROOT (env -> identity.json -> git toplevel -> script dir) ---

# Absolute dir of this script, POSIX-portable (no readlink -f on macOS).
script_path="$0"
case "$script_path" in
    /*) : ;;
    *) script_path="$(pwd)/$script_path" ;;
esac
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$script_path")" && pwd)"

ROOT=""
if [ -n "${CLAUDETOOLS_ROOT:-}" ] && [ -d "${CLAUDETOOLS_ROOT}" ]; then
    ROOT="$CLAUDETOOLS_ROOT"
fi

# Walk up from the script dir to find a dir containing .claude/ (covers the
# normal layout <root>/.claude/scripts/sync-memory.sh).
if [ -z "$ROOT" ]; then
    d="$SCRIPT_DIR"
    while [ "$d" != "/" ] && [ -n "$d" ]; do
        if [ -d "$d/.claude" ]; then
            ROOT="$d"
            break
        fi
        parent="$(dirname -- "$d")"
        [ "$parent" = "$d" ] && break
        d="$parent"
    done
fi

# identity.json may override with claudetools_root (authoritative per machine).
if [ -n "$ROOT" ] && [ -f "$ROOT/.claude/identity.json" ]; then
    ID_ROOT="$(
        PYBIN=""
        for c in py python3 python; do
            if command -v "$c" >/dev/null 2>&1; then PYBIN="$c"; break; fi
        done
        if [ -n "$PYBIN" ]; then
            "$PYBIN" - "$ROOT/.claude/identity.json" <<'PYEOF' 2>/dev/null
import json, os, sys
try:
    d = json.load(open(sys.argv[1]))
    r = d.get("claudetools_root")
    if r and os.path.isdir(r):
        print(r)
except Exception:
    pass
PYEOF
        fi
    )"
    if [ -n "$ID_ROOT" ] && [ -d "$ID_ROOT" ]; then
        ROOT="$ID_ROOT"
    fi
fi

# Last resort: git toplevel.
if [ -z "$ROOT" ]; then
    ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
fi

if [ -z "$ROOT" ] || [ ! -d "$ROOT/.claude" ]; then
    echo "[ERROR] could not resolve CLAUDETOOLS_ROOT (no .claude dir found)." >&2
    exit 1
fi

REPO_MEM="$ROOT/.claude/memory"
if [ ! -d "$REPO_MEM" ]; then
    echo "[ERROR] repo memory dir not found: $REPO_MEM" >&2
    exit 1
fi

# --- Derive the harness profile memory dir ---------------------------------
# Slug = absolute project path with every run of non-alphanumeric chars -> '-'.
# Must match memory_dream.py's derivation. Prefer CLAUDE_PROJECT_DIR if set;
# also honor GROK_WORKSPACE_ROOT for Grok driver hooks (coexistence).
HOME_DIR="${HOME:-$(cd ~ 2>/dev/null && pwd)}"
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${GROK_WORKSPACE_ROOT:-$ROOT}}"

# Absolutize PROJECT_DIR.
case "$PROJECT_DIR" in
    /*) : ;;
    *) PROJECT_DIR="$(CDPATH= cd -- "$PROJECT_DIR" 2>/dev/null && pwd || echo "$PROJECT_DIR")" ;;
esac

# Compute the slug. Use Python when available (exact, matches the analyzer);
# otherwise fall back to sed/tr.
# Single-dash collapse: replace any non-alphanumeric run with a single '-'.
# Prefer Python (exact, matches the analyzer) when available, else sed.
SLUG_SINGLE=""
PYBIN=""
for c in py python3 python; do
    if command -v "$c" >/dev/null 2>&1; then PYBIN="$c"; break; fi
done
if [ -n "$PYBIN" ]; then
    SLUG_SINGLE="$(
        "$PYBIN" - "$PROJECT_DIR" <<'PYEOF' 2>/dev/null
import os, re, sys
print(re.sub(r"[^A-Za-z0-9]+", "-", os.path.abspath(sys.argv[1])))
PYEOF
    )"
fi
if [ -z "$SLUG_SINGLE" ]; then
    SLUG_SINGLE="$(printf '%s' "$PROJECT_DIR" | sed 's/[^A-Za-z0-9][^A-Za-z0-9]*/-/g')"
fi

# The Claude Code harness maps a Windows drive colon to '--' (so
# "D:\claudetools" -> "D--claudetools"), while the single-dash collapse above
# produces "D-claudetools". Reproduce the harness rule by doubling a leading
# "<drive>-" into "<drive>--".
SLUG_DOUBLE="$(printf '%s' "$SLUG_SINGLE" | sed 's/^\([A-Za-z]\)-/\1--/')"

# Resolve the profile dir. Try the EXACT candidate slugs in priority order
# (harness double-dash first, then single-dash collapse); use the first whose
# profile memory dir actually exists.
PROJ_ROOT="$HOME_DIR/.claude/projects"
SLUG=""
PROFILE_MEM=""
SKIP_PROFILE=0
for cand_slug in "$SLUG_DOUBLE" "$SLUG_SINGLE"; do
    [ -n "$cand_slug" ] || continue
    if [ -d "$PROJ_ROOT/$cand_slug/memory" ]; then
        SLUG="$cand_slug"
        PROFILE_MEM="$PROJ_ROOT/$cand_slug/memory"
        break
    elif [ -d "$PROJ_ROOT/$cand_slug" ]; then
        SLUG="$cand_slug"
        PROFILE_MEM="$PROJ_ROOT/$cand_slug/memory"
        break
    fi
done

# ONLY if no exact candidate exists, fall back to a case-insensitive tail-scan
# of $HOME/.claude/projects/*/memory for a dir whose slug ends with the repo's
# basename. If MORE THAN ONE dir matches, do NOT guess -- skip the profile side
# entirely to avoid cross-project contamination.
if [ -z "$PROFILE_MEM" ]; then
    # Default to the harness (double-dash) slug for messaging / dir creation.
    SLUG="$SLUG_DOUBLE"
    PROFILE_MEM="$PROJ_ROOT/$SLUG/memory"
    if [ -d "$PROJ_ROOT" ]; then
        BASE="$(basename -- "$ROOT")"
        BASE_SLUG="$(printf '%s' "$BASE" | sed 's/[^A-Za-z0-9][^A-Za-z0-9]*/-/g' | tr 'A-Z' 'a-z')"
        MATCH_COUNT=0
        MATCH_LIST=""
        MATCH_DIR=""
        for cand in "$PROJ_ROOT"/*/memory; do
            [ -d "$cand" ] || continue
            cand_parent="$(basename -- "$(dirname -- "$cand")")"
            cand_lc="$(printf '%s' "$cand_parent" | tr 'A-Z' 'a-z')"
            case "$cand_lc" in
                *"$BASE_SLUG")
                    MATCH_COUNT=$((MATCH_COUNT + 1))
                    MATCH_DIR="$cand"
                    if [ -z "$MATCH_LIST" ]; then
                        MATCH_LIST="$cand_parent"
                    else
                        MATCH_LIST="$MATCH_LIST, $cand_parent"
                    fi
                    ;;
            esac
        done
        if [ "$MATCH_COUNT" -gt 1 ]; then
            echo "[WARNING] multiple profile dirs matched ($MATCH_LIST); skipping profile sync to avoid cross-project contamination"
            SKIP_PROFILE=1
        elif [ "$MATCH_COUNT" -eq 1 ]; then
            PROFILE_MEM="$MATCH_DIR"
            SLUG="$(basename -- "$(dirname -- "$MATCH_DIR")")"
        fi
    fi
fi

echo "[INFO] sync-memory.sh"
if [ "$DRY_RUN" -eq 1 ]; then
    echo "[INFO] MODE: DRY-RUN (no files will be copied, deleted, or created)"
else
    echo "[INFO] MODE: APPLY (mirror -- repo is source of truth)"
fi
echo "[INFO] repo store    : $REPO_MEM"
if [ "$SKIP_PROFILE" -eq 1 ]; then
    echo "[INFO] profile store : (skipped -- ambiguous match)"
    echo "[INFO] profile slug  : (skipped -- ambiguous match)"
    echo "[INFO] ----- summary -----"
    echo "[INFO] profile sync SKIPPED: multiple candidate profile dirs matched; refusing to guess."
    echo "[INFO] mirror mode: repo is source of truth; profile is synced to match."
    exit 0
fi
echo "[INFO] profile store : $PROFILE_MEM"
echo "[INFO] profile slug  : $SLUG"

# Create the profile dir (apply mode only).
if [ ! -d "$PROFILE_MEM" ]; then
    if [ "$DRY_RUN" -eq 1 ]; then
        echo "[INFO] would create profile dir: $PROFILE_MEM"
    else
        mkdir -p "$PROFILE_MEM" || {
            echo "[ERROR] could not create profile dir: $PROFILE_MEM" >&2
            exit 1
        }
        echo "[OK] created profile dir: $PROFILE_MEM"
    fi
fi

# --- Build the union of *.md basenames (excluding MEMORY.md) ----------------
# MEMORY.md (the human index) is intentionally NOT synced -- the repo index is
# authoritative and the profile index is regenerated by the harness; copying it
# either way would create a guaranteed perpetual "conflict".

list_md() {
    # $1 = dir ; prints basenames of *.md except MEMORY.md, one per line
    dir="$1"
    [ -d "$dir" ] || return 0
    for f in "$dir"/*.md; do
        [ -e "$f" ] || continue
        b="$(basename -- "$f")"
        case "$b" in
            MEMORY.md|memory.md) continue ;;
        esac
        printf '%s\n' "$b"
    done
}

# Collect names into a deduped, sorted list using a temp file (portable).
NAMES_TMP="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/sync-memory-names.$$")"
trap 'rm -f "$NAMES_TMP"' EXIT INT TERM
{
    list_md "$REPO_MEM"
    [ -d "$PROFILE_MEM" ] && list_md "$PROFILE_MEM"
} | sort -u > "$NAMES_TMP"

# --- Safety check: refuse to mirror from a suspiciously-empty repo store ----
# Count *.md files in REPO_MEM (excluding MEMORY.md). If too few, abort BEFORE
# any destructive op so a corrupted/empty repo can't wipe the profile store.
REPO_COUNT=0
for f in "$REPO_MEM"/*.md; do
    [ -e "$f" ] || continue
    b="$(basename -- "$f")"
    case "$b" in
        MEMORY.md|memory.md) continue ;;
    esac
    REPO_COUNT=$((REPO_COUNT + 1))
done

if [ "$REPO_COUNT" -lt 5 ]; then
    echo "[ERROR] repo memory store has only $REPO_COUNT file(s); refusing to mirror -- would wipe profile store. Verify repo state and re-run." >&2
    exit 1
fi

copied_r2p=0
deleted_profile=0
overwrote_profile=0
identical=0

# Copy helper honoring dry-run.
#   $1 src, $2 dst, $3 label, $4 (optional) overwrite=1 to permit overwriting
#   an existing destination. Default behavior is no-clobber (STRICT_NO_CLOBBER).
do_copy() {
    src="$1"; dst="$2"; label="$3"; overwrite="${4:-0}"
    if [ "$DRY_RUN" -eq 1 ]; then
        echo "[DRY-RUN] would copy $label : $(basename -- "$src")"
    else
        # Belt-and-suspenders no-clobber: re-check destination existence to
        # close any TOCTOU window. In MIRROR MODE the conflict path
        # legitimately overwrites, so callers pass overwrite=1 for that case.
        if [ -e "$dst" ] && [ "$overwrite" -ne 1 ]; then
            echo "[SKIP] dst appeared, not overwriting: $(basename -- "$dst")"
            return 0
        fi
        # cp -p preserves mtime; portable across BSD/GNU.
        if cp -p "$src" "$dst" 2>/dev/null || cp "$src" "$dst"; then
            echo "[OK] copied $label : $(basename -- "$src")"
        else
            echo "[ERROR] failed to copy $label : $(basename -- "$src")" >&2
            return 1
        fi
    fi
}

# Delete helper honoring dry-run. Only ever called on PROFILE side.
do_delete_profile() {
    target="$1"
    if [ "$DRY_RUN" -eq 1 ]; then
        echo "[DRY-RUN] would DELETE from PROFILE : $(basename -- "$target")"
    else
        if rm -f -- "$target"; then
            echo "[OK] deleted from PROFILE : $(basename -- "$target")"
        else
            echo "[ERROR] failed to delete from PROFILE : $(basename -- "$target")" >&2
            return 1
        fi
    fi
}

while IFS= read -r name; do
    [ -n "$name" ] || continue
    r="$REPO_MEM/$name"
    p="$PROFILE_MEM/$name"
    if [ -f "$r" ] && [ ! -f "$p" ]; then
        # In repo, not in profile -> copy repo -> profile.
        do_copy "$r" "$p" "REPO->PROFILE" && copied_r2p=$((copied_r2p + 1))
    elif [ ! -f "$r" ] && [ -f "$p" ]; then
        # In profile, not in repo -> repo absence is a deliberate removal;
        # DELETE from profile.
        do_delete_profile "$p" && deleted_profile=$((deleted_profile + 1))
    elif [ -f "$r" ] && [ -f "$p" ]; then
        if cmp -s "$r" "$p"; then
            identical=$((identical + 1))
        else
            # Mirror mode: repo wins, overwrite profile.
            if [ "$DRY_RUN" -eq 1 ]; then
                echo "[DRY-RUN] would OVERWRITE PROFILE (repo newer) : $name"
                overwrote_profile=$((overwrote_profile + 1))
            else
                if do_copy "$r" "$p" "REPO->PROFILE (overwrite)" 1; then
                    overwrote_profile=$((overwrote_profile + 1))
                fi
            fi
        fi
    fi
done < "$NAMES_TMP"

echo "[INFO] ----- summary -----"
echo "[INFO] copied REPO -> PROFILE         : $copied_r2p"
echo "[INFO] deleted from PROFILE           : $deleted_profile"
echo "[INFO] overwrote PROFILE (repo-newer) : $overwrote_profile"
echo "[INFO] identical (no action)          : $identical"
if [ "$DRY_RUN" -eq 1 ]; then
    echo "[INFO] DRY-RUN: nothing was changed."
fi
echo "[INFO] mirror mode: repo is source of truth; profile is synced to match."

exit 0
