#!/usr/bin/env bash
# metodo — CLI del Método. v0: sólo el linter `metodo check`.
#
# Modelo: bin/sync.sh --check de manabo-ui (raíl C11). Su anatomía, que copiamos:
#   - UN solo script (nada de dos implementaciones que diverjan).
#   - El testigo es una comprobación CRUDA del fichero real, no un hash guardado.
#   - Veredicto DISCRETO por check, con la reparación impresa al lado.
#   - Exit-code apto para CI: 0 si pasa, 1 si algo incumple.
#   - Fallo benigno EXPLÍCITO (raíl 8): lo que no aplica se SALTA (SKIP), no falla.
#
# Cada AVISO cita la regla Cn de constitucion-de-facto.md que lo respalda.
# Uso:  metodo check [ruta]     (por defecto, el repo actual)

set -uo pipefail   # -u: variable sin definir es error · pipefail: un pipe hereda el fallo.
                   # (NO ponemos -e: los grep sin match devuelven 1 y no queremos abortar por eso.)

ROOT="."
FAILS=0
SELF_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)"   # raíz del repo del script

# sha256 portable (Git Bash trae sha256sum; si no, shasum -a 256; si ninguno, la deriva no se mide)
_hash() {
  if   command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" 2>/dev/null | awk '{print $1}'
  elif command -v shasum     >/dev/null 2>&1; then shasum -a 256 "$1" 2>/dev/null | awk '{print $1}'
  else return 127; fi
}
_vendored_files() { printf '%s\n' "metodo.md" "bin/metodo"; }   # lo que se estampa y se vigila
_write_checksums() {   # $1 = dir .metodo del destino
  local md="$1" f h; : > "$md/CHECKSUMS"
  for f in $(_vendored_files); do
    h=$(_hash "$md/$f") || { rm -f "$md/CHECKSUMS"; return 1; }
    printf '%s  %s\n' "$h" "$f" >> "$md/CHECKSUMS"
  done
}
_is_matriz() { [ -f "$SELF_ROOT/constitucion-de-facto.md" ]; }   # marca inequívoca de la matriz

# --- veredictos (los cuatro colores del check) ---
hdr()  { printf '\n\033[1m== %s ==\033[0m\n' "$*"; }
ok()   { printf '  \033[32mok  \033[0m %s\n' "$*"; }
bad()  { printf '  \033[31mAVISO\033[0m %s\n' "$*"; FAILS=$((FAILS+1)); }
skip() { printf '  \033[33mSKIP\033[0m %s\n' "$*"; }

# ── Check 1 · integridad de la copia vendorizada (C11) ───────────────────────
# El máster vive en OTRO repo (la matriz), así que el testigo no es el máster sino el
# CHECKSUM sellado al distribuir (metodo init/update). Caza si alguien editó la copia a mano.
check_drift() {
  hdr "integridad de la copia vendorizada (C11)"
  local md="$ROOT/.metodo"
  [ -f "$md/VERSION" ] || { skip "no hay .metodo/VERSION — repo sin el Método vendorizado"; return; }
  local ver estado
  ver=$(grep -E '^version:'  "$md/VERSION" | sed 's/version:[[:space:]]*//')
  estado=$(grep -E '^estado:' "$md/VERSION" | sed 's/estado:[[:space:]]*//')
  ok "VERSION presente (version: ${ver:-?}, estado: ${estado:-?})"
  if [ ! -f "$md/CHECKSUMS" ]; then
    skip "sin CHECKSUMS — corre 'metodo init|update' para sellar la línea base de integridad"
    return
  fi
  if ! _hash "$md/CHECKSUMS" >/dev/null 2>&1; then
    skip "sin sha256sum/shasum disponible → no se puede comprobar la deriva"
    return
  fi
  local rec f cur drift=0
  while read -r rec f; do
    [ -n "$f" ] || continue
    cur=$(_hash "$md/$f")
    if   [ -z "$cur" ];        then bad "falta la copia vendorizada: $f (C11)"; drift=$((drift+1))
    elif [ "$cur" != "$rec" ]; then bad "DERIVA: .metodo/$f editado a mano — nunca se edita la copia (C11); 'metodo update' o revierte"; drift=$((drift+1)); fi
  done < "$md/CHECKSUMS"
  [ "$drift" -eq 0 ] && ok "copia vendorizada íntegra (coincide con lo distribuido)"
}

# ── Check 2 · colisión de ids en los ficheros de estado (C17) ────────────────
check_ids() {
  hdr "colisión de ids en los ficheros de estado (C17)"
  local statefiles=( "$ROOT/doc/AVISOS.md" "$ROOT/doc/backlog.md" )
  local any=0 f dups id
  for f in "${statefiles[@]}"; do
    [ -f "$f" ] || continue
    any=1
    # id DEFINIDO = al principio de una fila de tabla (|) o item de lista (-*) en negrita.
    # Así una MENCIÓN inline ("ver D5") no cuenta como definición (evita falsos positivos).
    dups=$(grep -oE '^[[:space:]]*[|*_-]+[[:space:]]*\**~*\**(D|A|G|H|C|R|F|E|P)[0-9]+' "$f" \
           | grep -oE '(D|A|G|H|C|R|F|E|P)[0-9]+' | sort | uniq -d)
    if [ -n "$dups" ]; then
      while IFS= read -r id; do
        [ -n "$id" ] && bad "id duplicado en $(basename "$f"): $id — renombra o fusiona (C17)"
      done <<< "$dups"
    else
      ok "$(basename "$f"): sin ids duplicados"
    fi
  done
  [ "$any" -eq 0 ] && skip "sin doc/AVISOS.md ni doc/backlog.md (repo sin corpus de estado)"
}

# ── Check 3 · encabezados sin marca de estado (C16) ──────────────────────────
check_headings() {
  hdr "encabezados sin marca de estado (C16)"
  local f hits=0 line
  for f in "$ROOT"/*.md "$ROOT"/doc/*.md; do
    [ -f "$f" ] || continue
    # Un encabezado ## no lleva emoji de ESTADO (el estado vive en las casillas).
    # ⚠️ se excluye a propósito: en un título es "bandera/aviso", no estado de sección.
    while IFS= read -r line; do
      bad "encabezado con estado en $(basename "$f"): ${line} — el estado va en las casillas (C16)"
      hits=$((hits+1))
    done < <(grep -nE '^#{2,6} .*(✅|🟢|🔴|🟠|🟡|❌)' "$f")
  done
  [ "$hits" -eq 0 ] && ok "ningún encabezado ##+ lleva marca de estado"
}

# ── Check 4 · enlaces locales resuelven (integridad del corpus) ──────────────
check_links() {
  hdr "enlaces locales resuelven (integridad)"
  local f dir tgt found=0 broken=0
  for f in "$ROOT"/*.md "$ROOT"/doc/*.md; do
    [ -f "$f" ] || continue
    dir=$(dirname "$f")
    # process substitution (< <(...)) para que el while corra en ESTE shell y bad() cuente
    # (un `grep | while` correría en un subshell y perdería el contador — raíl 1: el testigo
    #  tiene que medir de verdad, no en un proceso que se traga el resultado).
    while IFS= read -r tgt; do
      case "$tgt" in http*|\#*|mailto:*|"") continue;; esac
      tgt="${tgt%%#*}"                       # quita el ancla #seccion
      [ -z "$tgt" ] && continue
      found=$((found+1))
      [ -e "$dir/$tgt" ] || { bad "enlace roto en $(basename "$f"): $tgt"; broken=$((broken+1)); }
    done < <(grep -oE '\]\([^)]+\)' "$f" | sed -E 's/^\]\(//; s/\)$//')
  done
  [ "$broken" -eq 0 ] && ok "$found enlaces locales, todos resuelven"
}

# ── Check 5 · commits Conventional DESDE LA ADOPCIÓN (C25) ───────────────────
# Sólo se juzga lo NUEVO: la adopción es brownfield HACIA ADELANTE, el historial previo no se
# reescribe → no se juzga (juzgarlo dejaría un repo brownfield rojo para siempre — raíl 5/C35).
# El punto de adopción se sella en .metodo/VERSION (adopted_commit); sin sello = greenfield → todo.
check_commits() {
  hdr "commits Conventional desde la adopción (C25)"
  git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1 || { skip "no es un repo git"; return; }
  local since range n s bad_c=0
  since=$(grep -E '^adopted_commit:' "$ROOT/.metodo/VERSION" 2>/dev/null | sed 's/adopted_commit:[[:space:]]*//')
  if [ -n "$since" ] && git -C "$ROOT" rev-parse --verify -q "$since^{commit}" >/dev/null 2>&1; then
    range="${since}..HEAD"
  else
    range="HEAD"; since=""          # sin marcador: repo nuevo → se juzga todo el historial
  fi
  n=$(git -C "$ROOT" rev-list --count $range 2>/dev/null || echo 0)
  [ "${n:-0}" -eq 0 ] && { skip "sin commits nuevos desde la adopción (nada que revisar)"; return; }
  while IFS= read -r s; do
    printf '%s' "$s" | grep -qE '^(feat|fix|docs|ci|refactor|test|chore|build|perf)(\(.+\))?!?: .' \
      || { bad "commit no-Conventional: \"$s\" (C25)"; bad_c=$((bad_c+1)); }
  done < <(git -C "$ROOT" log --format=%s $range 2>/dev/null)
  [ "$bad_c" -eq 0 ] && ok "los ${n} commit(s) desde la adopción siguen Conventional"
}

# ── Check 6 · el puntero de enrutado existe y RESUELVE (raíl 8) ──────────────
# Un puntero sin gate se pudre: es la línea que nadie lee de mañana. Y no basta con que el bloque
# esté — su DESTINO tiene que resolver, porque un puntero a la nada enruta a la nada (y encima con
# la conciencia tranquila de haberlo puesto). Sabe ponerse rojo de tres formas distintas:
# sin CLAUDE.md · con CLAUDE.md pero sin bloque · con bloque cuyo destino no existe.
check_pointer() {
  hdr "puntero de enrutado en CLAUDE.md (raíl 8)"
  [ -d "$ROOT/.metodo" ] || { skip "no hay .metodo/ — repo sin el Método vendorizado"; return; }
  local cm="$ROOT/CLAUDE.md"
  [ -f "$cm" ] || { bad "no hay CLAUDE.md — el Método VIAJA pero nada lo enruta (raíl 8); 'metodo update' desde la matriz"; return; }
  if ! grep -qxF '<!-- metodo:puntero -->' "$cm" || ! grep -qxF '<!-- /metodo:puntero -->' "$cm"; then
    bad "CLAUDE.md sin el bloque 'metodo:puntero' — el Método viaja y nada lo abre; 'metodo update'"; return
  fi
  ok "CLAUDE.md lleva el bloque del puntero"
  local tgt found=0 broken=0 l
  while IFS= read -r l; do
    tgt="${l%%#*}"; [ -z "$tgt" ] && continue
    found=$((found+1))
    [ -e "$ROOT/$tgt" ] || { bad "el puntero apunta a algo que NO EXISTE: $tgt"; broken=$((broken+1)); }
  done < <(awk '/^<!-- metodo:puntero -->$/{f=1;next} /^<!-- \/metodo:puntero -->$/{f=0} f' "$cm" \
           | grep -oE '\]\([^)]+\)' | sed -E 's/^\]\(//; s/\)$//')
  [ "$found" -eq 0 ] && { bad "el bloque del puntero no lleva NINGÚN enlace — no enruta a nada"; return; }
  [ "$broken" -eq 0 ] && ok "el destino del puntero resuelve ($found enlace(s) comprobado(s))"
}

# ── plantillas de doc/ (sólo se crean si faltan — brownfield: no pisar) ───────
_scaffold_docs() {   # $1 = repo destino
  # La casa de docs es UNA y se llama doc/ (el linter ancla ahí el corpus de estado).
  [ -d "$1/doc" ] && { echo "  (doc/ ya existe → no scaffoldeo el corpus)"; return 0; }
  # Una casa con otro nombre NO se duplica en silencio: se pide el rename (cicatriz: kubernetes
  # tenía documentacion/, el guard no la conocía y el init dejó DOS carpetas de documentación).
  local alt; for alt in docs documentacion documentation; do
    [ -d "$1/$alt" ] && { echo "  ⚠ $alt/ existe pero la convención es doc/ — haz 'git mv $alt doc' (si no, el linter no ve el corpus de estado). No scaffoldeo."; return 0; }
  done
  local d="$1/doc"; mkdir -p "$d"
  [ -f "$d/backlog.md" ] || cat > "$d/backlog.md" <<'EOF'
# Backlog

> Tres capas: **este backlog** = estado (metas + decisiones cerradas `D` + items) ·
> AVISOS.md = bandera (se lee primero) · bitacora.md = relato append-only.
> Método vendorizado en ../.metodo/metodo.md. ⛔ Las `D` son inmutables. Un `##` no lleva marca de
> estado; abierto = tiene `[ ]`.

## Decisiones cerradas

## Abierto
- [ ] (siembra aquí el trabajo ABIERTO de hoy — brownfield: el pasado no se reconstruye)
EOF
  [ -f "$d/AVISOS.md" ] || cat > "$d/AVISOS.md" <<'EOF'
# ⚠️ AVISOS

**Lo que hay que saber el primer día.** Se lee antes que el backlog. Cada aviso dice quién lo cierra.

| # | Qué | Estado | Quién lo cierra |
|---|---|---|---|
EOF
  [ -f "$d/bitacora.md" ] || cat > "$d/bitacora.md" <<'EOF'
# Bitácora

*Relato append-only. Lo nuevo va ABAJO. Se lee por fecha.*

---
EOF
}

# ── el PUNTERO de enrutado (bloque gestionado en CLAUDE.md) ──────────────────
# Por qué en CLAUDE.md y sólo ahí: es el ÚNICO fichero que el harness carga solo en toda sesión
# sobre el repo. Un puntero en cualquier otro sitio reproduce la enfermedad que viene a curar
# ("protege si alguien lo abre") — medido el 2026-08-19: la copia viajaba a 9 de 18 repos sin que
# nada la abriera. El bloque va entre marcadores y `update` sustituye SÓLO su interior: los
# CLAUDE.md ricos son del repo, no nuestros, y no se toca un byte fuera.
_PTR_OPEN='<!-- metodo:puntero -->'
_PTR_CLOSE='<!-- /metodo:puntero -->'

_ptr_block() {   # escribe el bloque COMPLETO (marcadores incluidos) por stdout
  cat <<'PTREOF'
<!-- metodo:puntero -->
⛔ **Antes de tocar nada, lee el Método: [`.metodo/metodo.md`](.metodo/metodo.md)** — los raíles
transversales, completos y con sus cicatrices. **Viaja con este repo y aquí NO se edita**: su fuente
es el repo `metodo` (la matriz), que lo re-estampa con `metodo update` — este bloque incluido.
⇒ Del corpus de este repo, **`doc/AVISOS.md` se lee ANTES que el backlog**. El gate de todo esto es
**`metodo check .`** — y si sale rojo, no lo apagues: arréglalo o dilo.
⇒ *Existe porque el 2026-08-19 se midió que el Método viajaba a 9 de 18 repos **sin que nada lo
abriera**. Una constitución solo gobierna si alguien la lee: es su propio raíl `§8`.*
<!-- /metodo:puntero -->
PTREOF
}

_stamp_pointer() {   # $1 = repo destino
  local tgt="$1" cm="$1/CLAUDE.md" blk tmp
  blk=$(mktemp); _ptr_block > "$blk"
  tmp=$(mktemp)
  if [ ! -f "$cm" ]; then
    # No hay CLAUDE.md: se crea MÍNIMO (título + bloque y nada más). Lo que la matriz no puede
    # mantener no se siembra: se pudriría (raíl 7).
    { printf '# %s\n\n' "$(basename "$(cd "$tgt" && pwd)")"; cat "$blk"; } > "$cm"
    rm -f "$blk" "$tmp"; echo "  CLAUDE.md creado (mínimo) con el puntero"; return
  fi
  if grep -qxF "$_PTR_OPEN" "$cm" && grep -qxF "$_PTR_CLOSE" "$cm"; then
    # Ya está: se sustituye SÓLO el interior de los marcadores (idempotente).
    awk -v blk="$blk" -v o="$_PTR_OPEN" -v c="$_PTR_CLOSE" '
      $0 == o { inb=1; while ((getline l < blk) > 0) print l; close(blk); next }
      $0 == c { inb=0; next }
      inb     { next }
      { print }' "$cm" > "$tmp" && mv "$tmp" "$cm"
    rm -f "$blk"; echo "  CLAUDE.md: puntero re-estampado (sólo el interior de los marcadores)"; return
  fi
  # Existe pero sin bloque: se inserta ANTES del primer `## `; si no hay, tras el H1; si no, arriba.
  awk -v blk="$blk" '
    function put(  l){ while ((getline l < blk) > 0) print l; close(blk); print ""; done=1 }
    !done && /^## / { put() }
    { print }
    NR==1 && $0 ~ /^# / { h1=1 }
    END { if (!done) exit 9 }' "$cm" > "$tmp"
  if [ $? -eq 9 ]; then
    # sin ningún `## `: tras el H1 (o al principio si no hay H1)
    awk -v blk="$blk" '
      function put(  l){ print ""; while ((getline l < blk) > 0) print l; done=1 }
      NR==1 { print; if ($0 ~ /^# /) put(); else { print ""; } ; next }
      NR==2 && !done { put(); print; next }
      { print }' "$cm" > "$tmp"
  fi
  mv "$tmp" "$cm"; rm -f "$blk"
  echo "  CLAUDE.md: puntero insertado antes del primer encabezado"
}

# ── metodo init <repo> — vendoriza el Método a un repo por primera vez ────────
cmd_init() {
  local tgt="${1:-.}"
  _is_matriz || { echo "metodo init: sólo se corre desde la MATRIZ, no desde una copia vendorizada."; exit 2; }
  [ -d "$tgt" ] || { echo "metodo init: no existe el destino: $tgt"; exit 2; }
  local md="$tgt/.metodo"
  [ -e "$md" ] && { echo "metodo init: $tgt ya tiene .metodo/ — usa 'metodo update $tgt'."; exit 2; }
  mkdir -p "$md/bin"
  cp "$SELF_ROOT/metodo.md"  "$md/metodo.md"
  cp "$SELF_ROOT/bin/metodo" "$md/bin/metodo"; chmod +x "$md/bin/metodo" 2>/dev/null || true
  local mver adopt
  mver=$(grep -E '^version:' "$SELF_ROOT/.metodo/VERSION" 2>/dev/null | sed 's/version:[[:space:]]*//')
  adopt=$(git -C "$tgt" rev-parse HEAD 2>/dev/null || echo "")   # marca de adopción brownfield
  { echo "# Copia vendorizada del Método (raíl C11). Gestionada por 'metodo init/update'."
    echo "master: metodo"; echo "version: ${mver:-?}"; echo "estado: vendored"
    [ -n "$adopt" ] && echo "adopted_commit: $adopt"; } > "$md/VERSION"
  _write_checksums "$md" || echo "  (aviso: sin sha256 → CHECKSUMS no escrito; la deriva no se medirá)"
  printf '* text eol=lf\n' > "$md/.gitattributes"   # un clone en Windows no mete CRLF ni falsea el checksum
  [ -f "$md/realimentacion.md" ] || cat > "$md/realimentacion.md" <<EOF
# Realimentación del Método — repo: $(basename "$(cd "$tgt" && pwd)")

*Log append-only local, viaja con el repo. Anota cuando trabajar aquí enseñe algo del Método.
Etiquetas: [NUEVA-REGLA] · [REFINA Cn] · [Cn-NO-ENCAJA] · [FALSA]. Se cosecha con 'metodo harvest'.*

---
EOF
  _scaffold_docs "$tgt"
  _stamp_pointer "$tgt"
  echo "metodo init: $tgt inicializado — .metodo/ (máster $mver) + doc/ de tres capas."
  echo "  siguiente: siembra doc/backlog.md con el trabajo abierto de hoy y corre 'metodo check .'"
}

# ── metodo update <repo> — re-estampa el máster+linter en un repo ya iniciado ─
cmd_update() {
  local tgt="${1:-.}"
  _is_matriz || { echo "metodo update: sólo se corre desde la MATRIZ."; exit 2; }
  local md="$tgt/.metodo"
  [ -d "$md" ] || { echo "metodo update: $tgt no tiene .metodo/ — usa 'metodo init $tgt'."; exit 2; }
  cp "$SELF_ROOT/metodo.md"  "$md/metodo.md"
  cp "$SELF_ROOT/bin/metodo" "$md/bin/metodo"; chmod +x "$md/bin/metodo" 2>/dev/null || true
  local mver adopt
  mver=$(grep -E '^version:' "$SELF_ROOT/.metodo/VERSION" 2>/dev/null | sed 's/version:[[:space:]]*//')
  adopt=$(grep -E '^adopted_commit:' "$md/VERSION" 2>/dev/null | sed 's/adopted_commit:[[:space:]]*//')  # se preserva
  { echo "# Copia vendorizada del Método (raíl C11). Gestionada por 'metodo init/update'."
    echo "master: metodo"; echo "version: ${mver:-?}"; echo "estado: vendored"
    [ -n "$adopt" ] && echo "adopted_commit: $adopt"; } > "$md/VERSION"
  _write_checksums "$md" || echo "  (aviso: sin sha256 → CHECKSUMS no escrito)"
  printf '* text eol=lf\n' > "$md/.gitattributes"
  _stamp_pointer "$tgt"
  echo "metodo update: $tgt actualizado al máster $mver (metodo.md + linter, byte a byte); realimentacion.md y adopted_commit preservados."
}

cmd_check() {
  printf 'metodo check — %s\n' "$ROOT"
  check_drift
  check_ids
  check_headings
  check_links
  check_commits
  check_pointer
  hdr "resultado"
  if [ "$FAILS" -eq 0 ]; then
    printf '  \033[32mPASA el Método\033[0m (0 avisos)\n'; exit 0
  else
    printf '  \033[31m%d aviso(s) — el Método NO pasa\033[0m\n' "$FAILS"; exit 1
  fi
}

case "${1:-}" in
  check)             ROOT="${2:-.}"; cmd_check ;;
  init)              cmd_init "${2:-.}" ;;
  update)            cmd_update "${2:-.}" ;;
  harvest)           printf "metodo harvest: aún no implementado (recogerá los .metodo/realimentacion.md al buzón de la matriz).\n"; exit 2 ;;
  ""|help|-h|--help) printf 'uso: metodo <check|init|update> [ruta]\n  check   linter · init  vendoriza a un repo nuevo · update  re-estampa (desde la matriz)\n'; exit 2 ;;
  *)                 printf "metodo: subcomando desconocido: %s\n" "$1"; exit 2 ;;
esac
