'use strict';

const crypto = require('node:crypto');
const os = require('node:os');
const { WorkspaceReplay } = require('./workspace-replay');
const { createLaneEvent } = require('./lane-events');

const VERSION = 'walle-jsonl-v1';

class CodingTranscript {
  constructor({
    sessionId,
    replay = null,
    cwd = '',
    chatSessionId = '',
    label = '',
    modelId = '',
    modelProvider = '',
    runtimeMode = '',
    agentKind = '',
    agentMode = '',
    agent = '',
    mode = '',
    now = () => new Date().toISOString(),
  } = {}) {
    if (!sessionId) throw new Error('sessionId is required');
    this.sessionId = sessionId;
    this.replay = replay || new WorkspaceReplay({ now });
    this.defaults = { cwd, chatSessionId, label, modelId, modelProvider, runtimeMode, agentKind, agentMode, agent, mode };
    this.now = now;
    this._sessions = new Set();
  }

  ensureSession(opts = {}) {
    const sessionId = opts.sessionId || this.sessionId;
    if (this._sessions.has(sessionId)) return null;
    this._sessions.add(sessionId);
    return this.replay.appendRecord(sessionId, {
      type: 'session_meta',
      provider: 'walle',
      version: VERSION,
      sessionId,
      chatSessionId: opts.chatSessionId || this.defaults.chatSessionId || '',
      cwd: opts.cwd || this.defaults.cwd || '',
      label: opts.label || this.defaults.label || '',
      modelId: opts.modelId || this.defaults.modelId || '',
      modelProvider: opts.modelProvider || this.defaults.modelProvider || '',
      runtimeMode: opts.runtimeMode || this.defaults.runtimeMode || '',
      agentKind: opts.agentKind || this.defaults.agentKind || '',
      agentMode: opts.agentMode || this.defaults.agentMode || '',
      agent: opts.agent || this.defaults.agent || '',
      mode: opts.mode || this.defaults.mode || '',
      hostname: os.hostname(),
      uuid: newUuid(),
      timestamp: opts.timestamp || this.now(),
    });
  }

  append(record = {}) {
    const sessionId = record.sessionId || this.sessionId;
    this.ensureSession({ sessionId, cwd: record.cwd || this.defaults.cwd });
    return this.replay.appendRecord(sessionId, normalizeRecord({
      sessionId,
      cwd: this.defaults.cwd,
      timestamp: this.now(),
      ...record,
    }));
  }

  appendPart(arg, partType, data, opts = {}) {
    if (arguments.length === 1 && arg && typeof arg === 'object') {
      const record = arg;
      const sessionId = record.sessionId || this.sessionId;
      this.ensureSession({ sessionId, cwd: record.cwd || this.defaults.cwd });
      return this.append({
        type: 'walle_part',
        ...record,
        sessionId,
        cwd: record.cwd || this.defaults.cwd,
        chatSessionId: record.chatSessionId || this.defaults.chatSessionId,
        partType: record.partType || 'event',
        data: record.data || {},
      });
    }
    const sessionId = arg || this.sessionId;
    this.ensureSession({ sessionId, cwd: opts.cwd || this.defaults.cwd });
    return this.replay.appendPart(sessionId, partType || 'event', data || {}, {
      cwd: opts.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || null,
    });
  }

  appendUserMessage(text, opts = {}) {
    return this.append(messageRecord('user', text, opts, this));
  }

  appendAssistantMessage(text, opts = {}) {
    const record = messageRecord('assistant', text, opts, this);
    record.modelId = opts.model || opts.modelId || this.defaults.modelId || '';
    record.modelProvider = opts.provider || opts.modelProvider || this.defaults.modelProvider || '';
    record.tokens = opts.tokens || {};
    record.cost = Number.isFinite(opts.cost) ? opts.cost : null;
    return this.append(record);
  }

  appendToolCall(event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'tool_call',
      data: normalizeToolCallEvent(event),
    });
  }

  appendToolResult(event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: event.error ? 'tool_result' : (event.partType || 'tool_result'),
      data: normalizeToolResultEvent(event),
    });
  }

  appendApprovalRequest(event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'approval_request',
      data: normalizeApprovalEvent(event, 'pending'),
    });
  }

  appendApprovalResponse(event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'approval_response',
      data: normalizeApprovalEvent(event, event.decision || event.status || 'answered'),
    });
  }

  appendCompaction(event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'compaction',
      data: {
        type: 'compaction',
        summary: String(event.summary || ''),
        sourceRecordIds: Array.isArray(event.sourceRecordIds) ? event.sourceRecordIds.map(String) : [],
        tokensBefore: numberOrNull(event.tokensBefore),
        tokensAfter: numberOrNull(event.tokensAfter),
        reason: String(event.reason || ''),
      },
    });
  }

  appendSkillEvent(event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'skill_event',
      data: {
        type: 'skill_event',
        name: String(event.name || event.skill || ''),
        skill: String(event.skill || event.name || ''),
        action: String(event.action || ''),
        status: String(event.status || event.state || ''),
        source: String(event.source || ''),
        details: safeJsonValue(event.details || {}),
      },
    });
  }

  appendRuntimeError(error = {}, opts = {}) {
    const event = error instanceof Error
      ? { message: error.message, name: error.name, stack: error.stack }
      : (error || {});
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'runtime_error',
      data: {
        type: 'runtime_error',
        name: String(event.name || 'Error'),
        message: String(event.message || event.error || ''),
        code: event.code ? String(event.code) : '',
        fatal: Boolean(event.fatal),
        stack: event.stack ? truncate(String(event.stack), 12000) : '',
        details: safeJsonValue(event.details || {}),
      },
    });
  }

  appendLaneEvent(event = {}, opts = {}) {
    const normalized = createLaneEvent({
      ...event,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      timestamp: opts.timestamp || event.timestamp,
    });
    return this.appendPart({
      ...opts,
      sessionId: normalized.sessionId,
      cwd: normalized.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: normalized.timestamp,
      partType: 'lane_event',
      data: normalized,
    });
  }

  appendRuntimeThread(event = {}, opts = {}) {
    return this.appendRuntimePart('runtime_thread', event, opts);
  }

  appendRuntimeTurn(event = {}, opts = {}) {
    return this.appendRuntimePart('runtime_turn', event, opts);
  }

  appendRuntimeItem(event = {}, opts = {}) {
    return this.appendRuntimePart('runtime_item', event, opts);
  }

  appendRuntimeEvent(event = {}, opts = {}) {
    return this.appendRuntimePart('runtime_event', event, opts);
  }

  appendRuntimePart(partType, event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType,
      data: normalizeRuntimeLedgerEvent(partType, event),
    });
  }

  appendArtifact(event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'artifact',
      data: {
        type: String(event.type || 'tool_output'),
        toolCallId: String(event.toolCallId || event.tool_call_id || ''),
        name: String(event.name || event.tool || ''),
        artifact: safeJsonValue(event.artifact || {}),
      },
    });
  }

  appendDiagnostic(event = {}, opts = {}) {
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'diagnostic',
      data: {
        type: 'diagnostic',
        source: String(event.source || 'runtime'),
        toolCallId: String(event.toolCallId || event.tool_call_id || ''),
        name: String(event.name || event.tool || ''),
        message: String(event.message || event.text || ''),
        diagnostics: safeJsonValue(event.diagnostics || []),
      },
    });
  }

  appendTurnComplete(event = {}, opts = {}) {
    const usage = event.usage || event.tokens || {};
    return this.appendPart({
      ...opts,
      sessionId: opts.sessionId || event.sessionId || this.sessionId,
      cwd: opts.cwd || event.cwd || this.defaults.cwd,
      chatSessionId: opts.chatSessionId || event.chatSessionId || this.defaults.chatSessionId,
      parentUuid: opts.parentUuid || event.parentUuid || null,
      timestamp: opts.timestamp || event.timestamp,
      partType: 'turn_complete',
      data: {
        type: 'turn_complete',
        status: String(event.status || event.stopReason || 'end_turn'),
        stopReason: String(event.stopReason || event.status || 'end_turn'),
        final: Boolean(event.final ?? true),
        text: String(event.text || event.message || ''),
        usage: safeJsonValue(usage),
        tokens: safeJsonValue(usage),
        cost: numberOrNull(event.cost),
        latencyMs: numberOrNull(event.latencyMs),
      },
    });
  }
}

function createCodingTranscript(opts = {}) {
  if (opts.transcript) return opts.transcript;
  if (opts.persistTranscript === false) return null;
  return new CodingTranscript(opts);
}

function normalizeRecord(record = {}) {
  return {
    provider: 'walle',
    version: VERSION,
    uuid: record.uuid || newUuid(),
    timestamp: record.timestamp || new Date().toISOString(),
    ...record,
  };
}

function messageRecord(role, text, opts = {}, transcript) {
  const sessionId = opts.sessionId || transcript.sessionId;
  return normalizeRecord({
    type: role,
    sessionId,
    chatSessionId: opts.chatSessionId || transcript.defaults.chatSessionId || '',
    parentUuid: opts.parentUuid || null,
    cwd: opts.cwd || transcript.defaults.cwd || '',
    message: {
      role,
      content: [{ type: 'text', text: String(text || '') }],
    },
  });
}

function normalizeToolCallEvent(event = {}) {
  const id = String(event.toolCallId || event.tool_call_id || event.id || newUuid());
  const name = String(event.name || event.tool || 'tool');
  const input = safeJsonValue(event.input ?? event.args ?? event.arguments ?? {});
  const status = String(event.status || event.state || 'working');
  return {
    type: 'tool_call',
    id,
    toolCallId: id,
    tool_call_id: id,
    name,
    tool: name,
    title: String(event.title || event.summary || name),
    kind: String(event.kind || 'other'),
    status,
    state: status,
    summary: String(event.summary || ''),
    input,
    args: input,
  };
}

function normalizeToolResultEvent(event = {}) {
  const id = String(event.toolCallId || event.tool_call_id || event.id || '');
  const name = String(event.name || event.tool || 'tool');
  const result = safeJsonValue(event.result ?? event.output ?? '');
  const hasError = Boolean(event.error);
  const status = String(event.status || event.state || (hasError ? 'error' : 'done'));
  return {
    type: 'tool_result',
    id,
    toolCallId: id,
    tool_call_id: id,
    name,
    tool: name,
    status,
    state: status,
    summary: String(event.summary || ''),
    result,
    output: typeof result === 'string' ? truncate(result, 20000) : truncate(JSON.stringify(result), 20000),
    error: hasError ? safeError(event.error) : null,
    durationMs: numberOrNull(event.durationMs),
  };
}

function normalizeApprovalEvent(event = {}, fallbackStatus) {
  const id = String(event.approvalId || event.permissionId || event.id || newUuid());
  const toolCallId = String(event.toolCallId || event.tool_call_id || '');
  return {
    type: event.decision ? 'approval_response' : 'approval_request',
    id,
    approvalId: id,
    permissionId: String(event.permissionId || id),
    toolCallId,
    tool_call_id: toolCallId,
    tool: String(event.tool || event.name || ''),
    command: String(event.command || ''),
    reason: String(event.reason || ''),
    message: String(event.message || ''),
    decision: event.decision ? String(event.decision) : '',
    status: String(event.status || event.state || fallbackStatus || ''),
    state: String(event.state || event.status || fallbackStatus || ''),
    source: String(event.source || ''),
    options: Array.isArray(event.options) ? safeJsonValue(event.options) : [],
  };
}

function normalizeRuntimeLedgerEvent(partType, event = {}) {
  const kind = String(event.kind || partType.replace(/^runtime_/, '') || 'event');
  const status = String(event.status || event.state || defaultRuntimeStatus(kind));
  const threadId = String(event.threadId || event.thread_id || event.sessionId || '');
  const turnId = event.turnId || event.turn_id ? String(event.turnId || event.turn_id) : '';
  const itemId = event.itemId || event.item_id ? String(event.itemId || event.item_id) : '';
  const eventId = event.eventId || event.event_id || event.id || newUuid();
  const name = String(event.name || event.event || status || 'updated');
  return {
    type: String(event.type || `runtime.${kind}.${name}`),
    kind,
    status,
    state: status,
    eventId,
    threadId,
    turnId,
    itemId,
    parentItemId: event.parentItemId || event.parent_item_id ? String(event.parentItemId || event.parent_item_id) : '',
    role: String(event.role || ''),
    name,
    summary: String(event.summary || ''),
    provenance: String(event.provenance || 'live'),
    data: safeJsonValue(event.data ?? event.details ?? {}),
  };
}

function defaultRuntimeStatus(kind) {
  if (kind === 'thread') return 'created';
  if (kind === 'turn' || kind === 'item') return 'in_progress';
  return 'recorded';
}

function safeJsonValue(value) {
  if (value === undefined) return null;
  if (value instanceof Error) return safeError(value);
  try {
    return JSON.parse(JSON.stringify(value));
  } catch {
    return String(value);
  }
}

function safeError(error) {
  if (!error) return null;
  if (error instanceof Error) {
    return {
      name: error.name,
      message: error.message,
      stack: error.stack ? truncate(error.stack, 12000) : '',
    };
  }
  if (typeof error === 'object') {
    return safeJsonValue(error);
  }
  return { message: String(error) };
}

function numberOrNull(value) {
  const n = Number(value);
  return Number.isFinite(n) ? n : null;
}

function truncate(value, max) {
  const text = String(value || '');
  if (!max || text.length <= max) return text;
  return `${text.slice(0, Math.max(0, max - 30))}\n[transcript value truncated]`;
}

function newUuid() {
  return crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
}

module.exports = {
  VERSION,
  CodingTranscript,
  createCodingTranscript,
  normalizeRecord,
  normalizeToolCallEvent,
  normalizeToolResultEvent,
  normalizeRuntimeLedgerEvent,
};
