#!/usr/bin/env bash
#
# plan-todos.sh  -  manage the Phase 2 plan as a live Todo list.
#
# The Phase 2 plan is broken into a live, reviewable Todo list that updates
# step-by-step as Phase 3 works through it.
#
# State lives in `agent-state.json` under `.plan.todos[]` per
# pipeline/schemas/plan-todos.schema.json. Phase 2 (Planning) emits the
# initial plan; Phase 3 (Dev) iterates step-by-step; Phase 4 (Review)
# inspects completion; Phase 7 (Report) renders the rollup.
#
# Subcommands:
#   init      <task-id> [title]            Initialize with empty todos[] (Phase 2 usually pipes the JSON instead)
#   set       <task-id> <plan-json>        Replace the whole plan from a JSON blob on stdin or arg
#   start     <task-id> <todo-id>          Mark in_progress, set startedAt; refuses if deps unsatisfied
#   complete  <task-id> <todo-id> [notes]  Mark completed, set completedAt
#   fail      <task-id> <todo-id> <reason> Mark failed
#   skip      <task-id> <todo-id> <reason> Mark skipped (counts as satisfied for dependents)
#   next      <task-id>                    Print next pending todo (deps-respecting); empty if none
#   list      <task-id>                    Render Markdown checklist
#   status    <task-id>                    Print one-line summary: "3/7 done, 1 in progress"
#   show      <task-id>                    Print the full plan JSON
#
# Exit codes:
#   0  success
#   1  todo not found / state missing
#   2  bad transition (deps unsatisfied, double-start, ...)
#   64 usage error

set -euo pipefail

CMD="${1:-}"
shift || true

usage() {
  sed -n '2,30p' "$0" >&2
  exit 64
}

[ -z "$CMD" ] && usage

# --- agent-state resolver ---------------------------------------------------

resolve_state() {
  local task_id="$1"
  local found=""
  # multi-repo-pipeline.sh writes agent-state.json under a per-project
  # directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/) - not the
  # ~/.claude/projects/*/state/ path this used to glob, which no writer in
  # the pipeline ever populates.
  for candidate in "$HOME"/.claude/logs/multi-agent/*/"$task_id"/agent-state.json; do
    if [ -f "$candidate" ]; then
      found="$candidate"
      break
    fi
  done
  if [ -z "$found" ]; then
    echo "plan-todos: agent-state.json not found for task=$task_id" >&2
    exit 1
  fi
  printf '%s' "$found"
}

write_state() {
  local path="$1" content="$2"
  # mktemp NEXT TO the target: a $TMPDIR temp file can live on a different
  # filesystem than $HOME, where mv degrades to copy+unlink and loses the
  # atomic-rename guarantee.
  local tmp; tmp=$(mktemp "${path}.XXXXXX")
  printf '%s\n' "$content" > "$tmp"
  mv "$tmp" "$path"
}

iso_now() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }

# --- Subcommands ------------------------------------------------------------

do_init() {
  local task_id="${1:-}"; local title="${2:-Plan}"
  [ -z "$task_id" ] && { echo "usage: init <task-id> [title]" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  local now; now=$(iso_now)
  local new
  new=$(jq --arg t "$title" --arg c "$now" '
    .plan = (.plan // {}) | .plan.title = $t | .plan.createdAt = $c | .plan.todos = []
  ' "$state_path")
  write_state "$state_path" "$new"
}

do_set() {
  local task_id="${1:-}"; local plan_blob="${2:-}"
  [ -z "$task_id" ] && { echo "usage: set <task-id> <plan-json|-(stdin)>" >&2; exit 64; }
  if [ -z "$plan_blob" ] || [ "$plan_blob" = "-" ]; then
    plan_blob=$(cat)
  fi
  # Validate against schema (best-effort  -  jq syntax check, then required-field probe).
  if ! jq -e '.title and (.todos | type == "array")' <<<"$plan_blob" >/dev/null 2>&1; then
    echo "plan-todos: input must be an object with .title (string) and .todos (array)" >&2
    exit 2
  fi
  # Per-item validation: every todo needs a string .id and .task, otherwise
  # downstream jq like `.id + "\t" + .task` crashes on (null + string).
  if ! jq -e '.todos | all(type == "object" and (.id | type == "string") and (.task | type == "string"))' <<<"$plan_blob" >/dev/null 2>&1; then
    echo "plan-todos: every todo must be an object with string .id and .task fields" >&2
    exit 2
  fi
  local state_path; state_path=$(resolve_state "$task_id")
  local now; now=$(iso_now)
  local new
  new=$(jq --argjson p "$plan_blob" --arg c "$now" '
    .plan = ($p | .createdAt = $c)
    | .plan.todos = ((.plan.todos // []) | map(. + {status: (.status // "pending"), deps: (.deps // [])}))
  ' "$state_path")
  write_state "$state_path" "$new"
}

deps_satisfied() {
  # $1 state JSON, $2 todo_id  -  exits 0 if all deps for that todo are completed|skipped.
  local state="$1" todo_id="$2"
  jq -e --arg id "$todo_id" '
    (.plan.todos // []) as $all
    | ($all | map(select(.id == $id))[0]) as $t
    | ($t // empty | .deps // [])
    | all(
        . as $d
        | $all | map(select(.id == $d))[0] // null
        | . != null and (.status == "completed" or .status == "skipped")
      )
  ' <<<"$state" >/dev/null 2>&1
}

do_start() {
  local task_id="${1:-}"; local todo_id="${2:-}"
  [ -z "$task_id" ] || [ -z "$todo_id" ] && { echo "usage: start <task-id> <todo-id>" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  local state; state=$(cat "$state_path")
  # Existence check
  if ! jq -e --arg id "$todo_id" '(.plan.todos // []) | map(select(.id == $id)) | length > 0' <<<"$state" >/dev/null 2>&1; then
    echo "plan-todos: todo '$todo_id' not found" >&2
    exit 1
  fi
  # Deps check
  if ! deps_satisfied "$state" "$todo_id"; then
    echo "plan-todos: deps for '$todo_id' not satisfied (still pending/in_progress/failed)" >&2
    exit 2
  fi
  local now; now=$(iso_now)
  local new
  new=$(jq --arg id "$todo_id" --arg t "$now" '
    .plan.todos |= map(
      if .id == $id then .status = "in_progress" | .startedAt = $t else . end
    )
  ' <<<"$state")
  write_state "$state_path" "$new"
}

do_complete() {
  local task_id="${1:-}"; local todo_id="${2:-}"; local notes="${3:-}"
  [ -z "$task_id" ] || [ -z "$todo_id" ] && { echo "usage: complete <task-id> <todo-id> [notes]" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  local now; now=$(iso_now)
  local new
  new=$(jq --arg id "$todo_id" --arg t "$now" --arg n "$notes" '
    .plan.todos |= map(
      if .id == $id then
        .status = "completed" | .completedAt = $t
        | if ($n != "") then .notes = $n else . end
      else . end
    )
  ' "$state_path")
  write_state "$state_path" "$new"
}

do_fail() {
  local task_id="${1:-}"; local todo_id="${2:-}"; local reason="${3:-}"
  [ -z "$task_id" ] || [ -z "$todo_id" ] || [ -z "$reason" ] && { echo "usage: fail <task-id> <todo-id> <reason>" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  local now; now=$(iso_now)
  local new
  new=$(jq --arg id "$todo_id" --arg t "$now" --arg r "$reason" '
    .plan.todos |= map(
      if .id == $id then .status = "failed" | .completedAt = $t | .failureReason = $r else . end
    )
  ' "$state_path")
  write_state "$state_path" "$new"
}

do_skip() {
  local task_id="${1:-}"; local todo_id="${2:-}"; local reason="${3:-}"
  [ -z "$task_id" ] || [ -z "$todo_id" ] || [ -z "$reason" ] && { echo "usage: skip <task-id> <todo-id> <reason>" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  local now; now=$(iso_now)
  local new
  new=$(jq --arg id "$todo_id" --arg t "$now" --arg r "$reason" '
    .plan.todos |= map(
      if .id == $id then .status = "skipped" | .completedAt = $t | .skipReason = $r else . end
    )
  ' "$state_path")
  write_state "$state_path" "$new"
}

do_next() {
  local task_id="${1:-}"
  [ -z "$task_id" ] && { echo "usage: next <task-id>" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  # Pick the first pending todo whose deps are all completed|skipped.
  jq -r '
    (.plan.todos // []) as $all
    | $all
    | map(select(.status == "pending"))
    | map(
        . as $t
        | select(
            (.deps // [])
            | all(
                . as $d
                | $all | map(select(.id == $d))[0] // null
                | . != null and (.status == "completed" or .status == "skipped")
              )
          )
      )
    | (.[0] // empty)
    | if . == null or . == "" then "" else (.id // "") + "\t" + (.task // "") end
  ' "$state_path"
}

do_list() {
  local task_id="${1:-}"
  [ -z "$task_id" ] && { echo "usage: list <task-id>" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  local title; title=$(jq -r '.plan.title // "Plan"' "$state_path")
  printf '## %s\n\n' "$title"
  jq -r '
    (.plan.todos // [])
    | map(
        ({
          pending:    "- [ ]",
          in_progress:"- [~]",
          completed:  "- [x]",
          skipped:    "- [/]",
          failed:     "- [!]"
        } as $marks
        | ($marks[.status] // "- [ ]") + " **" + (.id // "") + "** " + (.task // "")
          + (if (.notes // "") != "" then "  _(" + .notes + ")_" else "" end)
          + (if (.failureReason // "") != "" then "  _**failed:** " + .failureReason + "_" else "" end)
          + (if (.skipReason    // "") != "" then "  _**skipped:** "    + .skipReason    + "_" else "" end))
      )
    | .[]
  ' "$state_path"
}

do_status() {
  local task_id="${1:-}"
  [ -z "$task_id" ] && { echo "usage: status <task-id>" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  jq -r '
    (.plan.todos // []) as $t
    | ($t | length)                                                as $total
    | ($t | map(select(.status == "completed")) | length)          as $done
    | ($t | map(select(.status == "in_progress")) | length)        as $inprog
    | ($t | map(select(.status == "failed")) | length)             as $failed
    | ($t | map(select(.status == "skipped")) | length)            as $skipped
    | "\($done)/\($total) done"
      + (if $inprog > 0  then ", \($inprog) in progress" else "" end)
      + (if $failed > 0  then ", \($failed) failed"      else "" end)
      + (if $skipped > 0 then ", \($skipped) skipped"    else "" end)
  ' "$state_path"
}

do_show() {
  local task_id="${1:-}"
  [ -z "$task_id" ] && { echo "usage: show <task-id>" >&2; exit 64; }
  local state_path; state_path=$(resolve_state "$task_id")
  jq '.plan // {}' "$state_path"
}

# --- Dispatch ---------------------------------------------------------------

case "$CMD" in
  init)     do_init     "$@" ;;
  set)      do_set      "$@" ;;
  start)    do_start    "$@" ;;
  complete) do_complete "$@" ;;
  fail)     do_fail     "$@" ;;
  skip)     do_skip     "$@" ;;
  next)     do_next     "$@" ;;
  list)     do_list     "$@" ;;
  status)   do_status   "$@" ;;
  show)     do_show     "$@" ;;
  --help|-h) usage ;;
  *) echo "plan-todos: unknown subcommand '$CMD'" >&2; usage ;;
esac
