/**
 * src/session/timeline.ts — N-timeline (A4)
 *
 * Pure functions for the bounded, timestamped activity ring.
 * Zero DOM dependencies; importable from both backend and tests.
 *
 * Security:
 *  SEC-H6 — sanitizeField strips control chars and truncates tool names/paths
 *  SEC-M8 — appendEvent enforces the ring cap on every call
 */

import type { TimelineClass, TimelineEvent } from '../types.js';

// ── constants ─────────────────────────────────────────────────────────────────

/** Tools whose PostToolUse event gets an "edited" flavour label. */
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);

/** Whitelisted Claude Code hook event names. Events outside this set are
 *  dropped in makeTimelineEvent (unknown hooks = don't pollute the timeline). */
const WHITELISTED_EVENTS = new Set([
  'PreToolUse',
  'PostToolUse',
  'PermissionRequest',
  'Notification',
  'Stop',
  'SessionEnd',
  'UserPromptSubmit',
]);

/** Maps hook event names to semantic TimelineClass values (§3.1 review #1).
 *  The FE consumes `class` directly for icon/colour — never re-derives. */
const CLASS_MAP: Readonly<Record<string, TimelineClass>> = {
  PreToolUse: 'tool',
  PostToolUse: 'tool',
  PermissionRequest: 'waiting',
  Notification: 'waiting',
  Stop: 'done',
  SessionEnd: 'done',
  UserPromptSubmit: 'user',
};

// ── sanitizeField ─────────────────────────────────────────────────────────────

/**
 * Strip ASCII control characters (0x00–0x1f) from `s`, then truncate to `max`
 * characters. Used for tool names and any other hook-supplied string fields.
 * (SEC-H6)
 */
export function sanitizeField(s: string, max = 200): string {
  // eslint-disable-next-line no-control-regex
  return s.replace(/[\x00-\x1f]/g, '').slice(0, max);
}

// ── deriveClass ───────────────────────────────────────────────────────────────

/**
 * Map a raw Claude Code hook event name to its semantic TimelineClass (§3.1).
 * Falls back to 'tool' for any event that doesn't appear in the map —
 * makeTimelineEvent already whitelist-gates, so this is just a safety net.
 */
export function deriveClass(eventName: string): TimelineClass {
  return CLASS_MAP[eventName] ?? 'tool';
}

// ── deriveLabel ───────────────────────────────────────────────────────────────

/**
 * Produce a human-language label for a hook event.
 * `toolName` is expected to already be sanitized when called from
 * makeTimelineEvent; callers must not pass raw hook data here directly.
 */
export function deriveLabel(eventName: string, toolName?: string): string {
  switch (eventName) {
    case 'PreToolUse':
      return toolName ? `using ${toolName}` : 'using tool';

    case 'PostToolUse':
      if (toolName && EDIT_TOOLS.has(toolName)) {
        return `edited with ${toolName}`;
      }
      return toolName ? `ran ${toolName}` : 'ran tool';

    case 'PermissionRequest':
    case 'Notification':
      return 'waiting for approval';

    case 'Stop':
    case 'SessionEnd':
      return 'done';

    case 'UserPromptSubmit':
      return 'user input';

    default:
      return 'activity';
  }
}

// ── makeTimelineEvent ─────────────────────────────────────────────────────────

/**
 * Build a TimelineEvent from raw hook data, or return null if the event name
 * is not in the whitelist (unknown hooks are silently dropped).
 *
 * Sanitizes `toolName` via sanitizeField before embedding it in the event or
 * passing it to deriveLabel (SEC-H6).
 */
export function makeTimelineEvent(
  eventName: string,
  toolName: string | undefined,
  at: number,
): TimelineEvent | null {
  if (!WHITELISTED_EVENTS.has(eventName)) return null;

  const sanitizedToolName =
    toolName !== undefined ? sanitizeField(toolName) : undefined;

  return {
    at,
    class: deriveClass(eventName),
    toolName: sanitizedToolName,
    label: deriveLabel(eventName, sanitizedToolName),
  };
}

// ── appendEvent ───────────────────────────────────────────────────────────────

/**
 * Return a NEW array with `ev` appended to `events`, evicting the oldest
 * entries if the length would exceed `maxLen`. Never mutates `events` (SEC-M8).
 */
export function appendEvent(
  events: readonly TimelineEvent[],
  ev: TimelineEvent,
  maxLen: number,
): readonly TimelineEvent[] {
  const next = [...events, ev];
  if (next.length > maxLen) {
    return next.slice(next.length - maxLen);
  }
  return next;
}
