#!/usr/bin/env bash
# generate-bundled-changes.sh
#
# Emits:
#   1. human-readable bundled-changes markdown on stdout
#   2. optional machine-readable bundle manifest JSON via --json-out
#
# Source of truth:
# - explicit predecessor release membership comes from release tickets
# - commit scanning contributes only non-release housekeeping work items
#
# Usage:
#   bash scripts/generate-bundled-changes.sh <since-ref> [<version>] [--json-out <path>]

set -euo pipefail

SINCE_REF="${1:-}"
VERSION="current release"
if [ "${2:-}" != "" ] && [[ "${2:-}" != --* ]]; then
  VERSION="$2"
  shift 2
else
  shift $(( $# > 0 ? 1 : 0 ))
fi

JSON_OUT=""
DECLARED_BUNDLE=""
while [ "$#" -gt 0 ]; do
  case "$1" in
    --json-out) JSON_OUT="$2"; shift 2 ;;
    # devaudit-installer#736 — a comma-separated list of REQ-XXX declared
    # co-primary at Phase 1 planning time by sdlc-implementer's bundle
    # eligibility check, as opposed to the predecessor/housekeeping
    # absorption modelled by the rest of this script. Additive: does not
    # touch the ambiguity-guard logic above, since these REQs are declared
    # directly here, not extracted from a ticket's "Absorbed predecessor
    # releases" field.
    --declared-bundle) DECLARED_BUNDLE="$2"; shift 2 ;;
    *) echo "Unknown argument: $1" >&2; exit 2 ;;
  esac
done

if [ -z "$SINCE_REF" ]; then
  echo "Usage: bash scripts/generate-bundled-changes.sh <since-ref> [<version>] [--json-out <path>]" >&2
  exit 1
fi

if ! git rev-parse --verify "$SINCE_REF" >/dev/null 2>&1; then
  echo "Error: ref '$SINCE_REF' not found in git history." >&2
  exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
  echo "Error: jq is required." >&2
  exit 1
fi

HOUSEKEEPING_TYPES='^[0-9a-f]+[[:space:]]+(chore|docs|ci|build|test|revert|style|perf|refactor)(\([^)]+\))?!?:'
DATE_VERSION_RE='^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?$'

find_release_ticket() {
  local version="$1"
  local candidate=""
  for candidate in \
    "compliance/pending-releases/RELEASE-TICKET-${version}.md" \
    "compliance/approved-releases/RELEASE-TICKET-${version}.md" \
    "compliance/superseded-releases/RELEASE-TICKET-${version}.md"; do
    if [ -f "$candidate" ]; then
      printf '%s' "$candidate"
      return 0
    fi
  done
  return 1
}

trim() {
  printf '%s' "$1" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'
}

extract_ticket_title() {
  local file="$1"
  local line=""
  line=$(grep -m1 '^\*\*Requirement:\*\*' "$file" 2>/dev/null || true)
  if [ -n "$line" ]; then
    printf '%s' "$line" \
      | sed -E 's/^\*\*Requirement:\*\*[[:space:]]*//' \
      | sed -E 's/^REQ-[0-9]+[[:space:]]*[—–:-][[:space:]]*//' \
      | sed -E 's/^[[:space:]]+|[[:space:]]+$//g'
    return 0
  fi
  line=$(grep -m1 '^# ' "$file" 2>/dev/null || true)
  if [ -n "$line" ]; then
    printf '%s' "$line" \
      | sed -E 's/^# *//' \
      | sed -E 's/^Release Ticket[[:space:]]*[—:-][[:space:]]*//' \
      | sed -E 's/^REQ-[0-9]+[[:space:]]*[—:-][[:space:]]*//' \
      | sed -E 's/[[:space:]]*$//'
  fi
}

extract_ticket_summary() {
  local file="$1"
  awk '
    /^## Summary/ { found=1; next }
    /^## / { if (found) exit }
    found { print }
  ' "$file" 2>/dev/null \
    | sed -E 's/[[:space:]]+$//' \
    | awk 'NF { p=1 } p { print }' \
    | awk '{ lines[NR]=$0 } END { for (i=NR; i>=1; i--) { if (lines[i] ~ /[^[:space:]]/) { last=i; break } } for (i=1; i<=last; i++) print lines[i] }'
}

extract_ticket_pr() {
  local file="$1" line pr_num pr_url
  line=$(grep -m1 '^\*\*PR:\*\*' "$file" 2>/dev/null || true)
  pr_num=$(printf '%s' "$line" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true)
  pr_url=$(printf '%s' "$line" | grep -oE 'https://[^ )]+' | head -1 || true)
  printf '%s\t%s' "$pr_num" "$pr_url"
}

extract_explicit_predecessors() {
  local file="$1" line=""
  line=$(grep -m1 '^\- \*\*Absorbed predecessor releases:\*\*' "$file" 2>/dev/null || true)
  if [ -z "$line" ]; then
    return 0
  fi
  if printf '%s' "$line" | grep -qi '\bNone\b'; then
    return 0
  fi
  printf '%s' "$line" | grep -oE 'REQ-[0-9]+|v[0-9]{4}\.[0-9]{2}\.[0-9]{2}([.][0-9]+)?' || true
}

derive_commit_range() {
  local version="$1" file="$2"
  local from="" to="" sha=""
  if [[ "$version" =~ ^REQ-[0-9]+$ ]]; then
    while IFS= read -r sha; do
      [ -n "$sha" ] || continue
      [ -n "$from" ] || from="${sha:0:7}"
      to="${sha:0:7}"
    done < <(git log --reverse --format='%H' --grep "\\[${version}\\]\\|Ref: ${version}" 2>/dev/null || true)
  fi
  printf '%s\t%s' "$from" "$to"
}

CURRENT_TICKET=""
EXPLICIT_PREDECESSORS=()
MEMBERS='[]'
PREDECESSOR_LINES=()

# devaudit-installer#622 — a bare-date housekeeping version has no release
# ticket by design (SDLC framework only writes RELEASE-TICKET-*.md for
# REQ-tracked releases), so none of the explicit-predecessor-ownership
# machinery below applies to it: there is no ticket to declare absorbed
# predecessors in, and no ambiguity to resolve against other pending
# tickets. Skip straight to describing the version's own commit range via
# the non-release work-item scan further down — that part of the script
# already works for any version string and needs no release ticket.
if ! [[ "$VERSION" =~ $DATE_VERSION_RE ]]; then
  CURRENT_TICKET="$(find_release_ticket "$VERSION" 2>/dev/null || true)"
  if [ -z "$CURRENT_TICKET" ] && [ -d "compliance/pending-releases" ]; then
    echo "Error: release ticket for ${VERSION} not found; cannot build explicit bundle manifest." >&2
    exit 1
  fi

  mapfile -t CANDIDATE_RELEASES < <(
    if [ -d "compliance/pending-releases" ]; then
      for ticket in compliance/pending-releases/RELEASE-TICKET-*.md; do
        [ -f "$ticket" ] || continue
        version="${ticket##*/RELEASE-TICKET-}"
        version="${version%.md}"
        [ "$version" = "$VERSION" ] && continue
        printf '%s\n' "$version"
      done
    fi
  )

  mapfile -t EXPLICIT_PREDECESSORS < <(
    if [ -n "$CURRENT_TICKET" ]; then
      extract_explicit_predecessors "$CURRENT_TICKET"
    fi
  )

  # extract_explicit_predecessors() can't distinguish "field absent" from
  # "field explicitly None" through its stdout alone — both produce an
  # empty EXPLICIT_PREDECESSORS list. Check presence separately, directly
  # in this shell (not inside the process substitution above, whose
  # variable side effects wouldn't survive the subshell), so the two
  # checks below can tell a deliberate "None" declaration apart from an
  # omitted field (devaudit-installer#746).
  PREDECESSORS_FIELD_PRESENT=false
  if [ -n "$CURRENT_TICKET" ] && grep -q -m1 '^\- \*\*Absorbed predecessor releases:\*\*' "$CURRENT_TICKET" 2>/dev/null; then
    PREDECESSORS_FIELD_PRESENT=true
  fi

  declare -A EXPLICIT_SET=()
  for version in "${EXPLICIT_PREDECESSORS[@]}"; do
    if [ "$version" = "$VERSION" ]; then
      echo "Error: bundle manifest cannot self-supersede ${VERSION}." >&2
      exit 1
    fi
    if [ -n "${EXPLICIT_SET[$version]:-}" ]; then
      echo "Error: duplicate explicit predecessor '${version}'." >&2
      exit 1
    fi
    EXPLICIT_SET["$version"]=1
    if [ -z "$(find_release_ticket "$version" 2>/dev/null || true)" ]; then
      echo "Error: explicit predecessor '${version}' has no release ticket on disk." >&2
      exit 1
    fi
  done

  if [ "${#CANDIDATE_RELEASES[@]}" -gt 0 ] && [ "$PREDECESSORS_FIELD_PRESENT" = false ]; then
    echo "Error: ambiguous predecessor ownership for ${VERSION}. Pending release tickets exist but the release ticket does not explicitly list absorbed predecessor releases." >&2
    printf 'Candidates: %s\n' "${CANDIDATE_RELEASES[*]}" >&2
    exit 1
  fi

  # An explicit, full "None" already answers the accounting question for
  # every candidate release at once — only demand per-release accounting
  # when the ticket named at least one real predecessor (partial bundling),
  # not when it explicitly declared zero (devaudit-installer#746).
  UNACCOUNTED=()
  if [ "${#EXPLICIT_PREDECESSORS[@]}" -gt 0 ]; then
    for version in "${CANDIDATE_RELEASES[@]}"; do
      if [ -z "${EXPLICIT_SET[$version]:-}" ]; then
        UNACCOUNTED+=("$version")
      fi
    done
  fi
  if [ "${#UNACCOUNTED[@]}" -gt 0 ]; then
    echo "Error: ambiguous predecessor ownership for ${VERSION}. The following pending releases are not explicitly listed in the release ticket bundle section:" >&2
    printf '  - %s\n' "${UNACCOUNTED[@]}" >&2
    exit 1
  fi
fi

for version in "${EXPLICIT_PREDECESSORS[@]}"; do
  ticket="$(find_release_ticket "$version")"
  title="$(extract_ticket_title "$ticket")"
  summary="$(extract_ticket_summary "$ticket")"
  pr_meta="$(extract_ticket_pr "$ticket")"
  pr_number="${pr_meta%%$'\t'*}"
  pr_url="${pr_meta#*$'\t'}"
  range_meta="$(derive_commit_range "$version" "$ticket")"
  commit_from="${range_meta%%$'\t'*}"
  commit_to="${range_meta#*$'\t'}"

  if [[ "$version" =~ $DATE_VERSION_RE ]]; then
    role="housekeeping"
    relationship="absorbed"
    reason="Explicit housekeeping release ticket carried forward into approval envelope ${VERSION}."
  else
    role="predecessor"
    relationship="superseded"
    reason="Explicit predecessor release absorbed into approval envelope ${VERSION}."
  fi

  MEMBERS="$(
    jq -c \
      --arg version "$version" \
      --arg role "$role" \
      --arg relationship "$relationship" \
      --arg reason "$reason" \
      --arg scopeSummary "$summary" \
      --arg commitFrom "$commit_from" \
      --arg commitTo "$commit_to" \
      --arg prNumber "$pr_number" \
      --arg prUrl "$pr_url" \
      --arg originalTitle "${title:-Release $version}" \
      '. + [{
        version: $version,
        role: $role,
        relationship: $relationship,
        reason: $reason,
        scopeSummary: (if $scopeSummary == "" then null else $scopeSummary end),
        commitFrom: (if $commitFrom == "" then null else $commitFrom end),
        commitTo: (if $commitTo == "" then null else $commitTo end),
        prNumber: (if $prNumber == "" then null else ($prNumber | tonumber) end),
        prUrl: (if $prUrl == "" then null else $prUrl end),
        originalTitle: $originalTitle,
        evidenceInheritancePolicy: {
          mode: "all_eligible",
          includeCycles: true,
          scopes: ["release", "stage", "cycle"],
          reason: $reason
        }
      }]' <<<"$MEMBERS"
  )"
  PREDECESSOR_LINES+=("- \`${version}\` (${role}/${relationship}) — ${title:-Untitled release ticket}")
done

# devaudit-installer#736 — declared co-tracked bundle members (Phase-1
# planning-time opt-in, not retroactive absorption). Additive to the
# predecessor-absorption members above.
CO_TRACKED_LINES=()
if [ -n "$DECLARED_BUNDLE" ]; then
  declare -A CO_TRACKED_SET=()
  IFS=',' read -ra DECLARED_BUNDLE_ITEMS <<<"$DECLARED_BUNDLE"
  for raw_version in "${DECLARED_BUNDLE_ITEMS[@]}"; do
    version="$(printf '%s' "$raw_version" | xargs)"
    [ -n "$version" ] || continue
    if [ "$version" = "$VERSION" ]; then
      echo "Error: declared bundle cannot include its own core release ${VERSION}." >&2
      exit 1
    fi
    if [ -n "${CO_TRACKED_SET[$version]:-}" ]; then
      echo "Error: duplicate declared bundle member '${version}'." >&2
      exit 1
    fi
    CO_TRACKED_SET["$version"]=1

    ticket="$(find_release_ticket "$version" 2>/dev/null || true)"
    title=""
    summary=""
    if [ -n "$ticket" ]; then
      title="$(extract_ticket_title "$ticket")"
      summary="$(extract_ticket_summary "$ticket")"
    fi
    range_meta="$(derive_commit_range "$version" "${ticket:-}")"
    commit_from="${range_meta%%$'\t'*}"
    commit_to="${range_meta#*$'\t'}"
    reason="Declared co-primary REQ, bundled onto one shared branch/PR/release at Phase 1 planning time."

    MEMBERS="$(
      jq -c \
        --arg version "$version" \
        --arg reason "$reason" \
        --arg scopeSummary "$summary" \
        --arg commitFrom "$commit_from" \
        --arg commitTo "$commit_to" \
        --arg originalTitle "${title:-Bundled REQ $version}" \
        '. + [{
          version: $version,
          role: "co-tracked",
          relationship: "bundled",
          reason: $reason,
          scopeSummary: (if $scopeSummary == "" then null else $scopeSummary end),
          commitFrom: (if $commitFrom == "" then null else $commitFrom end),
          commitTo: (if $commitTo == "" then null else $commitTo end),
          prNumber: null,
          prUrl: null,
          originalTitle: $originalTitle,
          evidenceInheritancePolicy: {
            mode: "none",
            includeCycles: false,
            scopes: [],
            reason: "Co-tracked bundle members keep fully isolated per-REQ evidence — nothing is inherited between them."
          }
        }]' <<<"$MEMBERS"
    )"
    CO_TRACKED_LINES+=("- \`${version}\` (co-tracked/bundled) — ${title:-Bundled REQ $version}")
  done
fi

# devaudit-installer#600 — scan the full SINCE_REF..HEAD window for
# non-release housekeeping; do not re-narrow it to this REQ's own
# implementation window. A predecessor housekeeping release (e.g. a
# `devaudit update` sync) commonly lands on the integration branch shortly
# *before* this REQ's own first commit — narrowing the scan floor to that
# first commit's parent excluded exactly that predecessor from ever being
# detected (confirmed live: a housekeeping commit one commit outside the
# window was silently missed). The per-commit REQ-tag filter below already
# excludes any commit explicitly owned by a REQ (this one or another), which
# is what actually protects against recasting REQ-owned work as generic
# bundle work — no additional window-narrowing is needed on top of it.
SCAN_FROM="$SINCE_REF"
COMMITS="$(git log "$SCAN_FROM"..HEAD --format='%h%x09%s' 2>/dev/null || true)"
BUNDLED=""
while IFS=$'\t' read -r sha subject; do
  [ -n "$sha" ] || continue
  if ! printf '%s\t%s\n' "$sha" "$subject" | grep -Eq "$HOUSEKEEPING_TYPES"; then
    continue
  fi
  if git log -1 --format='%s%n%b' "$sha" | grep -Eq '\[REQ-[0-9]+\]|Ref:[[:space:]]*REQ-[0-9]+'; then
    continue
  fi
  BUNDLED+="${sha}"$'\t'"${subject}"$'\n'
done <<<"$COMMITS"
BUNDLED="${BUNDLED%$'\n'}"
NON_RELEASE_ITEMS='[]'
NON_RELEASE_LINES=()
if [ -n "$BUNDLED" ]; then
  while IFS=$'\t' read -r sha subject; do
    [ -n "$sha" ] || continue
    NON_RELEASE_ITEMS="$(
      jq -c \
        --arg kind "housekeeping_commit" \
        --arg title "$subject" \
        --arg reference "$sha" \
        '. + [{
          kind: $kind,
          title: $title,
          reference: $reference
        }]' <<<"$NON_RELEASE_ITEMS"
    )"
    NON_RELEASE_LINES+=("- \`${sha}\` ${subject}")
  done <<<"$BUNDLED"
fi

GENERATOR_VERSION="$(jq -r '.version' cli/package.json 2>/dev/null || echo '0.0.0')"
REPOSITORY_SLUG="$(git config --get remote.origin.url 2>/dev/null | sed -E 's#(git@github.com:|https://github.com/)##; s#\.git$##' || true)"
GENERATED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
[ -n "$REPOSITORY_SLUG" ] || REPOSITORY_SLUG="${GITHUB_REPOSITORY:-local/unknown}"

MANIFEST_BASE="$(
  jq -c -n \
    --arg version "$VERSION" \
    --arg members "$MEMBERS" \
    --arg nonReleaseWorkItems "$NON_RELEASE_ITEMS" \
    --arg generatorVersion "$GENERATOR_VERSION" \
    --arg repository "$REPOSITORY_SLUG" \
    --arg generatedAt "$GENERATED_AT" \
    '{
      schemaVersion: 2,
      approvalRelease: { version: $version },
      coreRelease: { version: $version },
      members: ($members | fromjson),
      nonReleaseWorkItems: ($nonReleaseWorkItems | fromjson),
      generator: {
        name: "devaudit-installer",
        version: $generatorVersion,
        repository: (if $repository == "" then null else $repository end),
        generatedAt: $generatedAt
      }
    }'
)"

HASH_INPUT="$(jq -cS 'del(.generator.generatedAt)' <<<"$MANIFEST_BASE")"
MANIFEST_HASH="sha256:$(printf '%s' "$HASH_INPUT" | sha256sum | awk '{print $1}')"
MANIFEST_JSON="$(jq --arg manifestHash "$MANIFEST_HASH" '. + { manifestHash: $manifestHash }' <<<"$MANIFEST_BASE")"

if [ -n "$JSON_OUT" ]; then
  mkdir -p "$(dirname "$JSON_OUT")"
  jq -S . <<<"$MANIFEST_JSON" > "$JSON_OUT"
fi

echo "## Bundled Changes"
echo ""
echo "- **Core tracked release:** \`${VERSION}\`"
echo "- **Bundle manifest:** \`BUNDLED-CHANGES-${VERSION}.json\`"
echo "- **Manifest hash:** \`${MANIFEST_HASH}\`"
echo "- **Absorbed predecessor releases:** $(if [ "${#PREDECESSOR_LINES[@]}" -gt 0 ]; then printf '%s' "${EXPLICIT_PREDECESSORS[*]}"; else printf 'None'; fi)"
if [ "${#CO_TRACKED_LINES[@]}" -gt 0 ]; then
  echo "- **Co-tracked bundle members:** $(printf '%s' "${DECLARED_BUNDLE_ITEMS[*]}")"
fi
if [ "${#NON_RELEASE_LINES[@]}" -gt 0 ]; then
  echo "- **Absorbed non-release work:** housekeeping commits since \`${SINCE_REF}\`"
else
  echo "- **Absorbed non-release work:** None"
fi
echo "- **Why bundled here:** Explicit predecessor release tickets and non-release work are consolidated under approval envelope \`${VERSION}\`."
echo "- **Evidence impact:** Evidence ownership remains on the source releases; the bundle manifest provides lineage and inherited visibility only."
echo "- **Reviewer impact:** Approval scope includes the core tracked release plus the explicit predecessor releases and non-release work listed below."
echo "- **Security / risk impact:** No additional security/risk impact identified automatically; reviewer must confirm in the canonical release artifacts."
echo "- **Reference:** commit range \`${SCAN_FROM}..HEAD\`"
echo ""

echo "### Explicit Constituent Releases"
echo ""
if [ "${#PREDECESSOR_LINES[@]}" -gt 0 ]; then
  printf '%s\n' "${PREDECESSOR_LINES[@]}"
else
  echo "- None"
fi
echo ""

echo "### Absorbed Non-Release Work"
echo ""
if [ "${#NON_RELEASE_LINES[@]}" -gt 0 ]; then
  printf '%s\n' "${NON_RELEASE_LINES[@]}"
else
  echo "- None"
fi
echo ""

if [ "${#CO_TRACKED_LINES[@]}" -gt 0 ]; then
  echo "### Co-Tracked Bundle Members"
  echo ""
  echo "Declared co-primary at Phase 1 planning time (devaudit-installer#736) — not absorbed predecessors, each keeps fully isolated per-REQ evidence:"
  echo ""
  printf '%s\n' "${CO_TRACKED_LINES[@]}"
  echo ""
fi
