#!/usr/bin/env bash
# reflection's multi-track dispatcher.
#
# One cron entry, forever. Iterates agent/runtime/tracks/*/ and fires per-track
# wakes for tracks that have signal. Tracks run concurrently via background
# processes (one uv run per track).
#
# Signal check and lock acquisition are delegated to agent/lib/tick.py — the
# Python module is the single source of truth for "should this track wake?"
# This avoids duplicating ~50 lines of signal logic between this script and
# any future reflect-tick rewrite.
#
# Adding a track: mkdir -p agent/runtime/tracks/{name}/inbox/from-originator/
# Decommissioning: mv agent/runtime/tracks/{name} agent/runtime/tracks/.decommissioned/
# No crontab change needed for either operation.
#
# Dry-run mode (no wakes fired, signal state printed):
#   bin/reflect-dispatch --dry-run
#
# Suggested crontab (replaces bin/reflect-tick):
#   * * * * * cd /home/prmichaelsen/.acp/projects/reflection && bin/reflect-dispatch >> /tmp/reflect-dispatch.log 2>&1
#
# Requirements: bin/reflect-wake must accept --track <name> and export
# REFLECT_TRACK to the Python runtime.
set -euo pipefail

export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"

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

TRACKS_DIR="agent/runtime/tracks"
DRY_RUN="${1:-}"

# DR21: Dispatcher fail-fast — refuse if any linked project has a double-dispatch config.
# A linked project should NOT also be running its own scoped cron dispatcher.
# We detect this by checking if the project has an active crontab dispatch line.
PROJECTS_DIR="agent/projects"
if [[ -d "$PROJECTS_DIR" ]]; then
    CRONTAB_LINES=$(crontab -l 2>/dev/null || true)
    for proj_link in "$PROJECTS_DIR"/*/; do
        [[ -d "$proj_link" ]] || continue
        proj_name=$(basename "$proj_link")
        [[ "$proj_name" == .* ]] && continue
        real_proj=$(readlink -f "$proj_link" 2>/dev/null || true)
        [[ -z "$real_proj" ]] && continue  # dead symlink; skip
        if echo "$CRONTAB_LINES" | grep -q "$real_proj.*reflect-dispatch"; then
            cron_line=$(echo "$CRONTAB_LINES" | grep "$real_proj.*reflect-dispatch" | head -1)
            {
                echo "ERROR: dispatch config invalid"
                echo "  $proj_name ($real_proj) is symlinked at $PROJECTS_DIR/$proj_name"
                echo "  AND has a scoped cron line: $cron_line"
                echo ""
                echo "  Resolve by removing exactly one:"
                echo "  (a) remove the symlink:    reflect project unlink $proj_name"
                echo "  (b) remove the cron line:  crontab -e   (delete the $proj_name dispatch line)"
                echo ""
                echo "  Refusing to run. See agent/design/design.multi-project-reflection~f7fd5be0.md DR2.1."
            } >> /tmp/reflect-dispatch.log
            exit 1
        fi
    done
fi

if [[ "$DRY_RUN" == "--dry-run" ]]; then
    # Dry-run: query signal state from Python module without firing.
    for track_dir in "$TRACKS_DIR"/*/; do
        [[ -d "$track_dir" ]] || continue
        name=$(basename "$track_dir")
        [[ "$name" == .* ]] && continue  # skip hidden / .decommissioned
        [[ -f "$track_dir/disabled.flag" ]] && continue  # track disabled
        [[ -f "$track_dir/human.flag" ]] && continue   # P9: human track, no wake
        echo -n "[$name] "
        uv run python -c "
from agent.lib.tick import check_track_signal
sig = check_track_signal('$name')
if sig.should_fire:
    print(f'would fire: {sig.reason}')
else:
    print('no signal')
" 2>/dev/null || echo "error checking signal"
    done
    exit 0
fi

# Compute REFLECT_PROJECT_ROOT for a track directory (DR18.1).
# Returns orchestrator root for global tracks; resolved project root for project-nested tracks.
#
# Key: compare the UNRESOLVED track_dir path against PROJECTS_DIR using string-prefix
# matching. readlink -f/-m follow symlinks, which defeats the check (a project-nested
# track at agent/projects/foo/... resolves to /real/path/to/foo/..., losing the prefix).
# Instead, normalize to absolute paths by prepending PROJECT_ROOT for relative paths,
# without any symlink traversal.
_project_root_for_track() {
    local track_dir="${1%/}"  # strip trailing slash
    # Normalize to absolute WITHOUT following symlinks
    local abs_track_dir
    if [[ "$track_dir" == /* ]]; then
        abs_track_dir="$track_dir"
    else
        abs_track_dir="$PROJECT_ROOT/$track_dir"
    fi
    # projects_abs: absolute path to agent/projects/ (no symlink resolution needed;
    # it's a real directory under PROJECT_ROOT)
    local projects_abs="$PROJECT_ROOT/$PROJECTS_DIR"
    if [[ -d "$PROJECTS_DIR" ]] && [[ "$abs_track_dir" == "$projects_abs"/* ]]; then
        # Project-nested track: first path component after projects_abs/ is the project name
        local rel="${abs_track_dir#$projects_abs/}"
        local proj_name="${rel%%/*}"
        # Resolve the symlink to get the real project root directory
        readlink -f "$PROJECTS_DIR/$proj_name" 2>/dev/null || echo "$PROJECT_ROOT"
    else
        echo "$PROJECT_ROOT"
    fi
}

# Walk orchestrator's global tracks
_fire_tracks() {
    local tracks_base="$1"
    local base_root="$2"
    for track_dir in "$tracks_base"/*/; do
        [[ -d "$track_dir" ]] || continue
        name=$(basename "$track_dir")
        [[ "$name" == .* ]] && continue  # skip hidden / .decommissioned
        [[ -f "$track_dir/disabled.flag" ]] && continue  # track disabled
        [[ -f "$track_dir/human.flag" ]] && continue   # P9: originator/human track — no wake supervisor

        # DR18.1: compute per-track project root and export for log routing
        track_project_root=$(_project_root_for_track "$track_dir")

        REFLECT_PROJECT_ROOT="$track_project_root" \
        uv run python -m agent.lib.tick fire-if-signal "$name" \
            >> "/tmp/reflect-tick-${name}.log" 2>&1 &
    done
}

# Live mode: one background process per track.
# Delegates signal check + lock acquisition + exec to agent/lib/tick.py.
# If signal is positive and lock is free: exec's reflect-wake with REFLECT_TRACK set.
# If no signal or lock held: exits 0 silently.

# Global tracks (orchestrator-owned)
_fire_tracks "$TRACKS_DIR" "$PROJECT_ROOT"

# Project-nested tracks (per linked project, DR7)
if [[ -d "$PROJECTS_DIR" ]]; then
    for proj_link in "$PROJECTS_DIR"/*/; do
        [[ -d "$proj_link" ]] || continue
        proj_name=$(basename "$proj_link")
        [[ "$proj_name" == .* ]] && continue
        proj_tracks="$proj_link/agent/runtime/tracks"
        [[ -d "$proj_tracks" ]] || continue
        _fire_tracks "$proj_tracks" "$(readlink -f "$proj_link" 2>/dev/null || echo "$proj_link")"
    done
fi

wait
