#!/usr/bin/env bash
# Notification/projection adapter for dispatch runs.
#
# Truth lives in status.json. Linear is only a retried projection, and this
# script never makes state-machine decisions.
set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STATUS_TOOL="$SCRIPT_DIR/dispatch_status.py"
NTFY_TOPIC_FILE="${NTFY_TOPIC_FILE:-$HOME/.recoil/ntfy_topic}"
LINEAR_TOKEN_FILE="${LINEAR_TOKEN_FILE:-$HOME/.recoil/linear_token}"
LINEAR_API_URL="${LINEAR_API_URL:-https://api.linear.app/graphql}"

usage() {
  cat >&2 <<'USAGE'
usage:
  dispatch_notify.sh notify --run-dir D --event EVENT --message MSG
  dispatch_notify.sh project-dirty --run-dir D
USAGE
}

append_event() {
  local run_dir="$1"
  local event="$2"
  shift 2
  mkdir -p "$run_dir"
  python3 - "$run_dir/events.jsonl" "$event" "$@" <<'PY'
import datetime
import json
import sys

path = sys.argv[1]
payload = {
    "at": datetime.datetime.now(datetime.timezone.utc)
    .replace(microsecond=0)
    .isoformat()
    .replace("+00:00", "Z"),
    "event": sys.argv[2],
}
for item in sys.argv[3:]:
    key, _, value = item.partition("=")
    if key:
        payload[key] = value
with open(path, "a", encoding="utf-8") as handle:
    handle.write(json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n")
PY
}

status_value() {
  local run_dir="$1"
  local key="$2"
  python3 - "$run_dir/status.json" "$key" <<'PY'
import json
import sys

with open(sys.argv[1], "r", encoding="utf-8") as handle:
    data = json.load(handle)
value = data.get(sys.argv[2])
if value is True:
    print("true")
elif value is False:
    print("false")
elif value is None:
    print("")
else:
    print(value)
PY
}

clean_message() {
  python3 - "$1" <<'PY'
import sys

text = " ".join(sys.argv[1].split())
print(text[:199])
PY
}

state_label() {
  case "$1" in
    STARTED|ATTEMPT_RUNNING) printf '%s\n' "dispatch-running" ;;
    RETRY_PENDING|ZOMBIE_SUSPECT|ZOMBIE_REAPED) printf '%s\n' "dispatch-healing" ;;
    CONVERGED_PR_CREATED) printf '%s\n' "dispatch-converged" ;;
    CAPPED_NEEDS_HUMAN) printf '%s\n' "dispatch-needs-human" ;;
    *) return 1 ;;
  esac
}

curl_http() {
  local body_file="$1"
  shift
  local code
  code="$(curl -sS -m 8 -o "$body_file" -w "%{http_code}" "$@" 2>/dev/null)"
  if [ "$?" -ne 0 ] || [ -z "$code" ]; then
    code="000"
  fi
  printf '%s\n' "$code"
}

http_is_2xx() {
  case "$1" in
    2*) return 0 ;;
    *) return 1 ;;
  esac
}

graphql_ok() {
  local code="$1"
  local body_file="$2"
  http_is_2xx "$code" || return 1
  python3 - "$body_file" <<'PY'
import json
import sys

try:
    with open(sys.argv[1], "r", encoding="utf-8") as handle:
        payload = json.load(handle)
except Exception:
    raise SystemExit(1)
if payload.get("errors"):
    raise SystemExit(1)
PY
}

graphql_payload() {
  local kind="$1"
  shift
  python3 - "$kind" "$@" <<'PY'
import json
import sys

kind = sys.argv[1]
if kind == "query":
    issue = sys.argv[2]
    query = """
query($issueId: String!) {
  issue(id: $issueId) {
    id
    labels {
      nodes { id name }
    }
  }
  issueLabels(first: 200) {
    nodes { id name }
  }
}
"""
    print(json.dumps({"query": query, "variables": {"issueId": issue}}))
elif kind == "comment":
    issue_id = sys.argv[2]
    body = sys.argv[3]
    query = """
mutation($issueId: String!, $body: String!) {
  commentCreate(input: { issueId: $issueId, body: $body }) {
    success
  }
}
"""
    print(json.dumps({"query": query, "variables": {"issueId": issue_id, "body": body}}))
elif kind == "update":
    issue_id = sys.argv[2]
    label_ids = json.loads(sys.argv[3])
    query = """
mutation($issueId: String!, $labelIds: [String!]) {
  issueUpdate(id: $issueId, input: { labelIds: $labelIds }) {
    success
  }
}
"""
    print(json.dumps({"query": query, "variables": {"issueId": issue_id, "labelIds": label_ids}}))
else:
    raise SystemExit(2)
PY
}

extract_label_plan() {
  local body_file="$1"
  local target_label="$2"
  python3 - "$body_file" "$target_label" <<'PY'
import json
import sys

with open(sys.argv[1], "r", encoding="utf-8") as handle:
    payload = json.load(handle)
data = payload.get("data") or {}
issue = data.get("issue") or {}
issue_id = issue.get("id")
if not issue_id:
    raise SystemExit(1)

current = ((issue.get("labels") or {}).get("nodes") or [])
available = ((data.get("issueLabels") or {}).get("nodes") or []) + current
target_name = sys.argv[2]
target_id = None
for label in available:
    if label.get("name") == target_name:
        target_id = label.get("id")
        break
if not target_id:
    raise SystemExit(1)

label_ids = []
for label in current:
    name = label.get("name") or ""
    label_id = label.get("id")
    if label_id and not name.startswith("dispatch-") and label_id not in label_ids:
        label_ids.append(label_id)
if target_id not in label_ids:
    label_ids.append(target_id)

print(issue_id)
print(json.dumps(label_ids, separators=(",", ":")))
PY
}

post_graphql() {
  local token="$1"
  local payload="$2"
  local body_file="$3"
  curl_http "$body_file" \
    -H "Authorization: $token" \
    -H "Content-Type: application/json" \
    --data-binary "$payload" \
    "$LINEAR_API_URL"
}

send_ntfy() {
  # DEPRECATED (REC-229): the ntfy push channel is retired — superseded by the
  # pull pane (/dashboard). This is now a NO-OP shim: it makes NO HTTP call.
  # The function signature and the notify_ntfy ledger event are KEPT so the
  # events ledger and any notify_ntfy consumer are unchanged, and cmd_notify's
  # control flow (send_ntfy then project_linear) is byte-unchanged.
  local run_dir="$1"
  local event="$2"
  # $3 message and $4 priority are accepted for signature compatibility (unused).
  append_event "$run_dir" "notify_ntfy" "dispatch_event=$event" "status=deprecated"
  return 0
}

project_linear() {
  local run_dir="$1"
  local message="${2:-}"
  local state issue target_label token query_payload query_body query_code plan
  local issue_id label_ids comment_payload comment_body comment_code update_payload update_body update_code

  state="$(status_value "$run_dir" "state")"
  issue="$(status_value "$run_dir" "issue")"
  if ! target_label="$(state_label "$state")"; then
    append_event "$run_dir" "linear_projection" "state=$state" "status=no projection target"
    return 0
  fi

  if [ ! -r "$LINEAR_TOKEN_FILE" ] || [ -z "$(tr -d '[:space:]' < "$LINEAR_TOKEN_FILE" 2>/dev/null)" ]; then
    append_event "$run_dir" "linear_projection" "state=$state" "label=$target_label" "status=channel unconfigured"
    return 0
  fi

  token="$(tr -d '[:space:]' < "$LINEAR_TOKEN_FILE")"
  query_payload="$(graphql_payload query "$issue")"
  query_body="$(mktemp)"
  query_code="$(post_graphql "$token" "$query_payload" "$query_body")"
  if ! graphql_ok "$query_code" "$query_body"; then
    append_event "$run_dir" "linear_projection" "state=$state" "label=$target_label" "status=failed" "http_code=$query_code"
    rm -f "$query_body"
    return 0
  fi

  if ! plan="$(extract_label_plan "$query_body" "$target_label")"; then
    append_event "$run_dir" "linear_projection" "state=$state" "label=$target_label" "status=failed" "http_code=$query_code"
    rm -f "$query_body"
    return 0
  fi
  rm -f "$query_body"
  issue_id="$(printf '%s\n' "$plan" | sed -n '1p')"
  label_ids="$(printf '%s\n' "$plan" | sed -n '2p')"

  comment_payload="$(graphql_payload comment "$issue_id" "$message")"
  comment_body="$(mktemp)"
  comment_code="$(post_graphql "$token" "$comment_payload" "$comment_body")"
  if ! graphql_ok "$comment_code" "$comment_body"; then
    append_event "$run_dir" "linear_projection" "state=$state" "label=$target_label" "status=failed" "comment_http_code=$comment_code"
    rm -f "$comment_body"
    return 0
  fi
  rm -f "$comment_body"

  update_payload="$(graphql_payload update "$issue_id" "$label_ids")"
  update_body="$(mktemp)"
  update_code="$(post_graphql "$token" "$update_payload" "$update_body")"
  if graphql_ok "$update_code" "$update_body"; then
    python3 "$STATUS_TOOL" projection --run-dir "$run_dir" --clean --expected-state "$state" >/dev/null 2>&1 || true
    append_event "$run_dir" "linear_projection" "state=$state" "label=$target_label" "status=ok" "http_code=$update_code"
  else
    append_event "$run_dir" "linear_projection" "state=$state" "label=$target_label" "status=failed" "update_http_code=$update_code"
  fi
  rm -f "$update_body"
  return 0
}

cmd_notify() {
  local run_dir="" event="" message="" priority="" ntfy_only=0
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --run-dir) run_dir="${2:-}"; shift 2 ;;
      --event) event="${2:-}"; shift 2 ;;
      --message) message="${2:-}"; shift 2 ;;
      --priority) priority="${2:-}"; shift 2 ;;
      --ntfy-only) ntfy_only=1; shift ;;   # skip Linear projection (the named ntfy push is DEPRECATED/no-op; this flag now only skips the Linear projection)
      *) usage; return 2 ;;
    esac
  done
  if [ -z "$run_dir" ] || [ -z "$event" ] || [ -z "$message" ]; then
    usage
    return 2
  fi
  message="$(clean_message "$message")"
  send_ntfy "$run_dir" "$event" "$message" "$priority"
  [ "$ntfy_only" = "1" ] || project_linear "$run_dir" "$message"
  return 0
}

cmd_project_dirty() {
  local run_dir="" dirty
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --run-dir) run_dir="${2:-}"; shift 2 ;;
      *) usage; return 2 ;;
    esac
  done
  if [ -z "$run_dir" ]; then
    usage
    return 2
  fi
  dirty="$(status_value "$run_dir" "linear_projection_dirty")"
  if [ "$dirty" = "true" ]; then
    project_linear "$run_dir" "Projection retry: $(status_value "$run_dir" "state")"
  fi
  return 0
}

main() {
  local command="${1:-}"
  if [ -z "$command" ]; then
    usage
    return 2
  fi
  shift
  case "$command" in
    notify) cmd_notify "$@" ;;
    project-dirty) cmd_project_dirty "$@" ;;
    *) usage; return 2 ;;
  esac
}

main "$@"
