#!/usr/bin/env bash
# ── Timmy Development Loop v3 ──────────────────────────────────────────
# Runs forever. Each cycle: triage → Hermes executes → retro → repeat.
#
# TRIAGE (two tiers):
#   Fast (every 5 cycles):  mechanical scoring → .loop/queue.json
#   Deep (every 20 cycles): Hermes reads code + consults Timmy → refined queue
#
# RETRO (every cycle): structured log → .loop/retro/cycles.jsonl
# ───────────────────────────────────────────────────────────────────────

set -uo pipefail

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

REPO="$HOME/Timmy-Time-dashboard"
STATE="$REPO/.loop/state.json"
LOG_DIR="$REPO/.loop/logs"
CLAIMS="$REPO/.loop/claims.json"
PROMPT_FILE="$HOME/.hermes/bin/timmy-loop-prompt.md"
LOCKFILE="/tmp/timmy-loop.lock"
COOLDOWN=3
MAX_CYCLE_TIME=1200          # 20 min — enough for complex issues
CLAIM_TTL_SECONDS=3600       # 1 hour — stale claims auto-expire
FAST_TRIAGE_INTERVAL=5       # mechanical scoring every N cycles
DEEP_TRIAGE_INTERVAL=20      # Hermes+Timmy deep triage every N cycles
TRIAGE_SCRIPT="$REPO/scripts/triage_score.py"
RETRO_SCRIPT="$REPO/scripts/cycle_retro.py"
DEEP_TRIAGE_SCRIPT="$REPO/scripts/deep_triage.sh"
QUEUE_FILE="$REPO/.loop/queue.json"
# macOS doesn't have timeout; use perl fallback
if ! command -v timeout &>/dev/null; then
    timeout() {
        local duration="$1"; shift
        perl -e "alarm $duration; exec @ARGV" -- "$@"
    }
fi

mkdir -p "$LOG_DIR"

# ── Single-instance lock ──────────────────────────────────────────────
if [ -f "$LOCKFILE" ]; then
    PID=$(cat "$LOCKFILE" 2>/dev/null)
    if kill -0 "$PID" 2>/dev/null; then
        echo "[loop] Already running (PID $PID). Exiting."
        exit 0
    fi
    rm -f "$LOCKFILE"
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT

# ── Helpers ───────────────────────────────────────────────────────────
update_state() {
    local key="$1" value="$2"
    python3 -c "
import json
with open('$STATE') as f: s = json.load(f)
s['$key'] = $value
with open('$STATE', 'w') as f: json.dump(s, f, indent=2)
"
}

log() {
    echo "[$(date '+%H:%M:%S')] $*"
}

# ── Expire stale claims ──────────────────────────────────────────────
expire_claims() {
    python3 -c "
import json
from datetime import datetime, timezone, timedelta

TTL = timedelta(seconds=$CLAIM_TTL_SECONDS)
now = datetime.now(timezone.utc)

try:
    with open('$CLAIMS') as f:
        claims = json.load(f)
except:
    claims = {}

expired = []
active = {}
for issue, info in claims.items():
    if isinstance(info, dict):
        claimed_at = info.get('at', '')
        try:
            dt = datetime.fromisoformat(claimed_at.replace('Z', '+00:00'))
            if now - dt > TTL:
                expired.append(issue)
                continue
        except:
            pass
    active[issue] = info

if expired:
    print(f'[claims] Expired {len(expired)} stale claims: {expired}')
    with open('$CLAIMS', 'w') as f:
        json.dump(active, f, indent=2)
else:
    print('[claims] No stale claims')
" 2>&1
}

# ── Cleanup after timeout/failure ─────────────────────────────────────
cleanup_cycle() {
    local cycle_num="$1"
    local worktree="/tmp/timmy-cycle-${cycle_num}"

    # Remove worktree if it exists
    if [ -d "$worktree" ]; then
        log "Cleaning up worktree $worktree"
        cd "$REPO" && git worktree remove "$worktree" --force 2>/dev/null || true
    fi

    # Release any claims held by this cycle
    python3 -c "
import json
try:
    with open('$CLAIMS') as f:
        claims = json.load(f)
    released = []
    active = {}
    for issue, info in claims.items():
        agent = info.get('by', '') if isinstance(info, dict) else ''
        if 'loop' in agent or 'hermes' in agent.lower():
            released.append(issue)
        else:
            active[issue] = info
    if released:
        with open('$CLAIMS', 'w') as f:
            json.dump(active, f, indent=2)
        print(f'[cleanup] Released claims: {released}')
except Exception as e:
    print(f'[cleanup] Claim cleanup failed: {e}')
" 2>&1
}
# ── Fast triage (mechanical scoring) ──────────────────────────────────
run_fast_triage() {
    if [ -f "$TRIAGE_SCRIPT" ]; then
        log "Running fast triage..."
        python3 "$TRIAGE_SCRIPT" 2>&1
    else
        log "WARNING: triage script not found at $TRIAGE_SCRIPT"
    fi
}

# ── Deep triage (Hermes + Timmy) ─────────────────────────────────────
run_deep_triage() {
    if [ -f "$DEEP_TRIAGE_SCRIPT" ]; then
        log "Running deep triage (Hermes + Timmy)..."
        timeout 600 bash "$DEEP_TRIAGE_SCRIPT" 2>&1
    else
        log "WARNING: deep triage script not found at $DEEP_TRIAGE_SCRIPT"
    fi
}

# ── Cycle retrospective ─────────────────────────────────────────────
log_retro() {
    # Usage: log_retro <success|failure> [extra args for cycle_retro.py]
    local outcome="$1"; shift
    if [ -f "$RETRO_SCRIPT" ]; then
        local duration=$(( $(date +%s) - CYCLE_START ))
        if [ "$outcome" = "success" ]; then
            python3 "$RETRO_SCRIPT" --cycle "$CYCLE" --success \
                --duration "$duration" "$@" 2>&1
        else
            python3 "$RETRO_SCRIPT" --cycle "$CYCLE" --failure \
                --duration "$duration" "$@" 2>&1
        fi
    fi
}

# ── Inject queue context into prompt ─────────────────────────────────
get_queue_context() {
    if [ -f "$QUEUE_FILE" ]; then
        python3 -c "
import json
with open('$QUEUE_FILE') as f: q = json.load(f)
if not q:
    print('Queue is empty. Check open issues on Gitea and pick the highest-priority one.')
else:
    print(f'PRIORITIZED QUEUE ({len(q)} ready issues):')
    for i, item in enumerate(q[:8]):
        flag = 'BUG' if item.get('type') == 'bug' else item.get('type', '?').upper()
        print(f'  {i+1}. #{item[\"issue\"]} [{flag}] score={item[\"score\"]} — {item.get(\"title\",\"?\")[:60]}')
        if item.get('files'):
            print(f'     files: {\", \".join(item[\"files\"][:3])}')
    if len(q) > 8:
        print(f'  ... +{len(q)-8} more')
    print()
    print(f'Pick from the TOP of this queue. Issue #{q[0][\"issue\"]} is highest priority.')
" 2>/dev/null
    else
        echo "No queue file. Check open issues on Gitea and pick the highest-priority one."
    fi
}

# ── Main Loop ─────────────────────────────────────────────────────────
log "Timmy development loop v3 starting. PID $$"
log "Timeout: ${MAX_CYCLE_TIME}s | Cooldown: ${COOLDOWN}s | Claim TTL: ${CLAIM_TTL_SECONDS}s"
log "Repo:  $REPO"
update_state "started_at" "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""
update_state "status" '"running"'

CYCLE=$(python3 -c "import json; print(json.load(open('$STATE'))['cycle'])")

while true; do
    # Check for stop file
    if [ -f "$REPO/.loop/STOP" ]; then
        log "STOP file found. Halting loop."
        update_state "status" '"stopped"'
        break
    fi

    CYCLE=$((CYCLE + 1))
    CYCLE_LOG="$LOG_DIR/cycle-$(printf '%04d' $CYCLE).log"

    log "━━━ Cycle $CYCLE ━━━"
    update_state "cycle" "$CYCLE"
    update_state "status" '"working"'

    CYCLE_START=$(date +%s)

    # ── Pre-cycle housekeeping ────────────────────────────────────────
    expire_claims

    # ── Triage (fast every 5, deep every 20) ─────────────────────────
    if (( CYCLE % DEEP_TRIAGE_INTERVAL == 0 )); then
        run_deep_triage
    elif (( CYCLE % FAST_TRIAGE_INTERVAL == 0 )); then
        run_fast_triage
    fi

    # ── Build the prompt with time budget + queue ────────────────────
    QUEUE_CONTEXT=$(get_queue_context)
    PROMPT=$(cat "$PROMPT_FILE")
    PROMPT="TIME BUDGET: You have $((MAX_CYCLE_TIME / 60)) minutes for this cycle. Plan accordingly — do not start work you cannot finish.

$QUEUE_CONTEXT

$PROMPT"

    log "Spawning hermes for cycle $CYCLE..."

    # Run hermes with timeout — tee to log AND stdout for live visibility
    if timeout "$MAX_CYCLE_TIME" hermes chat --yolo -q "$PROMPT" 2>&1 | tee "$CYCLE_LOG"; then
        log "Cycle $CYCLE completed successfully"
        update_state "status" '"idle"'
        update_state "last_completed" "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""

        # ── Cycle retro (success) ────────────────────────────────────
        log_retro success

    else
        EXIT_CODE=$?
        log "Cycle $CYCLE exited with code $EXIT_CODE"
        update_state "status" '"error"'
        python3 -c "
import json
with open('$STATE') as f: s = json.load(f)
errs = s.get('errors', [])
errs.append({'cycle': $CYCLE, 'code': $EXIT_CODE, 'time': '$(date -u +%Y-%m-%dT%H:%M:%SZ)'})
s['errors'] = errs[-10:]
with open('$STATE', 'w') as f: json.dump(s, f, indent=2)
"
        # ── Cycle retro (failure) ────────────────────────────────────
        log_retro failure --reason "exit code $EXIT_CODE"

        # ── Cleanup on failure ───────────────────────────────────────
        cleanup_cycle "$CYCLE"
    fi

    log "Cooling down ${COOLDOWN}s before next cycle..."
    sleep "$COOLDOWN"
done
