#!/usr/bin/env bash
# pmo-roadmap pre-commit shim.
#
# Every structural rule check (contract presence/freshness/checkboxes,
# shipped-story detection, atomicity, evidence pairing) lives in the
# dw_pmo core and runs through `dw gate`. This shim only:
#   - wires configuration (.githooks/pre-commit.config),
#   - invokes the gate (fail closed if python3 is unavailable),
#   - exposes the documented extension seam (.githooks/pre-commit.local),
#   - captures the consented work-log payload,
#   - cleans up the contract artifacts on success.
#
# Bash 3.2 compatible. macOS + Linux. python3 is a hard dependency of
# the gate itself.

set -u

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
if [ -z "$REPO_ROOT" ]; then
  echo "pmo-roadmap pre-commit: not in a git repo, skipping." >&2
  exit 0
fi

HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
CONTRACT_FILE="$REPO_ROOT/.tmp/CONTRACT.md"
# shellcheck disable=SC2034  # documented seam path, readable by pre-commit.local
BUNDLE_OK_FILE="$REPO_ROOT/.tmp/BUNDLE-OK.md"
EXPECTED_BOXES="${EXPECTED_BOXES:-7}"
# PMO_WORK_LOG_DIR precedence: pre-commit.config > environment > default.
PMO_WORK_LOG_ENABLED="${PMO_WORK_LOG_ENABLED:-0}"
PMO_WORK_LOG_DIR="${PMO_WORK_LOG_DIR:-${HOME:-}/.work/log}"
PMO_WORK_LOG_MAX_DIFF_BYTES="${PMO_WORK_LOG_MAX_DIFF_BYTES:-120000}"
PMO_WORK_LOG_PROJECT_SLUG="${PMO_WORK_LOG_PROJECT_SLUG:-}"
PMO_WORK_LOG_ID="${PMO_WORK_LOG_ID:-}"
PMO_WORK_LOG_EXCLUDE_REGEX="${PMO_WORK_LOG_EXCLUDE_REGEX:-}"

# Project config seam — variable overrides only; structural rules go in
# pre-commit.local (sourced after the gate passes).
if [ -f "$REPO_ROOT/.githooks/pre-commit.config" ]; then
  # shellcheck disable=SC1091
  . "$REPO_ROOT/.githooks/pre-commit.config"
fi

bar() {
  printf '%s\n' "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}

# Available to pre-commit.local rules (sourced below), so shellcheck
# cannot see the call sites.
# shellcheck disable=SC2329
fail() {
  bar
  echo "✗ $1" >&2
  bar
  exit 1
}

# ── run the gate (fail closed without python3) ──────────────────────

PYTHON="${PMO_GATE_PYTHON:-}"
if [ -z "$PYTHON" ]; then
  PYTHON="$(command -v python3 || true)"
fi
if [ -z "$PYTHON" ] || ! "$PYTHON" -c 'pass' >/dev/null 2>&1; then
  bar
  echo "✗ PMO gate blocked: python3 is required to run the commit gate (dw gate)." >&2
  echo "  Install python3 and retry. The gate fails closed by design;" >&2
  echo "  agents may not bypass with --no-verify." >&2
  bar
  exit 1
fi

DW="$HOOK_DIR/dw"
if [ ! -f "$DW" ]; then
  bar
  echo "✗ PMO gate blocked: $DW not found. Re-run install.sh or update.sh" >&2
  echo "  so the dw CLI and dw_pmo core are installed next to this hook." >&2
  bar
  exit 1
fi

if ! GATE_OUT=$(EXPECTED_BOXES="$EXPECTED_BOXES" \
  PMO_WORK_LOG_ENABLED="$PMO_WORK_LOG_ENABLED" \
  "$PYTHON" "$DW" gate --hook pre-commit --porcelain); then
  # The gate already printed its remediation banner to stderr.
  exit 1
fi

porcelain_value() {
  printf '%s\n' "$GATE_OUT" | sed -n "s/^$1=//p" | sed -n '1p'
}

porcelain_list() {
  printf '%s\n' "$GATE_OUT" | sed -n "s/^$1=//p"
}

# Seam variables for pre-commit.local (newline-joined lists except
# SHIPPED_STORIES, which stays space-joined for compatibility with
# existing `for s in $SHIPPED_STORIES` consumers; paths with spaces are
# fully handled inside the gate itself).
STAGED="$(porcelain_list staged)"
# shellcheck disable=SC2034  # seam variables consumed by sourced pre-commit.local
STAGED_STORIES="$(porcelain_list staged_story)"
# shellcheck disable=SC2034  # seam variable consumed by sourced pre-commit.local
STAGED_EVIDENCE="$(porcelain_list staged_evidence)"
# shellcheck disable=SC2034  # seam variable consumed by sourced pre-commit.local
SHIPPED_STORIES="$(porcelain_list shipped_story | tr '\n' ' ' | sed 's/ *$//')"
SHIPPED_COUNT="$(porcelain_value shipped_count)"
SHIPPED_COUNT="${SHIPPED_COUNT:-0}"
CHECKED_BOXES="$(porcelain_value checked_boxes)"
CHECKED_BOXES="${CHECKED_BOXES:-0}"
REPORTED_BOXES="$(porcelain_value expected_boxes)"
REPORTED_BOXES="${REPORTED_BOXES:-$EXPECTED_BOXES}"
WORKLOG_CAPTURE="$(porcelain_value worklog_capture)"
WORKLOG_CAPTURE="${WORKLOG_CAPTURE:-no}"

# ── work-log capture mechanics (gate decided the preconditions) ─────

git_dir_abs() {
  gd=$(git rev-parse --git-dir 2>/dev/null || true)
  [ -n "$gd" ] || return 1
  case "$gd" in
    /*) printf '%s\n' "$gd" ;;
    *) printf '%s\n' "$REPO_ROOT/$gd" ;;
  esac
}

slugify() {
  printf '%s' "$1" | sed -E 's/[^A-Za-z0-9._-]+/-/g; s/^-+//; s/-+$//'
}

infer_project_slug() {
  if [ -n "$PMO_WORK_LOG_PROJECT_SLUG" ]; then
    slugify "$PMO_WORK_LOG_PROJECT_SLUG"
    return
  fi

  roadmap_dir="$REPO_ROOT/pm/roadmap"
  if [ -d "$roadmap_dir" ]; then
    roadmap_dirs=$(find "$roadmap_dir" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null | sort)
    roadmap_count=$(printf '%s\n' "$roadmap_dirs" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')
    if [ "$roadmap_count" = "1" ]; then
      slugify "$(printf '%s\n' "$roadmap_dirs" | sed -n '1p')"
      return
    fi
  fi

  slugify "$(basename "$REPO_ROOT")"
}

path_hash() {
  printf '%s' "$REPO_ROOT" | cksum | awk '{print $1}'
}

work_log_identity() {
  if [ -n "$PMO_WORK_LOG_ID" ]; then
    slugify "$PMO_WORK_LOG_ID"
  else
    printf '%s-%s\n' "$(infer_project_slug)" "$(path_hash)"
  fi
}

extract_work_log_reasons() {
  awk '
    /^\*\*Work-log reasons:\*\*/ {capture=1; next}
    /^\*\*Work-log exclusions:\*\*/ {capture=0}
    capture {print}
  ' "$CONTRACT_FILE" | sed '/^[[:space:]]*$/d'
}

extract_work_log_exclusions() {
  awk '
    /^\*\*Work-log exclusions:\*\*/ {capture=1; next}
    /^## / {capture=0}
    capture {print}
  ' "$CONTRACT_FILE" | sed '/^[[:space:]]*$/d'
}

work_log_included_paths() {
  if [ -n "$PMO_WORK_LOG_EXCLUDE_REGEX" ]; then
    printf '%s\n' "$STAGED" | grep -Ev "$PMO_WORK_LOG_EXCLUDE_REGEX" || true
  else
    printf '%s\n' "$STAGED"
  fi
}

work_log_omitted_paths() {
  if [ -n "$PMO_WORK_LOG_EXCLUDE_REGEX" ]; then
    printf '%s\n' "$STAGED" | grep -E "$PMO_WORK_LOG_EXCLUDE_REGEX" || true
  fi
}

work_log_cached_name_status() {
  included_paths=$(work_log_included_paths | sed '/^[[:space:]]*$/d')
  [ -n "$included_paths" ] || return 0
  # shellcheck disable=SC2086
  git diff --cached --name-status -- $included_paths 2>/dev/null || true
}

work_log_cached_stat() {
  included_paths=$(work_log_included_paths | sed '/^[[:space:]]*$/d')
  [ -n "$included_paths" ] || return 0
  # shellcheck disable=SC2086
  git diff --cached --stat -- $included_paths 2>/dev/null || true
}

limited_cached_diff() {
  included_paths=$(work_log_included_paths | sed '/^[[:space:]]*$/d')
  [ -n "$included_paths" ] || return 0
  # shellcheck disable=SC2086
  git diff --cached --no-ext-diff --unified=3 -- $included_paths 2>/dev/null | awk -v max="$PMO_WORK_LOG_MAX_DIFF_BYTES" '
    BEGIN { count=0; truncated=0 }
    {
      line=$0 "\n"
      len=length(line)
      if (count + len <= max) {
        printf "%s", line
        count += len
      } else {
        remaining=max-count
        if (remaining > 0) {
          printf "%s", substr(line, 1, remaining)
        }
        printf "\n[PMO_WORK_LOG_DIFF_TRUNCATED]\n"
        truncated=1
        exit
      }
    }
  '
}

capture_work_log_payload() {
  [ "$WORKLOG_CAPTURE" = "yes" ] || return 0

  gd=$(git_dir_abs 2>/dev/null || true)
  if [ -z "$gd" ]; then
    echo "  Work log warning: could not locate .git directory; no pending payload written." >&2
    return 0
  fi

  state_dir="$gd/pmo-work-log"
  pending_file="$state_dir/pending"
  mkdir -p "$state_dir" 2>/dev/null || {
    echo "  Work log warning: could not create $state_dir; no pending payload written." >&2
    return 0
  }

  if [ -f "$pending_file" ]; then
    echo "  Work log warning: overwriting stale pending payload from an earlier aborted commit." >&2
  fi

  branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || echo detached)
  index_tree=$(git write-tree 2>/dev/null || echo unknown)
  capture_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  project_slug=$(infer_project_slug)
  log_id=$(work_log_identity)
  reflog_action=${GIT_REFLOG_ACTION:-}

  tmp_pending="$pending_file.$$"
  {
    echo "PMO_WORK_LOG_PAYLOAD_VERSION=1"
    echo "capture_timestamp=$capture_ts"
    echo "repo_root=$REPO_ROOT"
    echo "git_dir=$gd"
    echo "branch=$branch"
    echo "index_tree=$index_tree"
    echo "project_slug=$project_slug"
    echo "log_identity=$log_id"
    echo "git_reflog_action=$reflog_action"
    echo "max_diff_bytes=$PMO_WORK_LOG_MAX_DIFF_BYTES"
    echo "exclude_regex=$PMO_WORK_LOG_EXCLUDE_REGEX"
    echo "--- WORK_LOG_REASONS ---"
    extract_work_log_reasons
    echo "--- WORK_LOG_EXCLUSIONS ---"
    extract_work_log_exclusions
    echo "--- CONTRACT ---"
    cat "$CONTRACT_FILE"
    echo "--- STAGED_NAME_STATUS ---"
    work_log_cached_name_status
    echo "--- OMITTED_PATHS ---"
    work_log_omitted_paths
    echo "--- STAGED_STAT ---"
    work_log_cached_stat
    echo "--- STAGED_DIFF ---"
    limited_cached_diff
  } > "$tmp_pending" || {
    rm -f "$tmp_pending"
    echo "  Work log warning: could not write pending payload; commit will continue." >&2
    return 0
  }

  mv "$tmp_pending" "$pending_file" || {
    rm -f "$tmp_pending"
    echo "  Work log warning: could not finalize pending payload; commit will continue." >&2
    return 0
  }

  EXTRA_BANNER_LINES="${EXTRA_BANNER_LINES}  Work log payload captured for post-commit finalization.
"
}

# ── project-specific extension seam ─────────────────────────────────
#
# If .githooks/pre-commit.local exists, source it. The local hook can:
#   - read $STAGED, $STAGED_STORIES, $STAGED_EVIDENCE, $SHIPPED_STORIES,
#     $SHIPPED_COUNT, $REPO_ROOT, and call `bar` / `fail` from this shim
#   - call `fail "reason"` to block the commit
#   - append paths to $EXTRA_CLEANUP_FILES (space-separated) to have
#     them removed on success
#   - append lines to $EXTRA_BANNER_LINES to extend the success banner
#
# Local extensions live OUTSIDE the framework and are NOT touched by
# `update.sh`. See PMO-CONTRACT.md §"Extending".

EXTRA_CLEANUP_FILES=""
EXTRA_BANNER_LINES=""

if [ -f "$REPO_ROOT/.githooks/pre-commit.local" ]; then
  # shellcheck disable=SC1091
  . "$REPO_ROOT/.githooks/pre-commit.local"
fi

capture_work_log_payload

# Pass — clean up project sentinel files and reinforce. The contract
# and BUNDLE-OK survive here: commit-msg stamps trailers from them and
# post-commit archives then clears them, so an aborted commit no longer
# consumes the contract.
for f in $EXTRA_CLEANUP_FILES; do
  rm -f "$f"
done

bar
echo "✓ The system accepted your understanding of the project management framework."
echo "  Contract acknowledged ($CHECKED_BOXES/$REPORTED_BOXES checkboxes)."
if [ "$SHIPPED_COUNT" -gt 0 ]; then
  echo "  Stories shipped this commit: $SHIPPED_COUNT (evidence verified by dw gate)."
fi
if [ -n "$EXTRA_BANNER_LINES" ]; then
  printf '%s' "$EXTRA_BANNER_LINES"
fi
echo "  Commit proceeding."
bar

exit 0
