#!/usr/bin/env bash
set -euo pipefail

# ─── Mosaic Framework Installer ──────────────────────────────────────────────
#
# Installs/upgrades the framework DATA to ~/.config/mosaic/.
# No executables are placed on PATH — the mosaic npm CLI is the only binary.
#
# Called by tools/install.sh (the unified installer). Can also be run directly.
#
# Environment:
#   MOSAIC_HOME              — target directory (default: ~/.config/mosaic)
#   MOSAIC_INSTALL_MODE      — prompt|keep|overwrite (default: prompt)
#   MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING — 1 to bypass MCP check
#   MOSAIC_SKIP_SKILLS_SYNC  — 1 to skip skill sync
# ──────────────────────────────────────────────────────────────────────────────

SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"

# Files/dirs protected from rsync --delete during sync. NOTE: framework-owned
# entries (CONSTITUTION/AGENTS/STANDARDS) ARE re-applied afterward by
# reconcile_framework_files (overwrite + backup-once); the rest stay user-owned.
# User-created content in these paths survives rsync --delete.
#
# fleet/* — the framework SEEDS fleet/examples, fleet/roles, fleet/profiles, and
# fleet/roster.schema.json (synced normally — every fleet/roles/*.md role contract
# and fleet/profiles/*.yaml system-type profile lands automatically via this sync,
# so no per-file entry is needed; the preserved "fleet/*.yaml" glob is anchored to
# the top level only and does NOT shadow fleet/profiles/*.yaml). The user's
# own fleet files MUST
# survive `mosaic update` (which runs this sync automatically): the active
# roster (`fleet/roster.yaml` + any other `fleet/*.yaml`), per-agent env
# (`fleet/agents/`), heartbeat run dir (`fleet/run/`), and the Mosaic-native
# backlog-of-record store (`fleet/backlog/` — embedded PGlite data dir; see
# packages/mosaic/src/commands/fleet-backlog.ts). Without these, an update
# wipes the operator's fleet AND their backlog. Glob entries are honored by
# both the rsync path (`--exclude`) and the glob-aware cp fallback below.
#
# fleet/roles.local — the persona OVERRIDE layer (H4). Baseline personas in
# fleet/roles/ are reseeded normally on every update (delivering new baseline
# personas), so any local edit there would be clobbered. User customizations
# and user-ADDED personas instead live in fleet/roles.local/ and MUST survive
# `mosaic update` — they win over the baseline on merge (AC-NS-7; see
# packages/mosaic/src/commands/fleet-personas.ts).
PRESERVE_PATHS=("CONSTITUTION.md" "AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials" "fleet/*.yaml" "fleet/agents" "fleet/run" "fleet/backlog" "fleet/roles.local")

# Framework-owned contract files: re-copied from defaults/ on every upgrade (the
# user must not edit them; a divergent copy is backed up once before overwrite).
# USER_SEEDED files are written once on first install, then owned by the user.
# Both lists are APPEND-FRIENDLY — add a new shipped framework file here and to the
# matching list in packages/mosaic/src/config/file-adapter.ts.
FRAMEWORK_OWNED=("CONSTITUTION.md" "AGENTS.md" "STANDARDS.md")
USER_SEEDED=("TOOLS.md")

# Current framework schema version — bump this when the layout changes.
# The migration system uses this to run upgrade steps.
FRAMEWORK_VERSION=3

# ─── colours ──────────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
  GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m'
  CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m'
else
  GREEN='' YELLOW='' RED='' CYAN='' BOLD='' RESET=''
fi

ok()   { echo -e "  ${GREEN}✓${RESET} $1"; }
warn() { echo -e "  ${YELLOW}⚠${RESET} $1" >&2; }
fail() { echo -e "  ${RED}✗${RESET} $1" >&2; }
step() { echo -e "\n${BOLD}$1${RESET}"; }

# ─── snapshot / restore (crash safety for upgrades) ──────────────────────────
SNAPSHOT_DIR=""
make_snapshot() {
  is_existing_install || return 0
  SNAPSHOT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-snapshot-XXXXXX")"
  cp -a "$TARGET_DIR/." "$SNAPSHOT_DIR/" 2>/dev/null || true
}
restore_snapshot() {
  [[ -n "$SNAPSHOT_DIR" && -d "$SNAPSHOT_DIR" ]] || return 0
  fail "Install interrupted/failed — restoring previous state from snapshot"
  rm -rf "$TARGET_DIR"; mkdir -p "$TARGET_DIR"
  cp -a "$SNAPSHOT_DIR/." "$TARGET_DIR/" 2>/dev/null || true
}
cleanup_snapshot() { [[ -n "$SNAPSHOT_DIR" && -d "$SNAPSHOT_DIR" ]] && rm -rf "$SNAPSHOT_DIR"; SNAPSHOT_DIR=""; }

# Reconcile contract files after sync: framework-owned overwrite (backup-once),
# user-seeded seed-if-absent.
reconcile_framework_files() {
  local defaults="$TARGET_DIR/defaults" f
  [[ -d "$defaults" ]] || return 0
  for f in "${FRAMEWORK_OWNED[@]}"; do
    [[ -f "$defaults/$f" ]] || continue
    # Already current — skip to avoid mtime churn.
    if [[ -f "$TARGET_DIR/$f" ]] && cmp -s "$TARGET_DIR/$f" "$defaults/$f"; then
      continue
    fi
    if [[ -f "$TARGET_DIR/$f" && ! -f "$TARGET_DIR/${f}.pre-constitution.bak" ]]; then
      cp "$TARGET_DIR/$f" "$TARGET_DIR/${f}.pre-constitution.bak"
      warn "$f is now framework-owned and was updated; your previous copy is saved as ${f}.pre-constitution.bak — re-apply intended changes as a .local overlay or policy/ file (see CONSTITUTION.md / constitution/LAYER-MODEL.md)."
    fi
    cp "$defaults/$f" "$TARGET_DIR/$f"
  done
  for f in "${USER_SEEDED[@]}"; do
    [[ -f "$defaults/$f" ]] || continue
    if [[ ! -f "$TARGET_DIR/$f" ]]; then
      cp "$defaults/$f" "$TARGET_DIR/$f"
      ok "Seeded $f from defaults"
    fi
  done
}

# ─── helpers ──────────────────────────────────────────────────────────────────

is_existing_install() {
  [[ -d "$TARGET_DIR" ]] || return 1
  [[ -f "$TARGET_DIR/AGENTS.md" || -f "$TARGET_DIR/SOUL.md" ]]
}

installed_framework_version() {
  local vf="$TARGET_DIR/.framework-version"
  if [[ -f "$vf" ]]; then
    cat "$vf" 2>/dev/null || echo "0"
  else
    # No version file = legacy install (version 0 or 1)
    if [[ -d "$TARGET_DIR/bin" ]]; then
      echo "1"   # Has bin/ → pre-migration legacy
    else
      echo "0"   # Fresh or unknown
    fi
  fi
}

write_framework_version() {
  echo "$FRAMEWORK_VERSION" > "$TARGET_DIR/.framework-version"
}

select_install_mode() {
  case "$INSTALL_MODE" in
    keep|overwrite|prompt) ;;
    *)
      fail "Invalid MOSAIC_INSTALL_MODE='$INSTALL_MODE'. Use: prompt, keep, overwrite."
      exit 1
      ;;
  esac

  if ! is_existing_install; then
    INSTALL_MODE="overwrite"
    return
  fi

  case "$INSTALL_MODE" in
    keep|overwrite) ;;
    prompt)
      if [[ -t 0 ]]; then
        echo ""
        echo "Existing Mosaic install detected at: $TARGET_DIR"
        echo "  1) keep      Update framework, preserve local files (SOUL.md, USER.md, etc.)"
        echo "  2) overwrite Replace everything"
        echo "  3) cancel    Abort"
        printf "Selection [1/2/3] (default: 1): "
        read -r selection
        case "${selection:-1}" in
          1|k|K|keep) INSTALL_MODE="keep" ;;
          2|o|O|overwrite) INSTALL_MODE="overwrite" ;;
          *) fail "Install cancelled."; exit 1 ;;
        esac
      else
        INSTALL_MODE="keep"
      fi
      ;;
  esac
}

sync_framework() {
  local source_real target_real
  source_real="$(cd "$SOURCE_DIR" && pwd -P)"
  target_real="$(mkdir -p "$TARGET_DIR" && cd "$TARGET_DIR" && pwd -P)"

  if [[ "$source_real" == "$target_real" ]]; then
    warn "Source and target are the same directory; skipping file sync."
    return
  fi

  if command -v rsync >/dev/null 2>&1; then
    local rsync_args=(-a --delete --exclude ".git" --exclude ".framework-version" --exclude "*.pre-constitution.bak")

    if [[ "$INSTALL_MODE" == "keep" ]]; then
      # Anchor to the transfer root (leading /) so we preserve the TOP-LEVEL
      # ~/.config/mosaic/<file> without also excluding defaults/<file> from sync
      # (reconcile_framework_files needs the freshly-synced defaults/ copies).
      for path in "${PRESERVE_PATHS[@]}"; do
        rsync_args+=(--exclude "/$path")
      done
    fi

    rsync "${rsync_args[@]}" "$SOURCE_DIR/" "$TARGET_DIR/"
    return
  fi

  # Fallback: cp-based sync. Glob-aware so entries like "fleet/*.yaml" preserve
  # every matching user file (parity with the rsync --exclude path above).
  local preserve_tmp=""
  if [[ "$INSTALL_MODE" == "keep" ]]; then
    preserve_tmp="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-preserve-XXXXXX")"
    local match rel
    for path in "${PRESERVE_PATHS[@]}"; do
      # Unquoted $path lets the glob expand against TARGET_DIR; nullglob makes a
      # non-matching pattern vanish instead of staying literal.
      shopt -s nullglob
      for match in "$TARGET_DIR/"$path; do
        [[ -e "$match" ]] || continue
        rel="${match#"$TARGET_DIR/"}"
        mkdir -p "$preserve_tmp/$(dirname "$rel")"
        cp -R "$match" "$preserve_tmp/$rel"
      done
      shopt -u nullglob
    done
  fi

  find "$TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name ".git" ! -name ".framework-version" ! -name "*.pre-constitution.bak" -exec rm -rf {} +
  cp -R "$SOURCE_DIR"/. "$TARGET_DIR"/
  rm -rf "$TARGET_DIR/.git"

  if [[ -n "$preserve_tmp" ]]; then
    # Restore by re-globbing the SAME patterns against preserve_tmp, so each
    # preserved item is restored at its own relative path (e.g. only
    # fleet/roster.yaml is replaced — the freshly-synced fleet/examples stays).
    for path in "${PRESERVE_PATHS[@]}"; do
      shopt -s nullglob
      for match in "$preserve_tmp/"$path; do
        [[ -e "$match" ]] || continue
        rel="${match#"$preserve_tmp/"}"
        rm -rf "$TARGET_DIR/$rel"
        mkdir -p "$TARGET_DIR/$(dirname "$rel")"
        cp -R "$match" "$TARGET_DIR/$rel"
      done
      shopt -u nullglob
    done
    rm -rf "$preserve_tmp"
  fi
}

# ═══════════════════════════════════════════════════════════════════════════════
# Migrations — run sequentially from the installed version to FRAMEWORK_VERSION
# ═══════════════════════════════════════════════════════════════════════════════

run_migrations() {
  local from_version
  from_version="$(installed_framework_version)"

  if [[ "$from_version" -ge "$FRAMEWORK_VERSION" ]]; then
    return  # Already current
  fi

  step "Running migrations (v${from_version} → v${FRAMEWORK_VERSION})"

  # ── Migration: v0/v1 → v2 ─────────────────────────────────────────────────
  # Remove bin/ directory — all executables now live in the npm CLI.
  # Scripts that were in bin/ are now in tools/_scripts/.
  if [[ "$from_version" -lt 2 ]]; then
    if [[ -d "$TARGET_DIR/bin" ]]; then
      ok "Removing legacy bin/ directory (executables now in npm CLI)"
      rm -rf "$TARGET_DIR/bin"
    fi

    # Remove old mosaic PATH entry from shell profiles
    for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
      if [[ -f "$profile" ]] && grep -qF "$TARGET_DIR/bin" "$profile"; then
        # Remove the PATH line and the comment above it
        sed -i.mosaic-migration-bak \
          -e "\|# Mosaic agent framework|d" \
          -e "\|$TARGET_DIR/bin|d" \
          "$profile"
        ok "Cleaned up old PATH entry from $(basename "$profile")"
        rm -f "${profile}.mosaic-migration-bak"
      fi
    done

    # Remove stale rails/ symlink
    if [[ -L "$TARGET_DIR/rails" ]]; then
      rm -f "$TARGET_DIR/rails"
    fi
  fi

  # ── Migration: v2 → v3 (Constitution split) ───────────────────────────────
  # CONSTITUTION.md / AGENTS.md / STANDARDS.md become framework-owned (overwritten
  # on upgrade). reconcile_framework_files() has already run before this point: it
  # backed up any user-edited copy to <file>.pre-constitution.bak and installed the
  # new framework version. Nothing further to do here — the advisory was emitted at
  # reconcile time. (STANDARDS.local.md composition lands with the overlay composer.)
  if [[ "$from_version" -lt 3 ]]; then
    ok "Migrated to the Constitution layout (framework-owned CONSTITUTION/AGENTS/STANDARDS)"
  fi
}

# ═══════════════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════════════

step "Installing Mosaic framework"

mkdir -p "$TARGET_DIR"
select_install_mode

if [[ "$INSTALL_MODE" == "keep" ]]; then
  ok "Install mode: keep local files (SOUL.md, USER.md, TOOLS.md, memory/)"
else
  ok "Install mode: overwrite"
fi

# Snapshot before any destructive file operation; restore on interrupt/failure.
make_snapshot
trap 'restore_snapshot' ERR INT TERM

sync_framework

# Ensure persistent directories exist
mkdir -p "$TARGET_DIR/memory"
mkdir -p "$TARGET_DIR/credentials"

# Reconcile contract files from defaults/ into the framework root: framework-owned
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
# copy is backed up once); user-seeded files (TOOLS) are written on first install only.
#
# This list must match the framework-contract whitelist in
# packages/mosaic/src/config/file-adapter.ts (FileConfigAdapter.syncFramework).
# SOUL.md and USER.md are intentionally NOT seeded here — they are generated
# by `mosaic init` from templates with user-supplied values.
reconcile_framework_files

# Ensure tool scripts are executable
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
find "$TARGET_DIR/tools/_scripts" -type f -exec chmod +x {} + 2>/dev/null || true

ok "Framework synced to $TARGET_DIR"

# Run migrations before post-install (migrations may remove old bin/ etc.)
run_migrations

# File-system phase complete and consistent — clear the restore trap.
trap - ERR INT TERM
cleanup_snapshot

# Testability / minimal-install hook: stop after the file-system phase, before any
# environment-touching post-install steps (runtime linking, MCP setup, skills, doctor).
if [[ "${MOSAIC_SYNC_ONLY:-0}" == "1" ]]; then
  write_framework_version
  ok "Sync-only mode: file phase complete"
  exit 0
fi

step "Post-install tasks"

SCRIPTS="$TARGET_DIR/tools/_scripts"

if [[ -x "$SCRIPTS/mosaic-link-runtime-assets" ]]; then
  if "$SCRIPTS/mosaic-link-runtime-assets" >/dev/null 2>&1; then
    ok "Runtime assets linked"
  else
    warn "Runtime asset linking failed (non-fatal)"
  fi
fi

if [[ -x "$SCRIPTS/mosaic-ensure-sequential-thinking" ]]; then
  if "$SCRIPTS/mosaic-ensure-sequential-thinking" >/dev/null 2>&1; then
    ok "sequential-thinking MCP configured"
  else
    if [[ "${MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING:-0}" == "1" ]]; then
      warn "sequential-thinking MCP setup bypassed (MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1)"
    else
      fail "sequential-thinking MCP setup failed (hard requirement)."
      exit 1
    fi
  fi
fi

if [[ -x "$SCRIPTS/mosaic-ensure-excalidraw" ]]; then
  "$SCRIPTS/mosaic-ensure-excalidraw" >/dev/null 2>&1 && ok "excalidraw MCP configured" || warn "excalidraw MCP setup failed (non-fatal)"
fi

if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" != "1" ]] && [[ -x "$SCRIPTS/mosaic-sync-skills" ]]; then
  "$SCRIPTS/mosaic-sync-skills" >/dev/null 2>&1 && ok "Skills synced" || warn "Skills sync failed (non-fatal)"
fi

if [[ -x "$SCRIPTS/mosaic-migrate-local-skills" ]]; then
  "$SCRIPTS/mosaic-migrate-local-skills" --apply >/dev/null 2>&1 && ok "Local skills migrated" || warn "Local skill migration failed (non-fatal)"
fi

if [[ -x "$SCRIPTS/mosaic-doctor" ]]; then
  "$SCRIPTS/mosaic-doctor" >/dev/null 2>&1 && ok "Health audit passed" || warn "Health audit reported issues — run 'mosaic doctor' for details"
fi

# Write version stamp AFTER everything succeeds
write_framework_version

# ── Summary ──────────────────────────────────────────────────
echo ""
echo -e "${GREEN}${BOLD}  Mosaic framework installed.${RESET}"
echo ""

if [[ ! -f "$TARGET_DIR/SOUL.md" ]]; then
  echo -e "  Run ${CYAN}mosaic init${RESET} to set up your agent identity."
  echo ""
fi
