#!/usr/bin/env bash
# reflection's cron-fired signal check.
#
# Designed to be wired as a per-minute cron entry. Most invocations exit cheap
# (no API spend). A wake only fires when there's actual signal — currently:
# dialogue.md or wake.md updated since last wake completed.
#
# Lock semantics: holds an flock-based exclusive lock on agent/runtime/wake.lock
# and inherits it across the exec into reflect-wake, so a long-running wake
# can't be doubled by another minute's cron firing. Kernel releases the lock
# automatically on process death — no stale-lock concern.
#
# Suggested crontab:
#   * * * * * cd /home/prmichaelsen/.acp/projects/reflection && bin/reflect-tick >> /tmp/reflect-tick.log 2>&1
#
# Off-limits to the agent — modifying this defeats the autonomous loop's bound.
set -euo pipefail

# Cron's PATH is minimal; ensure tools the runtime needs are findable when this
# script and its child reflect-wake run under cron.
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"

PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_ROOT"

LOCKFILE="agent/runtime/wake.lock"
LAST_WAKE_FILE="agent/runtime/last-wake.timestamp"

# Acquire exclusive lock; exit silently if held (a wake is in progress).
exec 200>"$LOCKFILE"
flock -n 200 || exit 0

# Read last completed-wake timestamp; default to 0 if absent (forces a wake).
if [[ -f "$LAST_WAKE_FILE" ]]; then
    LAST_WAKE=$(cat "$LAST_WAKE_FILE" | tr -d '[:space:]')
else
    LAST_WAKE=0
fi

# Bash arithmetic comparison shorthand for "file mtime > LAST_WAKE".
fire_if_newer() {
    local path="$1"
    [[ -f "$path" ]] || return 1
    local mtime
    mtime=$(stat -c %Y "$path" 2>/dev/null || echo 0)
    [[ $mtime -gt $LAST_WAKE ]]
}

DRY_RUN="${1:-}"
NOW=$(date +%s)

# Check the agent's self-scheduled next wake. If the agent wrote a unix
# timestamp to next-wake-by.timestamp during the previous wake, and that
# moment has arrived, fire. This is the self-pacing path: the agent decides
# when it wants to wake again.
NEXT_WAKE_FILE="agent/runtime/next-wake-by.timestamp"
agent_self_signal=0
if [[ -f "$NEXT_WAKE_FILE" ]]; then
    NEXT_WAKE=$(tr -d '[:space:]' < "$NEXT_WAKE_FILE" 2>/dev/null || echo 0)
    if [[ "$NEXT_WAKE" =~ ^[0-9]+$ ]] && [[ $NOW -ge $NEXT_WAKE ]] && [[ $NEXT_WAKE -gt 0 ]]; then
        agent_self_signal=1
    fi
fi

fire_if_dir_newer() {
    local dir="$1"
    [[ -d "$dir" ]] || return 1
    local mtime
    mtime=$(stat -c %Y "$dir" 2>/dev/null || echo 0)
    [[ $mtime -gt $LAST_WAKE ]]
}

reason=""
if fire_if_newer dialogue.md; then
    reason="dialogue.md newer than last wake"
elif fire_if_newer wake.md; then
    reason="wake.md newer than last wake"
elif fire_if_dir_newer agent/inbox/from-originator; then
    reason="new message in agent/inbox/from-originator/"
elif [[ $agent_self_signal -eq 1 ]]; then
    reason="agent self-scheduled wake (next-wake-by @ $NEXT_WAKE)"
fi

if [[ -n "$reason" ]]; then
    if [[ "$DRY_RUN" == "--dry-run" ]]; then
        echo "would fire wake ($reason)"
        exit 0
    fi
    # Signal present. Exec into the wake — FD 200 (the lock) is inherited.
    # When reflect-wake exits, the FD closes and the lock auto-releases.
    exec bin/reflect-wake
fi

if [[ "$DRY_RUN" == "--dry-run" ]]; then
    next_wake_str="absent"
    [[ -f "$NEXT_WAKE_FILE" ]] && next_wake_str="$(tr -d '[:space:]' < "$NEXT_WAKE_FILE")"
    echo "no signal (last wake @ $LAST_WAKE; dialogue mtime $(stat -c %Y dialogue.md 2>/dev/null || echo missing); wake.md mtime $(stat -c %Y wake.md 2>/dev/null || echo missing); next-wake-by $next_wake_str; now $NOW)"
fi

# No signal. Exit silently; the lock releases when this script's FD 200 closes.
exit 0
