// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * Session Logger — JSONL persistence for auto engagements.
 *
 * Every auto interaction appends a JSON line to a session file.
 * Sessions survive crashes (append-only, no buffering). Each session
 * gets a unique ID and lives in ~/.nerf/sessions/.
 *
 * @module session/logger
 */

import { randomBytes } from 'node:crypto';
import { existsSync, mkdirSync, appendFileSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/** Default session directory. */
export function getSessionDir() {
  return join(homedir(), '.nerf', 'sessions');
}

// ---------------------------------------------------------------------------
// SessionEntry
// ---------------------------------------------------------------------------

/**
 * A single interaction entry in a session log.
 */
export class SessionEntry {
  /**
   * @param {object} opts
   * @param {string} opts.type - Entry type: 'start' | 'interaction' | 'handoff' | 'cost' | 'end' | 'error'
   * @param {string} [opts.sessionId]
   * @param {number} [opts.timestamp] - Unix epoch seconds
   * @param {string} [opts.mode] - Agent mode
   * @param {string} [opts.task] - Task summary
   * @param {number} [opts.turn] - Turn number in the agent loop
   * @param {number} [opts.toolCalls] - Number of tool calls this interaction
   * @param {number} [opts.tokensIn] - Input tokens
   * @param {number} [opts.tokensOut] - Output tokens
   * @param {number} [opts.costUSD] - Estimated cost in USD
   * @param {number} [opts.cumulativeCostUSD] - Running total cost
   * @param {string} [opts.status] - 'running' | 'completed' | 'failed' | 'budget_exceeded'
   * @param {string} [opts.outputSummary] - Brief output summary
   * @param {object} [opts.metadata] - Additional data
   */
  constructor(opts = {}) {
    this.type = opts.type ?? 'interaction';
    this.sessionId = opts.sessionId ?? '';
    this.timestamp = opts.timestamp ?? Date.now() / 1000;
    this.mode = opts.mode ?? '';
    this.task = opts.task ?? '';
    this.turn = opts.turn ?? 0;
    this.toolCalls = opts.toolCalls ?? 0;
    this.tokensIn = opts.tokensIn ?? 0;
    this.tokensOut = opts.tokensOut ?? 0;
    this.costUSD = opts.costUSD ?? 0;
    this.cumulativeCostUSD = opts.cumulativeCostUSD ?? 0;
    this.status = opts.status ?? 'running';
    this.outputSummary = opts.outputSummary ?? '';
    this.metadata = opts.metadata ?? {};
  }

  /** Serialize to JSON string (one line). */
  toJSON() {
    return JSON.stringify({
      type: this.type,
      sessionId: this.sessionId,
      timestamp: this.timestamp,
      mode: this.mode,
      task: this.task,
      turn: this.turn,
      toolCalls: this.toolCalls,
      tokensIn: this.tokensIn,
      tokensOut: this.tokensOut,
      costUSD: this.costUSD,
      cumulativeCostUSD: this.cumulativeCostUSD,
      status: this.status,
      outputSummary: this.outputSummary,
      metadata: this.metadata,
    });
  }

  /** Deserialize from a parsed JSON object. */
  static fromObject(obj) {
    return new SessionEntry(obj);
  }
}

// ---------------------------------------------------------------------------
// Secret redaction for session logs
// ---------------------------------------------------------------------------

const SECRET_PATTERNS = [
  /sk-ant-[a-zA-Z0-9_-]{20,}/g,   // Anthropic API keys
  /sk-[a-zA-Z0-9_-]{20,}/g,        // OpenAI API keys
  /Bearer [a-zA-Z0-9._\-/+=]{20,}/gi, // Bearer tokens
  /ghp_[a-zA-Z0-9]{36}/g,          // GitHub PATs
  /ghs_[a-zA-Z0-9]{36}/g,          // GitHub App tokens
  /op:\/\/[^\s"']+/g,               // 1Password references
];

/** Redact known secret patterns from a string. */
function redactSecrets(text) {
  if (typeof text !== 'string') return text;
  let redacted = text;
  for (const pattern of SECRET_PATTERNS) {
    redacted = redacted.replace(pattern, '[REDACTED]');
  }
  return redacted;
}

// ---------------------------------------------------------------------------
// createSessionId
// ---------------------------------------------------------------------------

/**
 * Generate a unique session ID: timestamp prefix + random suffix.
 * Format: YYYYMMDD-HHMMSS-xxxxxxxx
 *
 * @returns {string}
 */
export function createSessionId() {
  const now = new Date();
  const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
  const rand = randomBytes(4).toString('hex');
  return `${ts}-${rand}`;
}

// ---------------------------------------------------------------------------
// SessionLogger
// ---------------------------------------------------------------------------

/**
 * Append-only JSONL session logger.
 */
export class SessionLogger {
  /**
   * @param {object} [opts]
   * @param {string} [opts.sessionId] - Existing session ID to continue
   * @param {string} [opts.sessionDir] - Override session directory
   * @param {string} [opts.mode] - Initial mode
   * @param {string} [opts.task] - Initial task description
   */
  constructor(opts = {}) {
    this._sessionId = opts.sessionId || createSessionId();
    this._sessionDir = opts.sessionDir || getSessionDir();
    this._mode = opts.mode || '';
    this._task = opts.task || '';
    this._cumulativeCost = 0;
    this._entryCount = 0;
    this._started = false;

    // Ensure directory exists
    mkdirSync(this._sessionDir, { recursive: true });
  }

  /** Current session ID. */
  get sessionId() { return this._sessionId; }

  /** Path to the session JSONL file. */
  get filePath() { return join(this._sessionDir, `${this._sessionId}.jsonl`); }

  /** Cumulative cost so far. */
  get cumulativeCost() { return this._cumulativeCost; }

  /** Number of entries written. */
  get entryCount() { return this._entryCount; }

  /**
   * Write the session start entry.
   * @param {object} [metadata]
   */
  start(metadata = {}) {
    if (this._started) return;
    this._started = true;

    this._append(new SessionEntry({
      type: 'start',
      sessionId: this._sessionId,
      mode: this._mode,
      task: this._task,
      status: 'running',
      metadata: { ...metadata, startedAt: new Date().toISOString() },
    }));
  }

  /**
   * Log an interaction (one agent turn).
   *
   * @param {object} opts
   * @param {string} opts.mode
   * @param {number} opts.turn
   * @param {number} opts.toolCalls
   * @param {number} opts.tokensIn
   * @param {number} opts.tokensOut
   * @param {number} opts.costUSD
   * @param {string} [opts.outputSummary]
   * @param {object} [opts.metadata]
   */
  logInteraction(opts) {
    this._cumulativeCost += opts.costUSD || 0;

    this._append(new SessionEntry({
      type: 'interaction',
      sessionId: this._sessionId,
      mode: opts.mode,
      task: this._task,
      turn: opts.turn,
      toolCalls: opts.toolCalls,
      tokensIn: opts.tokensIn,
      tokensOut: opts.tokensOut,
      costUSD: opts.costUSD || 0,
      cumulativeCostUSD: this._cumulativeCost,
      status: 'running',
      outputSummary: opts.outputSummary || '',
      metadata: opts.metadata || {},
    }));
  }

  /**
   * Log a handoff event.
   *
   * @param {string} sourceMode
   * @param {string} targetMode
   * @param {object} [metadata]
   */
  logHandoff(sourceMode, targetMode, metadata = {}) {
    this._append(new SessionEntry({
      type: 'handoff',
      sessionId: this._sessionId,
      mode: targetMode,
      status: 'running',
      metadata: { sourceMode, targetMode, ...metadata },
    }));
  }

  /**
   * Log a cost entry (budget check).
   *
   * @param {number} costUSD
   * @param {number} budgetUSD
   * @param {string} status - 'ok' | 'warning' | 'exceeded'
   */
  logCost(costUSD, budgetUSD, status) {
    this._append(new SessionEntry({
      type: 'cost',
      sessionId: this._sessionId,
      costUSD,
      cumulativeCostUSD: this._cumulativeCost,
      status,
      metadata: { budgetUSD, remaining: budgetUSD - this._cumulativeCost },
    }));
  }

  /**
   * Write the session end entry.
   *
   * @param {string} status - 'completed' | 'failed' | 'budget_exceeded' | 'interrupted'
   * @param {object} [metadata]
   */
  end(status = 'completed', metadata = {}) {
    this._append(new SessionEntry({
      type: 'end',
      sessionId: this._sessionId,
      status,
      cumulativeCostUSD: this._cumulativeCost,
      metadata: { ...metadata, endedAt: new Date().toISOString() },
    }));
  }

  // -- internal -----------------------------------------------------------

  /**
   * Append a SessionEntry to the JSONL file.
   * @param {SessionEntry} entry
   */
  _append(entry) {
    const raw = entry.toJSON();
    const redacted = redactSecrets(raw);
    appendFileSync(this.filePath, redacted + '\n', { encoding: 'utf-8', mode: 0o600 });
    this._entryCount += 1;
  }
}

// ---------------------------------------------------------------------------
// Session loading and listing
// ---------------------------------------------------------------------------

/**
 * Load a session from its JSONL file.
 *
 * @param {string} sessionId
 * @param {string} [sessionDir]
 * @returns {{ entries: SessionEntry[], metadata: object } | null}
 */
export function loadSession(sessionId, sessionDir) {
  const dir = sessionDir || getSessionDir();
  const filePath = join(dir, `${sessionId}.jsonl`);

  if (!existsSync(filePath)) return null;

  const lines = readFileSync(filePath, 'utf-8').trim().split('\n').filter(Boolean);
  const entries = lines.map(line => {
    try {
      return SessionEntry.fromObject(JSON.parse(line));
    } catch {
      return null;
    }
  }).filter(Boolean);

  if (entries.length === 0) return null;

  // Extract metadata from start entry
  const startEntry = entries.find(e => e.type === 'start');
  const endEntry = [...entries].reverse().find(e => e.type === 'end');
  const lastEntry = entries[entries.length - 1];

  return {
    entries,
    metadata: {
      sessionId,
      mode: startEntry?.mode || '',
      task: startEntry?.task || '',
      startedAt: startEntry?.metadata?.startedAt || '',
      endedAt: endEntry?.metadata?.endedAt || '',
      status: endEntry?.status || lastEntry?.status || 'interrupted',
      totalTokensIn: entries.reduce((s, e) => s + (e.tokensIn || 0), 0),
      totalTokensOut: entries.reduce((s, e) => s + (e.tokensOut || 0), 0),
      totalCostUSD: lastEntry?.cumulativeCostUSD || 0,
      interactionCount: entries.filter(e => e.type === 'interaction').length,
      entryCount: entries.length,
    },
  };
}

/**
 * List recent sessions from the session directory.
 *
 * @param {object} [opts]
 * @param {number} [opts.limit=20] - Max sessions to return
 * @param {string} [opts.sessionDir] - Override session directory
 * @returns {Array<object>} Session metadata sorted by most recent first
 */
export function listSessions(opts = {}) {
  const dir = opts.sessionDir || getSessionDir();
  const limit = opts.limit ?? 20;

  if (!existsSync(dir)) return [];

  const files = readdirSync(dir)
    .filter(f => f.endsWith('.jsonl'))
    .map(f => {
      const filePath = join(dir, f);
      const stat = statSync(filePath);
      return { name: f, mtime: stat.mtimeMs };
    })
    .sort((a, b) => b.mtime - a.mtime)
    .slice(0, limit);

  return files.map(f => {
    const sessionId = f.name.replace('.jsonl', '');
    const session = loadSession(sessionId, dir);
    if (!session) return null;
    return session.metadata;
  }).filter(Boolean);
}

// ---------------------------------------------------------------------------
// Session purge — clean up old session logs
// ---------------------------------------------------------------------------

/**
 * Purge session logs older than the specified number of days.
 *
 * @param {object} [opts]
 * @param {number} [opts.olderThanDays=7] - Delete sessions older than this
 * @param {string} [opts.sessionDir] - Override session directory
 * @returns {{ deleted: number, kept: number }}
 */
export function purgeSessions(opts = {}) {
  const days = opts.olderThanDays ?? 7;
  const dir = opts.sessionDir ?? getSessionDir();
  if (!existsSync(dir)) return { deleted: 0, kept: 0 };

  const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
  let deleted = 0;
  let kept = 0;

  for (const entry of readdirSync(dir)) {
    if (!entry.endsWith('.jsonl')) continue;
    const fullPath = join(dir, entry);
    try {
      const stat = statSync(fullPath);
      if (stat.mtimeMs < cutoff) {
        unlinkSync(fullPath);
        deleted++;
      } else {
        kept++;
      }
    } catch { kept++; }
  }

  return { deleted, kept };
}
