'use strict';

const EventEmitter = require('events');

const WAITING_TTL_MS = 30 * 60 * 1000;

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

function toMs(value) {
  const n = Number(value);
  return Number.isFinite(n) && n > 0 ? n : nowMs();
}

function explicitActivityMs(input = {}) {
  const candidates = [
    input.lastActivity,
    input.lastAgentOutputAt,
    input.agentOutputAt,
    input.lastPtyActivity,
    input.lastJsonlActivity,
    input.outputAt,
  ];
  for (const value of candidates) {
    const n = Number(value);
    if (Number.isFinite(n) && n > 0) return n;
  }
  return 0;
}

function isActivityBearingStatus(input = {}) {
  const status = normalizeStatus(input.status);
  const source = String(input.source || '').toLowerCase();
  const reason = String(input.reason || '').toLowerCase();
  if (reason === 'snapshot') return false;
  if (status === 'waiting' && /waiting-for-input|terminal-prompt/.test(`${source} ${reason}`)) return false;
  if (/pty-activity|jsonl-event|codex-jsonl-event|claude-jsonl|terminal-output|session-resumed/.test(`${source} ${reason}`)) return true;
  if (status === 'running' && /authoritative:hook|hook|worker/.test(source)) return true;
  return false;
}

function normalizeStatus(status) {
  const text = String(status || '').trim().toLowerCase().replace(/[-\s]+/g, '_');
  if (text === 'busy' || text === 'active' || text === 'working' || text === 'thinking') return 'running';
  if (text === 'restoring' || text === 'resuming' || text === 'restore_starting' || text === 'restarting') return 'resuming';
  if (text === 'waiting_input' || text === 'waiting_for_input') return 'waiting';
  if (text === 'running' || text === 'resuming' || text === 'waiting' || text === 'idle' || text === 'exited') return text;
  return 'unknown';
}

function statusFromServerMessage(msg) {
  if (!msg || typeof msg !== 'object') return null;

  if (msg.type === 'stream-status') {
    return {
      sessionId: msg.sessionId,
      ctmSessionId: msg.ctmSessionId || null,
      agentSessionId: msg.sessionId || null,
      status: msg.newStatus || msg.status,
      source: 'stream',
      reason: msg.reason || 'stream-status',
      timestamp: msg.timestamp,
      lastActivity: msg.lastActivity,
      lastPtyActivity: msg.lastPtyActivity,
      lastJsonlActivity: msg.lastJsonlActivity,
    };
  }

  if (msg.type === 'session.status') {
    const source = msg.source || 'status';
    const status = msg.working ? 'running' : source === 'terminal-prompt' ? 'waiting' : 'idle';
    return {
      sessionId: msg.id,
      ctmSessionId: msg.id,
      status,
      source: `authoritative:${source}`,
      reason: source,
      timestamp: msg.timestamp,
    };
  }

  if (msg.type === 'waiting-for-input') {
    return {
      sessionId: msg.id,
      ctmSessionId: msg.id,
      status: 'waiting',
      source: 'waiting-for-input',
      reason: msg.reason || 'input',
      timestamp: msg.timestamp,
    };
  }

  if (msg.type === 'session-resumed') {
    return {
      sessionId: msg.id,
      ctmSessionId: msg.id,
      status: 'running',
      source: 'session-resumed',
      reason: 'terminal-output',
      timestamp: msg.timestamp,
    };
  }

  if (msg.type === 'approval-decision') {
    return {
      sessionId: msg.sessionId,
      ctmSessionId: msg.sessionId,
      status: msg.decision === 'approved' ? 'running' : 'waiting',
      source: 'approval',
      reason: msg.decision || 'decision',
      timestamp: msg.timestamp,
      decisionId: msg.decisionId,
    };
  }

  return null;
}

class SessionCapture extends EventEmitter {
  constructor({ sessionStream } = {}) {
    super();
    this._sessionStream = null;
    this._records = new Map();
    this._ctmToAgent = new Map();
    this._agentToCtm = new Map();
    this._unsubs = [];
    if (sessionStream) this.attachSessionStream(sessionStream);
  }

  attachSessionStream(sessionStream) {
    this.detach();
    this._sessionStream = sessionStream || null;
    if (!sessionStream) return;

    const onLink = (link) => {
      this.linkIdentity(link.ctmSessionId, link.agentSessionId);
    };
    const onUnlink = (link) => {
      this.ingestStatus({
        sessionId: link.agentSessionId,
        ctmSessionId: link.ctmSessionId,
        agentSessionId: link.agentSessionId,
        status: 'exited',
        source: 'stream',
        reason: 'session-exit',
        timestamp: nowMs(),
      });
    };
    const onStatus = (statusEvt) => {
      this.ingestStreamStatus(statusEvt);
    };
    const onEvent = (evt) => {
      this.emit('capture-event', evt);
    };

    sessionStream.on('link', onLink);
    sessionStream.on('unlink', onUnlink);
    sessionStream.on('status', onStatus);
    sessionStream.on('event', onEvent);
    sessionStream.on('event:update', onEvent);
    this._unsubs.push(() => sessionStream.off('link', onLink));
    this._unsubs.push(() => sessionStream.off('unlink', onUnlink));
    this._unsubs.push(() => sessionStream.off('status', onStatus));
    this._unsubs.push(() => sessionStream.off('event', onEvent));
    this._unsubs.push(() => sessionStream.off('event:update', onEvent));
  }

  detach() {
    for (const unsub of this._unsubs.splice(0)) {
      try { unsub(); } catch {}
    }
    this._sessionStream = null;
  }

  linkIdentity(ctmSessionId, agentSessionId) {
    if (!agentSessionId && !ctmSessionId) return;
    if (ctmSessionId && agentSessionId) {
      this._ctmToAgent.set(ctmSessionId, agentSessionId);
      this._agentToCtm.set(agentSessionId, ctmSessionId);
      this._mergeRecord(agentSessionId, ctmSessionId);
    }
    const record = this._recordFor(ctmSessionId || agentSessionId);
    if (ctmSessionId) record.ctmSessionId = ctmSessionId;
    if (agentSessionId) record.agentSessionId = agentSessionId;
  }

  resolveAgentSessionId(sessionId) {
    return this._ctmToAgent.get(sessionId) || sessionId;
  }

  getRecentEvents(sessionId, limit = 50) {
    if (!this._sessionStream) return [];
    return this._sessionStream.getRecentEvents(this.resolveAgentSessionId(sessionId), limit);
  }

  getSummary(sessionId, turns = 3) {
    if (!this._sessionStream) return null;
    const agentId = this.resolveAgentSessionId(sessionId);
    const summary = this._sessionStream.getSummary(agentId, turns);
    if (!summary) return null;
    const projected = this.getStatus(agentId, summary.status);
    return {
      ...summary,
      status: projected.status,
      captureStatus: projected.status,
      statusEvidence: projected.evidence,
    };
  }

  subscribe(sessionId, cb) {
    if (!this._sessionStream) return () => {};
    return this._sessionStream.subscribe(this.resolveAgentSessionId(sessionId), cb);
  }

  subscribeAll(cb) {
    if (!this._sessionStream) return () => {};
    return this._sessionStream.subscribeAll(cb);
  }

  linkSession(ctmSessionId, agentSessionId, jsonlPath) {
    this.linkIdentity(ctmSessionId, agentSessionId);
    if (this._sessionStream && typeof this._sessionStream.linkSession === 'function') {
      this._sessionStream.linkSession(ctmSessionId, agentSessionId, jsonlPath);
    }
  }

  removeSession(agentSessionId) {
    const ctmSessionId = this._agentToCtm.get(agentSessionId) || null;
    this.ingestStatus({
      sessionId: agentSessionId,
      ctmSessionId,
      agentSessionId,
      status: 'exited',
      source: 'capture',
      reason: 'session-exit',
      timestamp: nowMs(),
    });
    if (this._sessionStream && typeof this._sessionStream.removeSession === 'function') {
      this._sessionStream.removeSession(agentSessionId);
    }
  }

  feedPtyActivity(ctmSessionId) {
    this.ingestStatus({
      sessionId: ctmSessionId,
      ctmSessionId,
      status: 'running',
      source: 'pty-activity',
      reason: 'pty-activity',
      timestamp: nowMs(),
    });
    if (this._sessionStream && typeof this._sessionStream.feedPtyActivity === 'function') {
      this._sessionStream.feedPtyActivity(ctmSessionId);
    }
  }

  ingestServerMessage(msg) {
    const evidence = statusFromServerMessage(msg);
    if (!evidence) return null;
    const projected = this.ingestStatus(evidence);
    this.emit('capture-event', {
      schemaVersion: 1,
      eventId: `server:${msg.type}:${evidence.sessionId || evidence.ctmSessionId}:${evidence.timestamp || nowMs()}`,
      captureKind: 'lifecycle',
      type: msg.type,
      source: evidence.source,
      sessionId: evidence.agentSessionId || evidence.sessionId,
      ctmSessionId: evidence.ctmSessionId || evidence.sessionId,
      agentSessionId: evidence.agentSessionId || null,
      timestamp: evidence.timestamp || nowMs(),
      status: projected.status,
      data: msg,
    });
    return projected;
  }

  ingestStreamStatus(statusEvt = {}) {
    return this.ingestStatus({
      sessionId: statusEvt.sessionId,
      ctmSessionId: statusEvt.ctmSessionId || null,
      agentSessionId: statusEvt.sessionId || null,
      status: statusEvt.newStatus || statusEvt.status,
      source: 'stream',
      reason: statusEvt.reason || 'stream-status',
      timestamp: statusEvt.timestamp,
      lastActivity: statusEvt.lastActivity,
      lastPtyActivity: statusEvt.lastPtyActivity,
      lastJsonlActivity: statusEvt.lastJsonlActivity,
      eventCount: statusEvt.eventCount,
    });
  }

  ingestStatus(input = {}) {
    const status = normalizeStatus(input.status);
    if (status === 'unknown') return this.getStatus(input.sessionId || input.ctmSessionId || input.agentSessionId, status);
    this.linkIdentity(input.ctmSessionId, input.agentSessionId || input.sessionId);
    const record = this._recordFor(input.ctmSessionId || input.sessionId || input.agentSessionId);
    const timestamp = toMs(input.timestamp);
    if (input.ctmSessionId) record.ctmSessionId = input.ctmSessionId;
    if (input.agentSessionId) record.agentSessionId = input.agentSessionId;
    if (!record.agentSessionId && input.sessionId && this._agentToCtm.has(input.sessionId)) record.agentSessionId = input.sessionId;
    if (!record.ctmSessionId && input.sessionId && this._ctmToAgent.has(input.sessionId)) record.ctmSessionId = input.sessionId;

    if (status === 'running' || status === 'exited') {
      delete record.evidence['waiting-for-input'];
      delete record.evidence.approval;
    }
    if (status === 'exited') {
      record.evidence = {};
    }

    record.evidence[input.source || 'capture'] = {
      status,
      source: input.source || 'capture',
      reason: input.reason || '',
      timestamp,
      decisionId: input.decisionId,
    };
    record.lastUpdated = Math.max(record.lastUpdated || 0, timestamp);
    const activityMs = explicitActivityMs(input) || (isActivityBearingStatus(input) ? timestamp : 0);
    if (activityMs) {
      record.lastActivity = Math.max(record.lastActivity || 0, activityMs);
    }
    if (input.eventCount != null) record.eventCount = input.eventCount;

    const projected = this._project(record);
    this.emit('status', projected);
    return projected;
  }

  getStatus(sessionId, fallbackStatus = 'unknown') {
    const record = this._records.get(this._canonicalId(sessionId));
    if (!record) {
      return {
        sessionId,
        ctmSessionId: this._agentToCtm.get(sessionId) || sessionId || null,
        agentSessionId: this._ctmToAgent.get(sessionId) || sessionId || null,
        status: normalizeStatus(fallbackStatus),
        evidence: [],
      };
    }
    return this._project(record, fallbackStatus);
  }

  getAllStatuses() {
    const results = [];
    const seen = new Set();
    if (this._sessionStream && typeof this._sessionStream.getAllStatuses === 'function') {
      for (const st of this._sessionStream.getAllStatuses()) {
        this.ingestStreamStatus({ ...st, timestamp: nowMs(), reason: 'snapshot' });
        const projected = this.getStatus(st.sessionId, st.status);
        const row = {
          ...st,
          status: projected.status,
          captureStatus: projected.status,
          statusEvidence: projected.evidence,
        };
        results.push(row);
        seen.add(this._canonicalId(st.sessionId));
      }
    }

    for (const [id, record] of this._records) {
      if (seen.has(id)) continue;
      const projected = this._project(record);
      if (projected.status === 'unknown') continue;
      results.push({
        sessionId: record.agentSessionId || id,
        ctmSessionId: record.ctmSessionId || null,
        status: projected.status,
        captureStatus: projected.status,
        lastActivity: record.lastActivity || 0,
        eventCount: record.eventCount || 0,
        statusEvidence: projected.evidence,
      });
    }
    return results;
  }

  _canonicalId(sessionId) {
    if (!sessionId) return '';
    return this._agentToCtm.get(sessionId) || sessionId;
  }

  _recordFor(sessionId) {
    const id = this._canonicalId(sessionId);
    let record = this._records.get(id);
    if (!record) {
      record = {
        sessionId: id,
        ctmSessionId: this._agentToCtm.get(sessionId) || sessionId || null,
        agentSessionId: this._ctmToAgent.get(sessionId) || null,
        evidence: {},
        lastUpdated: 0,
        lastActivity: 0,
        eventCount: 0,
      };
      this._records.set(id, record);
    }
    return record;
  }

  _mergeRecord(fromId, toId) {
    if (!fromId || !toId || fromId === toId) return;
    const fromKey = this._records.has(fromId) ? fromId : this._canonicalId(fromId);
    const toKey = this._canonicalId(toId);
    if (fromKey === toKey) return;
    const from = this._records.get(fromKey);
    if (!from) return;
    const to = this._recordFor(toKey);
    to.ctmSessionId = to.ctmSessionId || toKey;
    to.agentSessionId = to.agentSessionId || from.agentSessionId || fromId;
    to.lastUpdated = Math.max(to.lastUpdated || 0, from.lastUpdated || 0);
    to.lastActivity = Math.max(to.lastActivity || 0, from.lastActivity || 0);
    to.eventCount = Math.max(to.eventCount || 0, from.eventCount || 0);
    for (const [source, ev] of Object.entries(from.evidence || {})) {
      if (!to.evidence[source] || (ev.timestamp || 0) >= (to.evidence[source].timestamp || 0)) {
        to.evidence[source] = ev;
      }
    }
    this._records.delete(fromKey);
  }

  _project(record, fallbackStatus = 'unknown') {
    const evidence = Object.values(record.evidence || {})
      .filter(Boolean)
      .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
    const latest = evidence[0] || null;
    const waiting = evidence.find((ev) => ev.status === 'waiting' && (nowMs() - (ev.timestamp || 0)) < WAITING_TTL_MS);
    const exited = evidence.find((ev) => ev.status === 'exited');
    let status = normalizeStatus(fallbackStatus);
    if (latest) status = latest.status;
    if (waiting && (!latest || waiting.timestamp >= latest.timestamp || latest.status !== 'running')) status = 'waiting';
    if (exited && (!latest || exited.timestamp >= latest.timestamp)) status = 'exited';
    return {
      sessionId: record.agentSessionId || record.sessionId,
      ctmSessionId: record.ctmSessionId || null,
      agentSessionId: record.agentSessionId || null,
      status: normalizeStatus(status),
      lastActivity: record.lastActivity || 0,
      evidence: evidence.map((ev) => ({
        source: ev.source,
        status: ev.status,
        reason: ev.reason,
        timestamp: ev.timestamp,
      })),
    };
  }
}

module.exports = {
  SessionCapture,
  normalizeStatus,
  statusFromServerMessage,
};
