'use strict';

const ACTIVE_STATUSES = new Set(['queued', 'running', 'stopping']);
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'stopped', 'blocked', 'no_change', 'read_only']);
const RECENT_COMPLETED_MS = 5 * 60 * 1000;
const MAX_SNAPSHOT_TASKS = 80;
const ACTIVE_TASK_STALE_MS = Math.max(10 * 60 * 1000, Number(process.env.CTM_RUNTIME_WORK_ACTIVE_STALE_MS) || 2 * 60 * 60 * 1000);

function nowDefault() {
  return Date.now();
}

function cleanText(value, max = 240) {
  return String(value == null ? '' : value)
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, max);
}

function stableJsonPreview(value, max = 500) {
  if (value == null || value === '') return '';
  if (typeof value === 'string') return cleanText(value, max);
  try {
    return cleanText(JSON.stringify(value), max);
  } catch {
    return cleanText(String(value), max);
  }
}

function toolEventName(event = {}) {
  return cleanText(event.tool || event.name || event.toolName || event.tool_name || event.skill || 'tool', 80) || 'tool';
}

function toolEventId(event = {}, name = '') {
  return cleanText(event.id || event.toolCallId || event.tool_call_id || event.callId || event.call_id || name || 'tool', 120);
}

function kindForTool(name) {
  const clean = String(name || '').toLowerCase();
  if (/shell|bash|command|terminal|exec/.test(clean)) return 'shell';
  if (/skill/.test(clean)) return 'skill';
  if (/agent|subagent|worker/.test(clean)) return 'agent';
  return 'tool';
}

function taskPublicShape(task) {
  return {
    taskId: task.taskId,
    ctmSessionId: task.ctmSessionId,
    agentSessionId: task.agentSessionId || '',
    turnId: task.turnId || '',
    kind: task.kind || 'task',
    label: task.label || '',
    status: task.status || 'running',
    foregroundState: task.foregroundState || 'foreground',
    provider: task.provider || '',
    model: task.model || '',
    toolName: task.toolName || '',
    summary: task.summary || '',
    inputPreview: task.inputPreview || '',
    outputRef: task.outputRef || '',
    controlCaps: Array.isArray(task.controlCaps) ? task.controlCaps.slice(0, 8) : [],
    workerScope: task.workerScope || '',
    startedAt: Number(task.startedAt || 0),
    updatedAt: Number(task.updatedAt || 0),
    completedAt: Number(task.completedAt || 0),
    metadata: task.metadata && typeof task.metadata === 'object' ? task.metadata : {},
  };
}

function normalizeDbTask(row) {
  if (!row) return null;
  return {
    taskId: row.taskId || row.task_id,
    ctmSessionId: row.ctmSessionId || row.ctm_session_id,
    agentSessionId: row.agentSessionId || row.agent_session_id || '',
    turnId: row.turnId || row.turn_id || '',
    kind: row.kind || 'task',
    label: row.label || '',
    status: row.status || 'running',
    foregroundState: row.foregroundState || row.foreground_state || 'foreground',
    provider: row.provider || '',
    model: row.model || '',
    toolName: row.toolName || row.tool_name || '',
    summary: row.summary || '',
    inputPreview: row.inputPreview || row.input_preview || '',
    outputRef: row.outputRef || row.output_ref || '',
    controlCaps: Array.isArray(row.controlCaps) ? row.controlCaps : (Array.isArray(row.control_caps) ? row.control_caps : []),
    workerScope: row.workerScope || row.worker_scope || '',
    startedAt: Number(row.startedAt || row.started_at || 0),
    updatedAt: Number(row.updatedAt || row.updated_at || 0),
    completedAt: Number(row.completedAt || row.completed_at || 0),
    metadata: row.metadata && typeof row.metadata === 'object' ? row.metadata : {},
  };
}

function publicTaskWithStaleActiveGuard(task, now = Date.now()) {
  if (!task || !ACTIVE_STATUSES.has(task.status)) return taskPublicShape(task);
  const updatedAt = Number(task.updatedAt || task.startedAt || 0) || 0;
  if (!updatedAt || (now - updatedAt) <= ACTIVE_TASK_STALE_MS) return taskPublicShape(task);
  return taskPublicShape({
    ...task,
    status: 'stopped',
    foregroundState: 'background',
    completedAt: updatedAt,
    summary: task.summary || 'Stale running marker; terminal state is authoritative.',
    metadata: {
      ...(task.metadata || {}),
      staleActiveMarker: true,
      staleAfterMs: ACTIVE_TASK_STALE_MS,
    },
  });
}

class RuntimeWorkRegistry {
  constructor({ db, logger = console, now = nowDefault } = {}) {
    this.db = db || null;
    this.logger = logger;
    this.now = now;
    this.sessions = new Map();
    this.lastPersistErrorAt = 0;
  }

  _sessionMap(sessionId) {
    const key = String(sessionId || '').trim();
    if (!key) return null;
    let map = this.sessions.get(key);
    if (!map) {
      map = new Map();
      this.sessions.set(key, map);
      this._hydrateSession(key, map);
    }
    return map;
  }

  _hydrateSession(sessionId, map) {
    if (!this.db || typeof this.db.listRuntimeTasks !== 'function') return;
    try {
      const rows = this.db.listRuntimeTasks(sessionId, {
        includeCompletedSince: this.now() - RECENT_COMPLETED_MS,
        limit: MAX_SNAPSHOT_TASKS,
      });
      for (const row of rows || []) {
        const task = normalizeDbTask(row);
        if (task?.taskId) map.set(task.taskId, task);
      }
    } catch (err) {
      this._logPersistError('hydrate', err);
    }
  }

  _persist(task) {
    if (!this.db || typeof this.db.upsertRuntimeTask !== 'function') return;
    try {
      this.db.upsertRuntimeTask({
        taskId: task.taskId,
        ctmSessionId: task.ctmSessionId,
        agentSessionId: task.agentSessionId,
        turnId: task.turnId,
        kind: task.kind,
        label: task.label,
        status: task.status,
        foregroundState: task.foregroundState,
        provider: task.provider,
        model: task.model,
        toolName: task.toolName,
        summary: task.summary,
        inputPreview: task.inputPreview,
        outputRef: task.outputRef,
        controlCaps: task.controlCaps,
        workerScope: task.workerScope,
        startedAt: task.startedAt,
        updatedAt: task.updatedAt,
        completedAt: task.completedAt,
        metadata: task.metadata,
      });
    } catch (err) {
      this._logPersistError('persist', err);
    }
  }

  _logPersistError(action, err) {
    const now = this.now();
    if (now - this.lastPersistErrorAt < 30000) return;
    this.lastPersistErrorAt = now;
    try {
      this.logger.warn?.(`[runtime-work] ${action} failed: ${err?.message || err}`);
    } catch {}
  }

  _upsert(sessionId, task) {
    const map = this._sessionMap(sessionId);
    if (!map || !task?.taskId) return null;
    const existing = map.get(task.taskId) || {};
    const now = this.now();
    const merged = {
      ...existing,
      ...task,
      ctmSessionId: sessionId,
      startedAt: Number(existing.startedAt || task.startedAt || now) || now,
      updatedAt: Number(task.updatedAt || now) || now,
      controlCaps: Array.isArray(task.controlCaps)
        ? task.controlCaps
        : (Array.isArray(existing.controlCaps) ? existing.controlCaps : []),
      metadata: {
        ...(existing.metadata || {}),
        ...(task.metadata || {}),
      },
    };
    if (TERMINAL_STATUSES.has(merged.status) && !merged.completedAt) merged.completedAt = now;
    if (ACTIVE_STATUSES.has(merged.status)) merged.completedAt = 0;
    map.set(merged.taskId, merged);
    this._persist(merged);
    return taskPublicShape(merged);
  }

  startTurn({ sessionId, agentSessionId = '', turnId = '', prompt = '', provider = '', model = '', cwd = '', workerScope = 'walle-runtime' } = {}) {
    const cleanSessionId = String(sessionId || '').trim();
    if (!cleanSessionId) return null;
    const cleanTurnId = cleanText(turnId || `turn-${this.now()}`, 160);
    return this._upsert(cleanSessionId, {
      taskId: `${cleanSessionId}:turn:${cleanTurnId}`,
      agentSessionId,
      turnId: cleanTurnId,
      kind: 'turn',
      label: cleanText(prompt, 120) || 'Wall-E turn',
      status: 'running',
      foregroundState: 'foreground',
      provider,
      model,
      summary: 'Thinking',
      inputPreview: cleanText(prompt, 500),
      controlCaps: ['view', 'stop'],
      workerScope,
      metadata: { cwd },
    });
  }

  applyWalleProgress({ sessionId, agentSessionId = '', turnId = '', event = {}, provider = '', model = '', workerScope = 'walle-runtime' } = {}) {
    const cleanSessionId = String(sessionId || '').trim();
    const cleanTurnId = cleanText(turnId, 160);
    if (!cleanSessionId || !event || typeof event !== 'object') return null;
    const type = String(event.type || '').toLowerCase();
    if (type === 'thinking') {
      return this._upsert(cleanSessionId, {
        taskId: `${cleanSessionId}:turn:${cleanTurnId || 'current'}`,
        agentSessionId,
        turnId: cleanTurnId,
        kind: 'turn',
        label: 'Wall-E turn',
        status: 'running',
        provider,
        model,
        summary: 'Thinking',
        controlCaps: ['view', 'stop'],
        workerScope,
      });
    }
    if (type === 'tool_call') {
      const name = toolEventName(event);
      const eventId = toolEventId(event, name);
      return this._upsert(cleanSessionId, {
        taskId: `${cleanSessionId}:turn:${cleanTurnId || 'current'}:${eventId}`,
        agentSessionId,
        turnId: cleanTurnId,
        kind: kindForTool(name),
        label: cleanText(event.summary || name, 140) || name,
        status: 'running',
        foregroundState: 'foreground',
        provider,
        model,
        toolName: name,
        summary: cleanText(event.summary || `Running ${name}`, 240),
        inputPreview: stableJsonPreview(event.input || event.args || event.parameters || '', 500),
        controlCaps: ['view'],
        workerScope,
        metadata: { eventId },
      });
    }
    if (type === 'tool_result' || type === 'tool_done') {
      const explicitName = event.tool || event.name || event.toolName || event.tool_name || '';
      const name = explicitName ? toolEventName(event) : '';
      const eventId = toolEventId(event, name);
      const taskId = `${cleanSessionId}:turn:${cleanTurnId || 'current'}:${eventId}`;
      const existing = this._sessionMap(cleanSessionId)?.get(taskId) || null;
      const resolvedName = name || existing?.toolName || 'tool';
      const failed = !!event.error || !!event.failed || String(event.status || '').toLowerCase() === 'error';
      return this._upsert(cleanSessionId, {
        taskId,
        agentSessionId,
        turnId: cleanTurnId,
        kind: existing?.kind || kindForTool(resolvedName),
        label: existing?.label || cleanText(event.summary || resolvedName, 140) || resolvedName,
        status: failed ? 'failed' : 'completed',
        foregroundState: 'background',
        provider,
        model,
        toolName: resolvedName,
        summary: cleanText(event.summary || (failed ? `Failed ${resolvedName}` : `Completed ${resolvedName}`), 240),
        inputPreview: stableJsonPreview(event.input || event.args || '', 500),
        outputRef: cleanText(event.outputRef || event.output_ref || '', 500),
        controlCaps: ['view'],
        workerScope,
        metadata: {
          eventId,
          error: cleanText(event.error || '', 300),
        },
      });
    }
    if (type === 'skill_loaded' || type === 'skill_load_failed') {
      const name = cleanText(event.skill || event.name || 'skill', 80);
      const failed = type === 'skill_load_failed';
      const eventId = toolEventId(event, `skill-${name}`);
      return this._upsert(cleanSessionId, {
        taskId: `${cleanSessionId}:turn:${cleanTurnId || 'current'}:${eventId}`,
        agentSessionId,
        turnId: cleanTurnId,
        kind: 'skill',
        label: failed ? `Skill failed: ${name}` : `Skill loaded: ${name}`,
        status: failed ? 'failed' : 'completed',
        foregroundState: 'background',
        provider,
        model,
        toolName: 'skill_loaded',
        summary: cleanText(event.summary || (failed ? `Failed to load ${name}` : `Loaded ${name}`), 240),
        inputPreview: stableJsonPreview(event.input || event.args || { skill_name: name }, 500),
        controlCaps: ['view'],
        workerScope,
        metadata: { eventId, skill: name },
      });
    }
    if (type === 'cancelled') {
      return this.completeTurn({ sessionId: cleanSessionId, turnId: cleanTurnId, status: 'cancelled', summary: 'Cancelled' });
    }
    return null;
  }

  completeTurn({ sessionId, turnId = '', status = 'completed', summary = '' } = {}) {
    const cleanSessionId = String(sessionId || '').trim();
    const cleanTurnId = cleanText(turnId, 160);
    if (!cleanSessionId) return null;
    const map = this._sessionMap(cleanSessionId);
    if (!map) return null;
    const normalizedStatus = TERMINAL_STATUSES.has(status) ? status : 'completed';
    const now = this.now();
    let changed = null;
    for (const task of map.values()) {
      if (cleanTurnId && task.turnId !== cleanTurnId) continue;
      if (!ACTIVE_STATUSES.has(task.status)) continue;
      changed = this._upsert(cleanSessionId, {
        ...task,
        status: normalizedStatus,
        foregroundState: 'background',
        summary: cleanText(summary || task.summary || normalizedStatus, 240),
        updatedAt: now,
        completedAt: now,
      });
    }
    return changed;
  }

  requestStop(sessionId) {
    const cleanSessionId = String(sessionId || '').trim();
    const map = this._sessionMap(cleanSessionId);
    if (!map) return null;
    let changed = null;
    for (const task of map.values()) {
      if (!ACTIVE_STATUSES.has(task.status)) continue;
      changed = this._upsert(cleanSessionId, {
        ...task,
        status: 'stopping',
        summary: 'Stopping',
        updatedAt: this.now(),
      });
    }
    return changed;
  }

  snapshot(sessionId, { queue = null } = {}) {
    const cleanSessionId = String(sessionId || '').trim();
    const map = this._sessionMap(cleanSessionId) || new Map();
    const now = this.now();
    const tasks = Array.from(map.values())
      .filter((task) => ACTIVE_STATUSES.has(task.status) || !task.completedAt || now - task.completedAt <= RECENT_COMPLETED_MS)
      .sort((a, b) => {
        const activeDelta = (ACTIVE_STATUSES.has(b.status) ? 1 : 0) - (ACTIVE_STATUSES.has(a.status) ? 1 : 0);
        if (activeDelta) return activeDelta;
        return Number(b.updatedAt || 0) - Number(a.updatedAt || 0);
      })
      .slice(0, MAX_SNAPSHOT_TASKS)
      .map(task => publicTaskWithStaleActiveGuard(task, now));
    const counts = tasks.reduce((acc, task) => {
      if (ACTIVE_STATUSES.has(task.status)) acc.running += 1;
      if (task.kind === 'shell' && ACTIVE_STATUSES.has(task.status)) acc.shells += 1;
      if (task.kind === 'agent' && ACTIVE_STATUSES.has(task.status)) acc.agents += 1;
      if (task.kind === 'tool' && ACTIVE_STATUSES.has(task.status)) acc.tools += 1;
      if (task.kind === 'skill' && ACTIVE_STATUSES.has(task.status)) acc.skills += 1;
      return acc;
    }, { running: 0, shells: 0, agents: 0, tools: 0, skills: 0, queued: 0 });
    if (queue && Array.isArray(queue.items)) {
      counts.queued = queue.items.filter((item) => {
        const status = String(item?.status || 'pending').toLowerCase();
        return status !== 'sent' && status !== 'skipped';
      }).length;
    }
    return {
      sessionId: cleanSessionId,
      updatedAt: now,
      tasks,
      active: tasks.filter((task) => ACTIVE_STATUSES.has(task.status)),
      recent: tasks.filter((task) => !ACTIVE_STATUSES.has(task.status)),
      counts,
      isolation: {
        owner: 'ctm-runtime-work-registry',
        outputPolicy: 'metadata-only',
      },
    };
  }

  summary(sessionId) {
    const snapshot = this.snapshot(sessionId);
    return {
      running: snapshot.counts.running,
      shells: snapshot.counts.shells,
      agents: snapshot.counts.agents,
      tools: snapshot.counts.tools,
      skills: snapshot.counts.skills,
      queued: snapshot.counts.queued,
      updatedAt: snapshot.updatedAt,
    };
  }
}

function createRuntimeWorkRegistry(options) {
  return new RuntimeWorkRegistry(options);
}

module.exports = {
  ACTIVE_STATUSES,
  TERMINAL_STATUSES,
  RuntimeWorkRegistry,
  createRuntimeWorkRegistry,
  kindForTool,
};
