'use strict';

const crypto = require('node:crypto');

const RUNTIME_EVENT_TYPES = new Set([
  'session_started',
  'session_finished',
  'user_prompt_submitted',
  'prompt_built',
  'model_stream_started',
  'model_stream_finished',
  'turn_started',
  'turn_finished',
  'tool_requested',
  'tool_started',
  'tool_finished',
  'tool_failed',
  'tool_blocked',
  'permission_requested',
  'permission_resolved',
  'permission_denied',
  'snapshot_captured',
  'compaction_started',
  'compaction_finished',
  'hook_started',
  'hook_finished',
  'hook_failed',
  'task_started',
  'task_updated',
  'task_finished',
  'task_failed',
  'diagnostic_recorded',
]);

const SECRET_KEY_RE = /(?:^|_|\b)(api[_-]?key|authorization|auth|cookie|credential|password|passwd|secret|token)(?:$|_|\b)/i;
const SECRET_VALUE_RE = /\b(?:sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|AKIA[0-9A-Z]{12,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\b/g;

class RuntimeEventValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'RuntimeEventValidationError';
  }
}

function createRuntimeEvent(input = {}, defaults = {}) {
  const type = String(input.type || defaults.type || '').trim();
  if (!type) throw new RuntimeEventValidationError('runtime event type is required');
  if (!RUNTIME_EVENT_TYPES.has(type)) throw new RuntimeEventValidationError(`unknown runtime event type: ${type}`);

  const sessionId = String(input.sessionId || input.session_id || defaults.sessionId || defaults.session_id || '').trim();
  if (!sessionId) throw new RuntimeEventValidationError('runtime event sessionId is required');

  const payload = redactValue(input.payload ?? input.data ?? {});
  const redactions = collectRedactions(input.payload ?? input.data ?? {});
  const event = {
    id: String(input.id || input.eventId || defaults.id || newId('evt')),
    type,
    sessionId,
    session_id: sessionId,
    agentSessionId: stringOrEmpty(input.agentSessionId || input.agent_session_id || defaults.agentSessionId || defaults.agent_session_id),
    agent_session_id: stringOrEmpty(input.agent_session_id || input.agentSessionId || defaults.agent_session_id || defaults.agentSessionId),
    turnId: stringOrEmpty(input.turnId || input.turn_id || defaults.turnId || defaults.turn_id),
    turn_id: stringOrEmpty(input.turn_id || input.turnId || defaults.turn_id || defaults.turnId),
    parentEventId: stringOrEmpty(input.parentEventId || input.parent_event_id || defaults.parentEventId || defaults.parent_event_id),
    parent_event_id: stringOrEmpty(input.parent_event_id || input.parentEventId || defaults.parent_event_id || defaults.parentEventId),
    timestamp: String(input.timestamp || defaults.timestamp || new Date().toISOString()),
    cwd: stringOrEmpty(input.cwd || defaults.cwd),
    provider: stringOrEmpty(input.provider || defaults.provider),
    model: stringOrEmpty(input.model || defaults.model),
    actor: stringOrEmpty(input.actor || defaults.actor || 'walle'),
    status: stringOrEmpty(input.status || defaults.status),
    payload,
    data: payload,
    redactions,
  };
  event.hash = eventHash(event);
  return event;
}

class RuntimeEventWriter {
  constructor({ transcript = null, events = null, now = () => new Date().toISOString(), defaults = {} } = {}) {
    this.transcript = transcript;
    this.events = events;
    this.now = now;
    this.defaults = defaults;
  }

  emit(input = {}, defaults = {}) {
    const event = createRuntimeEvent(input, {
      ...this.defaults,
      ...defaults,
      timestamp: input.timestamp || defaults.timestamp || this.now(),
    });
    this._appendTranscript(event);
    this._emitBus(event);
    return event;
  }

  child(defaults = {}) {
    return new RuntimeEventWriter({
      transcript: this.transcript,
      events: this.events,
      now: this.now,
      defaults: { ...this.defaults, ...defaults },
    });
  }

  _appendTranscript(event) {
    if (!this.transcript) return;
    const payload = {
      sessionId: event.sessionId,
      cwd: event.cwd,
      partType: 'runtime_event',
      data: {
        kind: 'event',
        eventId: event.id,
        type: event.type,
        name: event.type,
        status: event.status || 'recorded',
        threadId: event.agentSessionId || event.sessionId,
        turnId: event.turnId,
        parentItemId: event.parentEventId,
        provider: event.provider,
        model: event.model,
        provenance: 'live',
        data: event.payload,
        redactions: event.redactions,
        hash: event.hash,
      },
    };
    try {
      if (this.transcript.appendRuntimeEvent) {
        this.transcript.appendRuntimeEvent(payload.data, payload);
      } else if (this.transcript.appendPart) {
        this.transcript.appendPart(payload);
      }
    } catch (err) {
      this._emitBus(createRuntimeEvent({
        ...event,
        type: 'diagnostic_recorded',
        payload: {
          source: 'runtime_event_writer',
          message: `Failed to append runtime event: ${err.message}`,
          eventType: event.type,
        },
      }));
    }
  }

  _emitBus(event) {
    if (!this.events?.emit) return;
    try {
      this.events.emit('runtime.event', event);
      this.events.emit(`runtime.${event.type}`, event);
    } catch {}
  }
}

function redactValue(value, path = [], redactions = []) {
  if (value == null) return value;
  if (typeof value === 'string') {
    const redacted = value.replace(SECRET_VALUE_RE, '[REDACTED]');
    if (redacted !== value) redactions.push(path.join('.') || '$');
    return redacted;
  }
  if (typeof value !== 'object') return value;
  if (Array.isArray(value)) {
    return value.map((item, index) => redactValue(item, [...path, String(index)], redactions));
  }
  const out = {};
  for (const [key, raw] of Object.entries(value)) {
    const childPath = [...path, key];
    if (SECRET_KEY_RE.test(key)) {
      out[key] = '[REDACTED]';
      redactions.push(childPath.join('.'));
      continue;
    }
    out[key] = redactValue(raw, childPath, redactions);
  }
  return out;
}

function collectRedactions(value) {
  const redactions = [];
  redactValue(value, [], redactions);
  return [...new Set(redactions)];
}

function eventHash(event) {
  const stable = {
    type: event.type,
    sessionId: event.sessionId,
    agentSessionId: event.agentSessionId,
    turnId: event.turnId,
    parentEventId: event.parentEventId,
    timestamp: event.timestamp,
    actor: event.actor,
    payload: event.payload,
  };
  return `sha256:${crypto.createHash('sha256').update(stableStringify(stable)).digest('hex')}`;
}

function stableStringify(value) {
  if (value === null || typeof value !== 'object') return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
  return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
}

function newId(prefix) {
  return `${prefix}_${crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex')}`;
}

function stringOrEmpty(value) {
  return value == null ? '' : String(value);
}

module.exports = {
  RUNTIME_EVENT_TYPES,
  RuntimeEventValidationError,
  RuntimeEventWriter,
  createRuntimeEvent,
  redactValue,
  stableStringify,
};
