#!/usr/bin/env bash
#
# analysis-jira-write.sh  -  create the tree that analysis-story-tree.mjs planned.
#
# WHY THIS IS A SEPARATE FILE
#
# Planning is deterministic, testable offline, and safe to run a hundred times.
# Creating issues is none of those. Keeping them apart means the file that can
# write to a tracker is small enough to read in one sitting, and the file that
# decides WHAT to write can be tested without a network.
#
# `jira-publish.sh` cannot do this: it writes a comment or a description on an
# issue that already exists. The only creation path in this repo was a curl
# hand-written inside a markdown instruction, which is the thing this replaces.
#
# THE WRITE IS LEDGERED, AND THE LEDGER IS THE POINT
#
# Before each POST an `intent` line is appended to the run ledger; after the
# response, the key. Crash in between and the ledger holds an intent with no key.
# The next run sees that and does a label search BEFORE sending anything, so a
# half-written tree cannot twin itself. Without the ledger, the failure mode is
# not "the run stopped" - it is "the run stopped and the retry made a second
# tree", which is the expensive one.
#
# IDENTITY IS SERVER-SIDE
#
# Each node carries a label from the plan. Finding the tree back is a JQL search
# on that label, not a lookup in a local file: a local index does not survive a
# new machine, a deleted ~/.claude, or a SECOND ANALYST - and the second analyst
# is exactly the person who would otherwise open a duplicate tree.
#
# AN EXISTING NODE IS SKIPPED, NEVER UPDATED
#
# Jira has no backup path for fields other than description. Rewriting a body an
# engineer has since edited would repeat, at tree scale, the defect that made
# jira-publish.sh take backups in the first place.
#
# Usage:
#   analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE]
#
# Exit: 0 written (or previewed), 3 the plan does not parse, 4 auth, 5 a Jira
#       call failed, 64 usage.

set -uo pipefail

SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
. "$SELF_DIR/_jira-auth.sh"

PLAN=""; PROJECT=""; DRY=0; LEDGER=""
while [ $# -gt 0 ]; do
  case "$1" in
    --plan) PLAN="${2:-}"; shift 2 ;;
    --project) PROJECT="${2:-}"; shift 2 ;;
    --ledger) LEDGER="${2:-}"; shift 2 ;;
    --dry-run) DRY=1; shift ;;
    *) echo "usage: analysis-jira-write.sh --plan FILE --project KEY [--dry-run] [--ledger FILE]" >&2; exit 64 ;;
  esac
done
[ -n "$PLAN" ] && [ -f "$PLAN" ] || { echo "ERR: --plan FILE is required and must exist" >&2; exit 64; }
[ -n "$PROJECT" ] || { echo "ERR: --project KEY is required" >&2; exit 64; }

command -v jq >/dev/null 2>&1 || { echo "ERR: jq is required" >&2; exit 3; }
jq -e . "$PLAN" >/dev/null 2>&1 || { echo "ERR: the plan does not parse as JSON: $PLAN" >&2; exit 3; }

DOC_ID="$(jq -r '.document.id // "unidentified"' "$PLAN")"
VERDICT="$(jq -r '.coverage.verdict // "unverifiable"' "$PLAN")"
if [ -z "$LEDGER" ]; then
  LEDGER="$HOME/.claude/logs/multi-agent/_analysis-jira/${DOC_ID}.jsonl"
fi
mkdir -p "$(dirname "$LEDGER")" 2>/dev/null || true

# A run that could not be verified is allowed to write; it is not allowed to look
# verified afterwards. The ledger records the verdict beside every key, so the
# question "was this tree checked?" is answerable later from the tree's own trail.
ledger() {  # ledger <event> <label> [key]
  [ "$DRY" -eq 1 ] && return 0
  printf '{"ts":"%s","event":"%s","label":"%s","key":%s,"coverage":"%s"}\n' \
    "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" "$2" \
    "$([ -n "${3:-}" ] && printf '"%s"' "$3" || printf 'null')" \
    "$VERDICT" >> "$LEDGER"
}

if [ "$DRY" -eq 0 ]; then
  jira_auth_resolve || exit 4
fi

# An unfinished write from a previous run: an intent whose key never arrived.
# Its existence is what forces the search below to run before anything is sent.
ORPHANS=0
if [ -f "$LEDGER" ]; then
  ORPHANS=$(jq -rs '[.[] | select(.event=="intent")] as $i
                  | [.[] | select(.event=="created") | .label] as $c
                  | [$i[] | select(.label as $l | ($c | index($l)) | not)] | length' \
             "$LEDGER" 2>/dev/null || echo 0)
  if [ "${ORPHANS:-0}" -gt 0 ]; then
    echo "NOTE: $ORPHANS write(s) from an earlier run have no recorded key." >&2
    echo "      Searching Jira by label before sending anything." >&2
  fi
fi

# What already exists, by label. One search, whatever the tree's size.
declare -a EXISTING=()
existing_for() {  # existing_for <label> -> prints the key, or empty
  local label="$1" i
  for i in "${EXISTING[@]:-}"; do
    case "$i" in "$label="*) printf '%s' "${i#*=}"; return 0 ;; esac
  done
  printf ''
}

LABELS="$(jq -r '[.nodes[] | .label, (.subtasks[]?.label)] | join(",")' "$PLAN")"
if [ "$DRY" -eq 0 ] && [ -n "$LABELS" ]; then
  JQL="project=${PROJECT} AND labels in (${LABELS})"
  ENC="$(printf '%s' "$JQL" | jq -sRr @uri)"
  SEARCH="$(jira_api GET "/rest/api/2/search?jql=${ENC}&fields=labels&maxResults=200" || echo "")"
  if [ -n "$SEARCH" ]; then
    while IFS= read -r pair; do
      [ -n "$pair" ] && EXISTING+=("$pair")
    done < <(printf '%s' "$SEARCH" | jq -r '.issues[]? | .key as $k | .fields.labels[]? | "\(.)=\($k)"' 2>/dev/null || true)
  fi
fi

# Two channels on purpose. The human line goes to stdout; the created key comes
# back in CREATED_KEY. Returning the key on stdout too meant a caller using
# command substitution swallowed the report - the first dry run printed a header
# and nothing else, and the tree looked empty.
CREATED_KEY=""
create_issue() {  # create_issue <label> <summary> <issuetype> <parentKey|""> <description>
  local label="$1" summary="$2" itype="$3" parent="$4" desc="$5" body key existing
  CREATED_KEY=""
  existing="$(existing_for "$label")"
  if [ -n "$existing" ]; then
    # Skipped, never updated: Jira has no backup path for fields other than
    # description, and an engineer may have edited this body since.
    echo "  exists  $existing  $summary"
    CREATED_KEY="$existing"
    return 0
  fi
  body="$(jq -n --arg p "$PROJECT" --arg s "$summary" --arg t "$itype" \
                --arg l "$label" --arg d "$desc" --arg par "$parent" '
    {fields: ({project: {key: $p}, summary: $s, issuetype: {name: $t},
               labels: [$l], description: $d}
              + (if $par == "" then {} else {parent: {key: $par}} end))}')"
  if [ "$DRY" -eq 1 ]; then
    echo "  create  $itype  $summary   [$label]"
    return 0
  fi
  ledger intent "$label"
  local resp
  resp="$(printf '%s' "$body" | jira_api POST "/rest/api/2/issue" --data @- || echo "")"
  key="$(printf '%s' "$resp" | jq -r '.key // empty' 2>/dev/null || echo "")"
  if [ -z "$key" ]; then
    echo "ERR: create failed for '$summary'" >&2
    printf '%s\n' "$resp" | head -3 >&2
    return 5
  fi
  ledger created "$label" "$key"
  echo "  created $key  $summary"
  CREATED_KEY="$key"
}

echo "analysis-jira-write: ${PROJECT}, coverage ${VERDICT}$([ "$DRY" -eq 1 ] && echo "  (dry run)")"
[ "$VERDICT" = "unverifiable" ] && \
  echo "  NOTE: coverage could not be checked for this document; the tree is unverified."

RC=0
NODES="$(jq -c '.nodes[]' "$PLAN")"
while IFS= read -r node; do
  [ -n "$node" ] || continue
  s_label="$(printf '%s' "$node" | jq -r '.label')"
  s_title="$(printf '%s' "$node" | jq -r '.title')"
  s_src="$(printf '%s' "$node" | jq -r '.sourceIds | join(", ")')"
  s_type="$(jq -r '.prefsUsed.storyIssueType // "Story"' "$PLAN")"
  s_desc="Sources: ${s_src:-none (derived from the section heading)}"
  if ! create_issue "$s_label" "$s_title" "$s_type" "" "$s_desc"; then
    # A failed story does not get its sub-tasks written anyway. `create_issue`
    # omits the parent field when the parent key is empty, so without this the
    # sub-tasks of a story Jira refused were still POSTed - and landed as live,
    # parentless issues nobody asked for, from a run that had already reported
    # an error.
    RC=5
    echo "  skipped   this story's sub-tasks: the story itself was not created" >&2
    continue
  fi
  skey="$CREATED_KEY"
  while IFS= read -r sub; do
    [ -n "$sub" ] || continue
    t_label="$(printf '%s' "$sub" | jq -r '.label')"
    t_role="$(printf '%s' "$sub" | jq -r '.role')"
    t_type="$(jq -r '.prefsUsed.subtaskIssueType // empty' "$PLAN")"
    if [ -z "$t_type" ] && [ "$DRY" -eq 0 ]; then
      # Discover rather than assume: whichever type this site marks subtask:true,
      # under whatever name the site gave it.
      t_type="$(jira_api GET "/rest/api/2/issue/createmeta?projectKeys=${PROJECT}&expand=projects.issuetypes" \
        | jq -r '[.projects[]?.issuetypes[]? | select(.subtask==true) | .name] | first // empty' 2>/dev/null || echo "")"
    fi
    [ -n "$t_type" ] || t_type="Sub-task"
    create_issue "$t_label" "${s_title} - ${t_role}" "$t_type" "$skey" "$s_desc" || RC=5
  done < <(printf '%s' "$node" | jq -c '.subtasks[]?')
done <<< "$NODES"

exit "$RC"
