#!/usr/bin/env bash
#
# plan-todos.sh  -  manage the Phase 1 plan as a live Todo list.
#
# The Phase 1 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 1 (Plan) emits the
# initial plan; Phase 2 (Dev) iterates step-by-step; Phase 3 (Review)
# inspects completion; Phase 5 (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: "2/5 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

# jq is not optional on this path. Without the guard below a missing binary
# renders as EMPTY DATA and the work continues on it; see lib/require-jq.sh.
for _rq in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/require-jq.sh" \
           "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/require-jq.sh" \
           "$HOME/.claude/lib/require-jq.sh" \
           "$HOME/.copilot/lib/require-jq.sh" \
           "$HOME/.codex/lib/require-jq.sh"; do
  [ -f "$_rq" ] || continue
  # shellcheck source=/dev/null
  . "$_rq" && break
done
unset _rq
if ! command -v ma_require_jq >/dev/null 2>&1; then
  # The helper itself is missing, which is an install problem, not a jq one.
  ma_require_jq() { command -v jq >/dev/null 2>&1 || { echo "jq not found - cannot ${1:-continue}." >&2; return 1; }; }
fi
ma_require_jq "read or write the plan" || exit 3

# Run-state path resolution: pipeline/lib/run-paths.sh owns the two layouts
# (nested <root>/<project>/<id>/ and flat <root>/<id>/) and every id spelling.
_MA_RP_HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
if [ -f "$_MA_RP_HERE/run-paths.sh" ]; then . "$_MA_RP_HERE/run-paths.sh"; else . "$HOME/.claude/lib/run-paths.sh"; fi


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.
  # This globbed ONLY the nested layout; flat-layout runs resolved to nothing.
  found="$(ma_resolve_run_file "$task_id" agent-state.json 2>/dev/null || true)"
  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
  # A planning-output document is accepted directly and converted here.
  #
  # The conversion used to live as a jq blob inside phase-1-plan.md, which
  # made the mapping from `tasks[]` to `todos[]` a thing two files defined - and
  # the phase doc was the copy nothing tested. Accepting both shapes costs four
  # lines and removes the second definition.
  if jq -e '.tasks and (.todos | not)' <<<"$plan_blob" >/dev/null 2>&1; then
    plan_blob=$(jq '{
      title: (.summary // .title // "plan"),
      todos: [ .tasks[] | {
        id: .id,
        task: (.title // .subject // ""),
        status: "pending",
        deps: (.dependsOn // .blockedBy // [])
      } ]
    }' <<<"$plan_blob")
  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"
}

require_todo() {
  # $1 state path, $2 todo_id  -  exits 1 when the id is not in the plan.
  if ! jq -e --arg id "$2" '(.plan.todos // []) | map(select(.id == $id)) | length > 0' "$1" >/dev/null 2>&1; then
    echo "plan-todos: todo '$2' not found" >&2
    exit 1
  fi
}

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")
  require_todo "$state_path" "$todo_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")
  require_todo "$state_path" "$todo_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")
  require_todo "$state_path" "$todo_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
