#!/usr/bin/env bash
# scripts/update-agent-docs.sh
#
# Maintains AI agent docs across the ByteLyst workspace with ZERO duplication.
#
# Architecture (locked 2026-05-23):
#   Canonical source of behavior rules lives in ONE file:
#     learning_ai_common_plat/AI.dev/SKILLS/agent-behavior-guidelines.md
#
#   Per-repo files (managed by this script):
#     AGENTS.md                       — repo-specific (hand-maintained).
#                                       Script idempotently prepends a "Read first"
#                                       pointer block pointing to the canonical file.
#     .github/copilot-instructions.md — thin pointer (5 lines). Regenerated.
#     .aider.conf.yml                 — Aider config (reads AGENTS.md). Regenerated.
#     .editorconfig                   — editor config. Regenerated.
#
#   Legacy files deleted by this script (they used to duplicate AGENTS.md content):
#     .cursorrules, .windsurfrules, .clinerules, CLAUDE.md
#
# Usage:
#   ./scripts/update-agent-docs.sh           # apply changes + commit per repo
#   ./scripts/update-agent-docs.sh --dry-run # show what would change, no writes
#   ./scripts/update-agent-docs.sh --no-commit # write files but don't git commit

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPOS_TXT="${SCRIPT_DIR}/../.windsurf/workflows/repos.txt"
BASE_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
CANONICAL_REL_FROM_COMMON_PLAT="AI.dev/SKILLS/agent-behavior-guidelines.md"
DRY_RUN=false
NO_COMMIT=false
CHANGED_REPOS=()

for arg in "$@"; do
  case "$arg" in
    --dry-run) DRY_RUN=true ;;
    --no-commit) NO_COMMIT=true ;;
    -h|--help)
      sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'
      exit 0
      ;;
  esac
done

# ── colours ────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${BLUE}[INFO]${NC}  $*"; }
ok()    { echo -e "${GREEN}[OK]${NC}    $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
err()   { echo -e "${RED}[ERR]${NC}   $*" >&2; }
header(){ echo -e "\n${CYAN}══ $* ══${NC}"; }

# ── per-repo metadata (only fields still needed) ───────────────────────────
# NAME       — display name for headings
# ID         — productId string
# STACK      — short stack description
# BUILD_VFY  — single command shown in copilot pointer & aider.conf
# LINT1-3    — optional lint commands for aider.conf
# AIDER_READ2 — second file aider should read alongside AGENTS.md
set_meta() {
  NAME="" ID="" STACK="" BUILD_VFY=""
  LINT1="" LINT2="" LINT3="" AIDER_READ2="README.md"
  case "$1" in
    learning_ai_common_plat)
      NAME="@bytelyst Common Platform"; ID="(product-agnostic)"
      STACK="TypeScript, ESM, pnpm workspace, Fastify 5, Vitest, Azure Cosmos DB"
      BUILD_VFY="pnpm build && pnpm test && pnpm typecheck"
      LINT1="pnpm build 2>&1 | tail -10"
      LINT2="pnpm test 2>&1 | tail -10"
      LINT3="pnpm typecheck 2>&1 | tail -10"
      ;;
    learning_voice_ai_agent)
      NAME="LysnrAI"; ID="lysnrai"
      STACK="Python 3.12 (desktop/backend) + Next.js 16 (dashboards) + Fastify 5 (microservices in sibling repo)"
      BUILD_VFY="python -m pytest tests/ -v --tb=short"
      LINT1="cd user-dashboard-web && npx tsc --noEmit 2>&1 | tail -10"
      LINT2="python -m pytest tests/ -x --tb=short 2>&1 | tail -10"
      AIDER_READ2="README_MONO_REPO.md"
      ;;
    learning_multimodal_memory_agents)
      NAME="MindLyst"; ID="mindlyst"
      STACK="KMP (shared) + SwiftUI (iOS) + Jetpack Compose (Android) + Next.js 16 (web)"
      BUILD_VFY="./gradlew :shared:compileKotlinIosSimulatorArm64"
      LINT1="cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64 2>&1 | tail -5"
      LINT2="cd mindlyst-native/web && npx next build 2>&1 | tail -10"
      AIDER_READ2="ARCHITECTURE.md"
      ;;
    learning_ai_clock)
      NAME="ChronoMind"; ID="chronomind"
      STACK="Next.js 16 (web) + SwiftUI (iOS/Watch/Mac) + Jetpack Compose (Android) + Fastify 5 (backend)"
      BUILD_VFY="cd web && npm test && npm run typecheck && npm run build"
      LINT1="cd web && npm test 2>&1 | tail -10"
      LINT2="cd web && npm run typecheck 2>&1 | tail -10"
      AIDER_READ2="docs/PRD.md"
      ;;
    learning_ai_fastgap)
      NAME="NomGap"; ID="nomgap"
      STACK="React Native (Expo SDK 55) + TypeScript + Fastify 5 (backend)"
      BUILD_VFY="npm test && npm run typecheck"
      LINT1="npm test 2>&1 | tail -10"
      LINT2="npm run typecheck 2>&1 | tail -10"
      AIDER_READ2="docs/PRD.md"
      ;;
    learning_ai_jarvis_jr)
      NAME="JarvisJr"; ID="jarvisjr"
      STACK="SwiftUI (iOS/Watch/Mac) + Next.js 16 (web) + Jetpack Compose (Android) + Fastify 5 (backend)"
      BUILD_VFY="cd web && npm test && npm run typecheck && npm run build"
      LINT1="cd web && npm test 2>&1 | tail -10"
      LINT2="cd web && npm run typecheck 2>&1 | tail -10"
      AIDER_READ2="docs/ENHANCED_IDEA_v2.md"
      ;;
    learning_ai_peakpulse)
      NAME="PeakPulse"; ID="peakpulse"
      STACK="SwiftUI (iOS 17+), SwiftData, MapKit, WeatherKit, ActivityKit, WidgetKit, App Intents"
      BUILD_VFY="cd ios && xcodegen generate && xcodebuild -scheme PeakPulse -sdk iphonesimulator build"
      LINT1="cd ios && xcodebuild -scheme PeakPulse -sdk iphonesimulator build 2>&1 | tail -20"
      AIDER_READ2="docs/PRD.md"
      ;;
    learning_ai_notes)
      NAME="ByteLyst Agentic Notes"; ID="bytelyst-notes"
      STACK="Docs + Fastify 5 backend scaffold with TypeScript ESM and @bytelyst/* shared packages"
      BUILD_VFY="cd backend && npm test && npm run typecheck && npm run build"
      LINT1="cd backend && npm test 2>&1 | tail -10"
      LINT2="cd backend && npm run typecheck 2>&1 | tail -10"
      LINT3="cd backend && npm run build 2>&1 | tail -10"
      AIDER_READ2="docs/ROADMAP.md"
      ;;
    learning_ai_flowmonk)
      NAME="FlowMonk"; ID="flowmonk"
      STACK="Next.js 16 (web) + React Native/Expo (mobile) + Fastify 5 (backend)"
      BUILD_VFY="cd backend && npm run typecheck && npm run build && cd ../web && npm run typecheck && npm run build && cd ../mobile && npm run typecheck"
      LINT1="cd backend && npm run typecheck 2>&1 | tail -10"
      LINT2="cd web && npm run typecheck 2>&1 | tail -10"
      LINT3="cd mobile && npm run typecheck 2>&1 | tail -10"
      AIDER_READ2="docs/ROADMAP.md"
      ;;
    learning_ai_trails)
      NAME="ActionTrail"; ID="actiontrail"
      STACK="Next.js 16 (web) + Fastify 5 (backend) + TypeScript ESM"
      BUILD_VFY="cd backend && npm test && npm run typecheck && cd ../sdk && npm test && npm run typecheck && cd ../web && npm run typecheck"
      LINT1="cd backend && npm test 2>&1 | tail -10"
      LINT2="cd backend && npm run typecheck 2>&1 | tail -10"
      LINT3="cd sdk && npm run typecheck 2>&1 | tail -10"
      AIDER_READ2="docs/roadmap.md"
      ;;
    learning_ai_local_memory_gpt)
      NAME="Local Memory GPT"; ID="localmemgpt"
      STACK="Fastify 5 + TypeScript ESM (backend) + Next.js 16 (web) + SQLite + Ollama"
      BUILD_VFY="cd backend && npm test && npm run typecheck && npm run build && cd ../web && npm run typecheck && npm run build"
      LINT1="cd backend && npm test 2>&1 | tail -10"
      LINT2="cd backend && npm run typecheck 2>&1 | tail -10"
      LINT3="cd web && npm run typecheck 2>&1 | tail -10"
      ;;
    learning_ai_auth_app)
      NAME="ByteLyst SmartAuth"; ID="smartauth"
      STACK="SwiftUI (iOS/Watch) + Jetpack Compose (Android) companion app + PRD/roadmap for auth features extending platform-service in common-plat"
      BUILD_VFY="cd ios && xcodegen generate && xcodebuild -scheme ByteLystAuth -sdk iphonesimulator build && cd ../android && ./gradlew :app:assembleDebug"
      LINT1="cd ios && xcodegen generate >/dev/null 2>&1 && xcodebuild -scheme ByteLystAuth -sdk iphonesimulator build 2>&1 | tail -20"
      LINT2="cd android && ./gradlew :app:assembleDebug 2>&1 | tail -20"
      ;;
    learning_ai_efforise)
      NAME="EffoRise"; ID="efforise"
      STACK="Vite + React 19 SPA (client/) + Fastify 5 backend (backend/) + React Native/Expo (mobile/) + @bytelyst/* shared packages"
      BUILD_VFY="cd backend && pnpm test && pnpm run typecheck && pnpm run build"
      LINT1="cd backend && pnpm test 2>&1 | tail -10"
      LINT2="cd backend && pnpm run typecheck 2>&1 | tail -10"
      LINT3="cd backend && pnpm run build 2>&1 | tail -10"
      ;;
    learning_ai_local_llms)
      NAME="Local LLM Lab"; ID="localllmlab"
      STACK="Next.js 16 (dashboard) + Ollama + @bytelyst/llm-router + Python (TTS)"
      BUILD_VFY="cd dashboard && pnpm test && pnpm typecheck && pnpm build"
      LINT1="cd dashboard && pnpm test 2>&1 | tail -10"
      LINT2="cd dashboard && pnpm typecheck 2>&1 | tail -10"
      LINT3="cd dashboard && pnpm build 2>&1 | tail -10"
      ;;
    learning_ai_productivity_web)
      NAME="Productivity Web"; ID="(internal)"
      STACK="Next.js 15 + React 19 + TailwindCSS"
      BUILD_VFY="npm run typecheck && npm run build"
      LINT1="npm run typecheck 2>&1 | tail -10"
      LINT2="npm run build 2>&1 | tail -10"
      ;;
    learning_ai_talk2obsidian)
      NAME="Talk2Obs"; ID="talk2obs"
      STACK="Fastify 5 + TypeScript ESM (backend) + Vite + React 19 (web) + SQLite + Ollama + whisper-cpp"
      BUILD_VFY="cd backend && npm test && npm run typecheck"
      LINT1="cd backend && npm test 2>&1 | tail -10"
      LINT2="cd backend && npm run typecheck 2>&1 | tail -10"
      AIDER_READ2="docs/ROADMAP.md"
      ;;
    learning_ai_mac_tooling)
      NAME="Mac Tooling"; ID="(internal)"
      STACK="Python (FastAPI backend) + Vite + React (dashboard) + SQLite + Swift (menu bar)"
      BUILD_VFY="cd dashboard && npm run build"
      LINT1="cd dashboard && npm run build 2>&1 | tail -10"
      ;;
    oss/learning_ai_claw-code-oss)
      NAME="Claw Code OSS"; ID="(upstream)"
      STACK="Rust (workspace) + Python + TypeScript"
      BUILD_VFY="cargo build --workspace && cargo test --workspace"
      LINT1="cargo fmt --check 2>&1 | tail -10"
      LINT2="cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -10"
      LINT3="cargo test --workspace 2>&1 | tail -10"
      ;;
    oss/learning_ai_claw-cowork)
      NAME="Claw Cowork"; ID="clawcowork"
      STACK="Rust (workspace) + React/TypeScript (Tauri frontend) + Python (skills server)"
      BUILD_VFY="cargo build --workspace && cargo test --workspace"
      LINT1="cargo fmt --check 2>&1 | tail -10"
      LINT2="cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -10"
      LINT3="cargo test --workspace 2>&1 | tail -10"
      AIDER_READ2="COWORK.md"
      ;;
    *)
      warn "Unknown repo: $1 — using minimal defaults"
      NAME="$1"; ID="(unknown)"
      STACK="(see AGENTS.md)"
      BUILD_VFY="(see AGENTS.md)"
      ;;
  esac
}

# Compute relative path from a repo to the canonical guidelines file.
# Repos at workspace root (depth 0) → ../learning_ai_common_plat/...
# Repos one level deep (e.g. oss/foo, depth 1) → ../../learning_ai_common_plat/...
# common_plat itself → AI.dev/SKILLS/...
canonical_path_for() {
  local repo="$1"
  if [[ "$repo" == "learning_ai_common_plat" ]]; then
    echo "${CANONICAL_REL_FROM_COMMON_PLAT}"
    return
  fi
  local slashes="${repo//[^\/]/}"
  local depth=${#slashes}
  local prefix=""
  for ((i=0; i<=depth; i++)); do
    prefix+="../"
  done
  echo "${prefix}learning_ai_common_plat/${CANONICAL_REL_FROM_COMMON_PLAT}"
}

# ── file generators ─────────────────────────────────────────────────────────

write_editorconfig() {
  cat > "$1" << 'EOF'
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
EOF
}

write_aider_conf() {
  local dest="$1"
  local lint_block=""
  for cmd in "${LINT1:-}" "${LINT2:-}" "${LINT3:-}"; do
    [[ -z "$cmd" ]] && continue
    lint_block+="  - '${cmd}'"$'\n'
  done
  local read_block="  - AGENTS.md"$'\n'
  [[ -n "${AIDER_READ2:-}" ]] && read_block+="  - ${AIDER_READ2}"$'\n'

  cat > "$dest" << EOF
# .aider.conf.yml — Aider Configuration for ${NAME}
# Auto-generated by learning_ai_common_plat/scripts/update-agent-docs.sh
# Hand-edit will be overwritten on next run.

read:
${read_block}
conventions: AGENTS.md

lint-cmd:
${lint_block}
auto-commits: false
EOF
}

write_copilot_pointer() {
  local dest_dir="$1" canonical="$2"
  mkdir -p "$dest_dir"
  cat > "${dest_dir}/copilot-instructions.md" << EOF
<!-- AUTO-GENERATED by learning_ai_common_plat/scripts/update-agent-docs.sh -->
<!-- DO NOT EDIT. Edit the canonical sources instead. -->

# GitHub Copilot Instructions — ${NAME}

**Project:** ${NAME} — \`${ID}\`
**Stack:** ${STACK}

## Read these (in order) before suggesting code

1. **\`AGENTS.md\`** at the repo root — repo-specific layout, conventions,
   build commands, file ownership.
2. **\`${canonical}\`** —
   ecosystem-wide agent behavior rules (Karpathy + ByteLyst). Identical
   across every repo in the workspace.

Both files together are the complete contract. This pointer file contains
no rules of its own to avoid duplication and drift.

## Quick verification command

\`\`\`bash
${BUILD_VFY}
\`\`\`
EOF
}

# Idempotently prepend a "Read first" pointer block to AGENTS.md.
# The block is delimited by HTML comment markers so we can re-find and
# replace it without touching the rest of AGENTS.md.
#
# Note: awk's -v does not preserve literal newlines reliably, so the
# replacement block is written to a temp file and slurped via getline.
prepend_agents_pointer() {
  local agents_md="$1" canonical="$2"
  [[ ! -f "$agents_md" ]] && return 0

  local begin_marker="<!-- BEGIN: canonical-behavior-pointer (auto-managed) -->"
  local end_marker="<!-- END: canonical-behavior-pointer -->"

  local tmp_block
  tmp_block="$(mktemp)"
  cat > "$tmp_block" << EOF
${begin_marker}
> **Read first (ecosystem-wide agent behavior):**
> [\`${canonical}\`](${canonical})
>
> The link above is the single source of truth for agent behavior across every
> ByteLyst repo (Karpathy + ByteLyst rules: tests sacred, verify before done,
> no shared-infra hand-edits, no \`console.log\`/\`print\`, productId on every
> Cosmos doc, conventional commits, style preservation).
>
> The per-repo content below extends — never duplicates — the canonical rules.
${end_marker}
EOF

  if grep -qF "$begin_marker" "$agents_md"; then
    # Replace existing block in place.
    awk -v begin="$begin_marker" -v endm="$end_marker" -v block_file="$tmp_block" '
      BEGIN {
        while ((getline line < block_file) > 0) {
          new = (new == "" ? line : new "\n" line)
        }
        close(block_file)
        in_block = 0
      }
      $0 == begin { print new; in_block = 1; next }
      in_block && $0 == endm { in_block = 0; next }
      !in_block { print }
    ' "$agents_md" > "${agents_md}.tmp" && mv "${agents_md}.tmp" "$agents_md"
  else
    # Insert after first H1 (line beginning with "# "). If no H1, prepend.
    if grep -qE '^# ' "$agents_md"; then
      awk -v block_file="$tmp_block" '
        BEGIN {
          while ((getline line < block_file) > 0) {
            block = (block == "" ? line : block "\n" line)
          }
          close(block_file)
          inserted = 0
        }
        { print }
        !inserted && /^# / { print ""; print block; inserted = 1 }
      ' "$agents_md" > "${agents_md}.tmp" && mv "${agents_md}.tmp" "$agents_md"
    else
      { cat "$tmp_block"; echo ""; cat "$agents_md"; } > "${agents_md}.tmp" \
        && mv "${agents_md}.tmp" "$agents_md"
    fi
  fi

  rm -f "$tmp_block"
}

# Delete legacy agent files (replaced by the canonical pointer architecture).
delete_legacy_files() {
  local repo_dir="$1"
  local deleted=()
  for f in .cursorrules .windsurfrules .clinerules CLAUDE.md; do
    local target="${repo_dir}/${f}"
    [[ ! -e "$target" ]] && continue
    if git -C "$repo_dir" ls-files --error-unmatch "$f" >/dev/null 2>&1; then
      git -C "$repo_dir" rm -q "$f" 2>/dev/null || rm -f "$target"
    else
      rm -f "$target"
    fi
    deleted+=("$f")
  done
  if [[ ${#deleted[@]} -gt 0 ]]; then
    ok "deleted legacy: ${deleted[*]}"
  fi
}

# ── main loop ───────────────────────────────────────────────────────────────

if [[ ! -f "$REPOS_TXT" ]]; then
  err "repos.txt not found at: ${REPOS_TXT}"
  exit 1
fi

REPOS=()
while IFS= read -r line; do
  [[ "$line" =~ ^[[:space:]]*# ]] && continue
  [[ -z "${line// }" ]] && continue
  REPOS+=("$line")
done < "$REPOS_TXT"

info "Base directory: ${BASE_DIR}"
info "Repos to process: ${#REPOS[@]}"
$DRY_RUN && warn "DRY RUN — no files will be written or committed"
$NO_COMMIT && info "NO-COMMIT mode — files will be written but not committed"

for REPO in "${REPOS[@]}"; do
  header "$REPO"
  REPO_DIR="${BASE_DIR}/${REPO}"

  if [[ ! -d "$REPO_DIR" ]]; then
    warn "Directory not found at ${REPO_DIR} — skipping"
    continue
  fi

  set_meta "$REPO"
  CANONICAL_PATH="$(canonical_path_for "$REPO")"
  info "canonical pointer → ${CANONICAL_PATH}"

  if [[ ! -f "${REPO_DIR}/AGENTS.md" ]]; then
    warn "AGENTS.md not found in ${REPO} — skipping (bootstrap AGENTS.md manually first)"
    continue
  fi

  if $DRY_RUN; then
    info "Would: delete legacy files, rewrite copilot pointer, prepend AGENTS.md pointer"
    info "        regenerate .editorconfig and .aider.conf.yml"
    continue
  fi

  # 1. Delete legacy files (.cursorrules, .windsurfrules, .clinerules, CLAUDE.md)
  delete_legacy_files "$REPO_DIR"

  # 2. Regenerate small config files
  write_editorconfig "${REPO_DIR}/.editorconfig"
  ok ".editorconfig"

  write_aider_conf "${REPO_DIR}/.aider.conf.yml"
  ok ".aider.conf.yml"

  # 3. Rewrite copilot pointer (thin, no rules)
  write_copilot_pointer "${REPO_DIR}/.github" "$CANONICAL_PATH"
  ok ".github/copilot-instructions.md (pointer)"

  # 4. Idempotently prepend canonical pointer to AGENTS.md
  prepend_agents_pointer "${REPO_DIR}/AGENTS.md" "$CANONICAL_PATH"
  ok "AGENTS.md (pointer block ensured)"

  # 5. Commit if there are any changes in this repo
  if $NO_COMMIT; then
    info "Skipping commit (--no-commit)"
    continue
  fi

  if git -C "$REPO_DIR" status --porcelain 2>/dev/null | grep -q .; then
    # Stage only paths that currently exist (legacy file deletes are already
    # staged by `git rm` inside delete_legacy_files()).
    for f in .editorconfig .aider.conf.yml AGENTS.md .github/copilot-instructions.md; do
      [[ -e "${REPO_DIR}/${f}" ]] && git -C "$REPO_DIR" add "$f" 2>/dev/null || true
    done
    if git -C "$REPO_DIR" commit -m "chore(docs): consolidate agent docs to single source of truth

- Delete legacy .cursorrules, .windsurfrules, .clinerules, CLAUDE.md
  (content duplicated AGENTS.md; replaced by canonical pointer architecture)
- Rewrite .github/copilot-instructions.md as thin pointer to AGENTS.md
  and the canonical agent-behavior-guidelines.md
- Prepend canonical 'Read first' pointer block to AGENTS.md
- Regenerate .editorconfig and .aider.conf.yml

Canonical source: learning_ai_common_plat/AI.dev/SKILLS/agent-behavior-guidelines.md
Managed by: learning_ai_common_plat/scripts/update-agent-docs.sh" \
      --no-verify 2>/dev/null; then
      ok "Committed changes in ${REPO}"
      CHANGED_REPOS+=("$REPO")
    else
      warn "Detected changes in ${REPO}, but no commit was created"
    fi
  else
    info "No changes in ${REPO}"
  fi
done

# ── summary ──────────────────────────────────────────────────────────────────
echo ""
if [[ ${#CHANGED_REPOS[@]} -gt 0 ]]; then
  ok "Updated and committed ${#CHANGED_REPOS[@]} repo(s):"
  for r in "${CHANGED_REPOS[@]}"; do
    echo "   • $r"
  done
  echo ""
  info "Push when ready:"
  for r in "${CHANGED_REPOS[@]}"; do
    echo "   git -C ${BASE_DIR}/${r} push"
  done
else
  if $NO_COMMIT; then
    ok "Files regenerated. No commits made (--no-commit). Inspect changes with: git -C <repo> status"
  elif $DRY_RUN; then
    ok "Dry run complete. Re-run without --dry-run to apply."
  else
    ok "All repos already in sync — no changes."
  fi
fi
