#!/usr/bin/env bash
# Task 1515 — UserPromptSubmit hook: inject a fresh <datetime> block as
# additionalContext on every turn, so the agent always has the current instant
# (date, time, timezone, weekday, and the raw UTC ISO) without hand-computing it
# and without depending on Claude Code's spawn-time "Today's date" line going
# stale. This is the per-turn current-time surface the datetime skill points at.
#
# The instant is computed fresh here every turn (refresh=per-turn). The timezone
# is NOT computed here — it is the operator's own zone, resolved from their
# UserProfile on the graph once per admin spawn and stamped into MAXY_ACCOUNT_TZ.
# When MAXY_ACCOUNT_TZ is empty (a non-admin turn, an unset profile timezone, or
# a neo4j hiccup at spawn) the block falls back to the server's own zone and
# labels the source, so a reader can always tell which clock the agent saw.
#
# Mirrors prompt-optimiser-directive.sh: emit hookSpecificOutput.additionalContext
# via python3, fail-open with exit 0 on every path. UserPromptSubmit output is
# ADDED to the prompt, never substituted — the raw prompt always still reaches
# the model.
#
# Input  (stdin, UserPromptSubmit contract): { "session_id": "...", ... } —
#         session_id is used only for the breadcrumb; the prompt text is ignored
#         (the block is standing and prompt-independent).
# Output (stdout JSON):
#   { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit",
#                             "additionalContext": "<datetime>...</datetime>" } }

set -uo pipefail

# Drain stdin FIRST, before any exit path. Claude Code writes the full
# UserPromptSubmit payload (which carries the prompt text) to our stdin; if we
# exit before reading it, a payload larger than the pipe buffer leaves the parent
# blocked on write and then hit with EPIPE/SIGPIPE. The sibling
# prompt-optimiser-directive.sh drains first for this exact reason. Only
# session_id is used here (for the breadcrumb); the block is prompt-independent.
HOOK_INPUT=$(cat 2>/dev/null || true)

# Fail-open if either encoder is unavailable: python3 wraps the JSON envelope,
# node formats the instant in the target zone (Intl has the full IANA database).
command -v python3 >/dev/null 2>&1 || exit 0
command -v node >/dev/null 2>&1 || exit 0

# The operator's zone, stamped at spawn from the graph. Empty -> server zone.
ACCOUNT_TZ="${MAXY_ACCOUNT_TZ:-}"

# Format the current instant. node prints two tab-separated fields:
#   1. the resolved IANA zone actually used (so the server-fallback names a real
#      zone, not the string "server")
#   2. the assembled block body
# TZ_IN is passed as argv[1]; empty means "use the runtime's own zone".
BLOCK=$(TZ_IN="$ACCOUNT_TZ" node -e '
const tzIn = process.env.TZ_IN && process.env.TZ_IN.trim().length ? process.env.TZ_IN.trim() : null;
const now = new Date();
// Resolve the zone actually used. A supplied zone that Intl rejects falls back
// to the runtime zone, so a bad stamp can never break the turn.
let zone = tzIn;
try { if (zone) new Intl.DateTimeFormat("en-GB", { timeZone: zone }); }
catch { zone = null; }
if (!zone) zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// zone === tzIn only when a non-empty, valid graph timezone was stamped; every
// other case (env empty on a non-admin turn, or a stamped value Intl rejected)
// falls back to the runtime zone. Label that neutrally as "unavailable" rather
// than asserting "unset" — the [datetime-tz] spawn log distinguishes unset from
// invalid from unavailable; here all the hook knows is the zone is not the graph one.
const source = tzIn && zone === tzIn ? "account-timezone (graph)" : "server-timezone (account timezone unavailable)";
const local = new Intl.DateTimeFormat("en-GB", {
  weekday: "long", day: "numeric", month: "long", year: "numeric",
  hour: "2-digit", minute: "2-digit", timeZone: zone, timeZoneName: "longOffset",
}).format(now);
const body = [
  "Current date and time, read this rather than computing it by hand:",
  "Now: " + local + " (" + zone + ")",
  "UTC: " + now.toISOString(),
  "Source: " + source,
].join("\n");
process.stdout.write(zone + "\t" + body);
' 2>/dev/null) || exit 0

[ -n "$BLOCK" ] || exit 0
ZONE_USED="${BLOCK%%$'\t'*}"
BODY="${BLOCK#*$'\t'}"
CONTEXT="<datetime>
$BODY
</datetime>"

OUT=$(printf '%s' "$CONTEXT" | python3 -c '
import json, sys
ctx = sys.stdin.read()
print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": ctx,
    }
}))
' 2>/dev/null) || exit 0

[ -n "$OUT" ] || exit 0
printf '%s\n' "$OUT"

# Breadcrumb: source (graph vs server-fallback) + refresh=per-turn so a future
# staleness report can tell which clock was live each turn without reproduction.
# Fail-open: a missing/unwritable LOG_DIR just skips the write.
SID=$(printf '%s' "$HOOK_INPUT" | python3 -c 'import json,sys
try:
    print(json.load(sys.stdin).get("session_id","") or "?")
except Exception:
    print("?")' 2>/dev/null)
SID=${SID:-?}
if [ -n "$ACCOUNT_TZ" ] && [ "$ZONE_USED" = "$ACCOUNT_TZ" ]; then SRC=graph; else SRC=server-fallback; fi
NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "?")
LINE="$NOW_ISO [datetime-inject] injected tz=$ZONE_USED source=$SRC refresh=per-turn session=$SID"
if [ -n "${LOG_DIR:-}" ] && [ -d "$LOG_DIR" ]; then
  printf '%s\n' "$LINE" >> "$LOG_DIR/datetime-inject.log" 2>/dev/null || true
fi
echo "$LINE" >&2 || true
exit 0
