#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
state_dir="${ACTION_TIMELINE_STATE_DIR:-$repo_root/.tmp/action-timeline}"
timeline_file="${ACTION_TIMELINE_FILE:-$state_dir/timeline.jsonl}"
mode="human"
limit=20
filter=""
include_all=false

while (($# > 0)); do
  case "$1" in
    --human) mode="human"; shift ;;
    --json) mode="json"; shift ;;
    --telegram) mode="telegram"; shift ;;
    --limit) limit="${2:?}"; shift 2 ;;
    --filter) filter="${2:?}"; shift 2 ;;
    --all) include_all=true; shift ;;
    --file) timeline_file="${2:?}"; shift 2 ;;
    -h | --help)
      echo "Usage: ai/tools/action-timeline/list.sh [--human|--json|--telegram] [--limit n] [--filter id] [--all] [--file path]"
      exit 0
      ;;
    *) echo "action timeline list error: unknown argument: $1" >&2; exit 2 ;;
  esac
done
[[ "$limit" =~ ^[0-9]+$ ]] || { echo "action timeline list error: --limit must be numeric" >&2; exit 2; }

node - "$timeline_file" "$mode" "$limit" "$filter" "$include_all" <<'NODE'
const fs = require('node:fs');
const path = require('node:path');
const [file, mode, limitRaw, filter, includeAllRaw] = process.argv.slice(2);
const limit = Number(limitRaw || 20);
const includeAll = includeAllRaw === 'true';
function readJsonl(targetFile, source) {
  if (!fs.existsSync(targetFile)) return [];
  const raw = fs.readFileSync(targetFile, 'utf8').trim();
  if (!raw) return [];
  return raw.split(/\r?\n/).filter(Boolean).map((line) => {
    try { return { ...JSON.parse(line), _source: source }; }
    catch { return { type: 'invalid', status: 'error', message: line, _source: source }; }
  });
}
function archiveFilesFor(targetFile) {
  const archiveDir = path.join(path.dirname(targetFile), 'archive');
  if (!fs.existsSync(archiveDir)) return [];
  return fs.readdirSync(archiveDir)
    .filter((name) => name.endsWith('.timeline.jsonl'))
    .sort()
    .map((name) => path.join(archiveDir, name));
}
const activeEvents = readJsonl(file, 'active');
const archiveFiles = archiveFilesFor(file);
let archiveTotalCount = 0;
for (const archiveFile of archiveFiles) {
  if (!fs.existsSync(archiveFile)) continue;
  const raw = fs.readFileSync(archiveFile, 'utf8').trim();
  if (raw) archiveTotalCount += raw.split(/\r?\n/).filter(Boolean).length;
}
const archivedEvents = (includeAll || filter)
  ? archiveFiles.flatMap((archiveFile) => readJsonl(archiveFile, 'archive'))
  : [];
const events = [...archivedEvents, ...activeEvents].sort((a, b) => String(a.timestamp || '').localeCompare(String(b.timestamp || '')));
const importantTypes = new Set([
  'question_created',
  'telegram_answer_received',
  'approval_accepted',
  'approval_rejected',
  'approved_action_registered',
  'approved_action_validated',
  'approved_action_started',
  'approved_action_executed',
  'approved_action_failed',
  'approved_action_blocked',
  'daemon_action_requested',
  'daemon_action_completed',
  'supervisor_action_requested',
  'supervisor_action_completed',
  'validation_failed',
  'failure',
  'recovery',
]);
const noisyTypes = new Set(['heartbeat', 'poll', 'debug', 'approved_action_validated']);
function idMatches(event, value) {
  if (!value) return true;
  return [event.question_id, event.request_id, event.run_id, event.action]
    .filter(Boolean)
    .some((item) => String(item).includes(value));
}
function isImportant(event) {
  if (includeAll) return true;
  if (!filter && ['simulated_approved_action', 'simulated_close_commit', 'write_temp_file'].includes(event.action || '')) return false;
  if (noisyTypes.has(event.type || '') && !['failed', 'blocked', 'error', 'stale', 'denied'].includes(event.status || '')) return false;
  if (event.status && ['failed', 'blocked', 'error', 'stale', 'denied', 'success'].includes(event.status)) return true;
  if (importantTypes.has(event.type || '')) return true;
  return !noisyTypes.has(event.type || '');
}
const filtered = events.filter((event) => idMatches(event, filter)).filter(isImportant);
function dedupe(eventsToDedupe) {
  if (includeAll || filter) return eventsToDedupe;
  const seen = new Map();
  for (const event of eventsToDedupe) {
    const reason = String(event.message || '').replace(/^stale approval:\s*/, '').trim();
    const staleGroup = event.type === 'approved_action_blocked' && String(event.message || '').startsWith('stale approval:');
    const key = [event.type || '', event.status || '', event.action || '', staleGroup ? '' : event.question_id || '', reason].join('\x1f');
    const existing = seen.get(key);
    if (existing) {
      existing.repeat_count = (existing.repeat_count || 1) + 1;
      existing.timestamp = event.timestamp || existing.timestamp;
      if (event.question_id) {
        existing.related_question_ids = Array.from(new Set([...(existing.related_question_ids || []), event.question_id]));
      }
    } else {
      seen.set(key, { ...event, repeat_count: 1, related_question_ids: event.question_id ? [event.question_id] : [] });
    }
  }
  return Array.from(seen.values()).sort((a, b) => String(a.timestamp || '').localeCompare(String(b.timestamp || '')));
}
const displayEvents = dedupe(filtered);
const recent = displayEvents.slice(-limit);
function nextAction(event) {
  if (event.next_action) return event.next_action;
  if (event.status === 'blocked' || event.status === 'failed') return 'Inspect /details_timeline or run ai/tools/action-timeline/list.sh --human --filter <id>';
  if (event.status === 'success') return 'Continue with the next workflow step.';
  return '';
}
function shortId(event) {
  return event.question_id || event.request_id || event.run_id || '';
}
function compactLine(event) {
  const parts = [
    event.timestamp || '',
    event.type || 'event',
    event.status || 'info',
    event.action || '',
    shortId(event),
  ].filter(Boolean);
  const next = nextAction(event);
  const repeat = event.repeat_count && event.repeat_count > 1 ? ` (${event.repeat_count} repeats)` : '';
  const related = event.related_question_ids && event.related_question_ids.length > 1
    ? `\n  Related questions: ${event.related_question_ids.slice(0, 6).join(', ')}${event.related_question_ids.length > 6 ? ', ...' : ''}`
    : '';
  return `${parts.join(' | ')}${repeat} :: ${event.message || ''}${related}${next ? `\n  Next: ${next}` : ''}`;
}
function lastMatching(predicate) {
  return [...displayEvents].reverse().find(predicate);
}
function eventLabel(event) {
  if (!event) return 'None';
  return `${event.type || 'event'} ${event.status || 'info'}${event.action ? ` ${event.action}` : ''}${shortId(event) ? ` ${shortId(event)}` : ''}: ${event.message || ''}`;
}
function topEvents(predicate, count) {
  return [...displayEvents].reverse().filter(predicate).slice(0, count).reverse();
}
function currentState() {
  const lastImportant = lastMatching(() => true);
  const lastFailure = lastMatching((event) => ['failed', 'blocked', 'error', 'stale', 'denied'].includes(event.status || ''));
  const next = lastFailure ? nextAction(lastFailure) : 'Continue with the next workflow step.';
  return {
    active_count: activeEvents.length,
    archived_count: archivedEvents.length,
    archived_total_count: archiveTotalCount,
    filtered_count: filtered.length,
    display_count: displayEvents.length,
    last_important_event: lastImportant || null,
    last_failure_or_block: lastFailure || null,
    suggested_next_action: next,
  };
}
function printHumanSection(title, sectionEvents) {
  console.log('');
  console.log(title);
  if (sectionEvents.length === 0) {
    console.log('- None');
    return;
  }
  for (const event of sectionEvents) {
    console.log(`- ${compactLine(event).replace(/\n/g, '\n  ')}`);
  }
}
function printTelegramEvent(event) {
  const repeat = event.repeat_count && event.repeat_count > 1 ? ` (${event.repeat_count} repeats)` : '';
  console.log(`${event.type || 'event'} · ${event.status || 'info'}${event.action ? ` · ${event.action}` : ''}${repeat}`);
  if (shortId(event)) console.log(`ID: ${shortId(event)}`);
  if (event.related_question_ids && event.related_question_ids.length > 1) {
    console.log(`Related: ${event.related_question_ids.slice(0, 4).join(', ')}${event.related_question_ids.length > 4 ? ', ...' : ''}`);
  }
  console.log(event.message || '');
  const next = nextAction(event);
  if (next) console.log(`Next: ${next}`);
}
if (mode === 'json') {
  console.log(JSON.stringify({
    file,
    active_count: activeEvents.length,
    archived_count: archivedEvents.length,
    archived_total_count: archiveTotalCount,
    count: events.length,
    filtered_count: filtered.length,
    display_count: displayEvents.length,
    filter,
    current_state: currentState(),
    recent,
  }, null, 2));
} else if (mode === 'telegram') {
  console.log('Action timeline');
  const state = currentState();
  console.log(`Events: ${displayEvents.length}/${events.length}${filter ? ` | Filter: ${filter}` : ''}`);
  console.log(`Last: ${eventLabel(state.last_important_event)}`);
  console.log(`Next: ${state.suggested_next_action}`);
  if (recent.length === 0) {
    console.log('No matching timeline events.');
  }
  const blocked = topEvents((event) => event.status === 'blocked' || event.status === 'stale', 3);
  const failed = topEvents((event) => event.status === 'failed' || event.status === 'error', 2);
  const successes = topEvents((event) => event.status === 'success', 2);
  if (!filter && !includeAll) {
    console.log('');
    console.log('Blocked/stale');
    if (blocked.length === 0) console.log('None');
    for (const event of blocked) printTelegramEvent(event);
    console.log('');
    console.log('Failures');
    if (failed.length === 0) console.log('None');
    for (const event of failed) printTelegramEvent(event);
    console.log('');
    console.log('Recent success');
    if (successes.length === 0) console.log('None');
    for (const event of successes) printTelegramEvent(event);
  } else {
    for (const event of recent) {
      console.log('');
      console.log(`${event.timestamp || ''}`);
      printTelegramEvent(event);
    }
  }
  console.log('');
  console.log('More: /timeline_full or /timeline <id>');
} else {
  console.log('Action Timeline');
  console.log(`File: ${file}`);
  const state = currentState();
  console.log(`Events: ${displayEvents.length}/${events.length}${filter ? ` | Filter: ${filter}` : ''}`);
  console.log('');
  console.log('Current state');
  console.log(`- Active events: ${activeEvents.length}`);
  console.log(`- Archived events available: ${archiveTotalCount}`);
  console.log(`- Archived events included: ${archivedEvents.length}`);
  console.log(`- Last important event: ${eventLabel(state.last_important_event)}`);
  console.log(`- Suggested next action: ${state.suggested_next_action}`);
  if (filter || includeAll) {
    printHumanSection('Matching events', recent);
  } else {
    printHumanSection('Blocked/stale approvals', topEvents((event) => event.status === 'blocked' || event.status === 'stale', 5));
    printHumanSection('Failed actions', topEvents((event) => event.status === 'failed' || event.status === 'error', 3));
    printHumanSection('Last successful executions', topEvents((event) => event.status === 'success', 3));
    printHumanSection('Recent important events', recent);
  }
}
NODE
