'use strict';

const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');

const SESSION_SCHEMA = 'wall-e-session-v1';
const MAX_SEGMENT_LENGTH = 180;
const LAST_EVENT_SCAN_BYTES = 4 * 1024 * 1024;

const lastUuidByFile = new Map();
const gitBranchByCwd = new Map();

function getDataDir() {
  return process.env.WALL_E_DATA_DIR || path.join(os.homedir(), '.walle', 'data');
}

function getSessionRoot() {
  return process.env.WALL_E_SESSION_DIR
    || process.env.WALLE_SESSION_DIR
    || path.join(getDataDir(), 'sessions');
}

function isSessionFilesEnabled() {
  const value = String(process.env.WALL_E_SESSION_FILES || process.env.WALLE_SESSION_FILES || '').toLowerCase();
  return value !== '0' && value !== 'false' && value !== 'off';
}

function projectSlug(cwd) {
  const resolved = path.resolve(cwd || process.cwd());
  const normalized = resolved.replace(/\\/g, '/');
  const slug = normalized
    .replace(/\//g, '-')
    .replace(/[^A-Za-z0-9._-]/g, '-')
    .replace(/-+/g, '-');
  return safeSegment(slug || 'unknown-project', 'unknown-project', 240);
}

function safeSegment(value, fallback = 'session', maxLength = MAX_SEGMENT_LENGTH) {
  const raw = String(value || fallback);
  const cleaned = raw
    .replace(/[\\/]/g, '_')
    .replace(/[^A-Za-z0-9._:-]/g, '_')
    .replace(/:+/g, '_')
    .replace(/_+/g, '_')
    .replace(/^_+|_+$/g, '')
    .slice(0, maxLength);
  return cleaned || fallback;
}

function resolveSessionFile({ sessionId, cwd, rootDir } = {}) {
  const id = sessionId || 'default';
  const baseDir = rootDir || getSessionRoot();
  const projectDir = path.join(baseDir, projectSlug(cwd));
  const fileName = `${safeSegment(id)}.jsonl`;
  return {
    rootDir: baseDir,
    projectDir,
    filePath: path.join(projectDir, fileName),
    sessionId: id,
  };
}

class WallESessionRecorder {
  constructor({ sessionId, channel = 'ctm', cwd, rootDir, version, gitBranch, metadata } = {}) {
    this.sessionId = sessionId || 'default';
    this.channel = channel || 'ctm';
    this.cwd = path.resolve(cwd || process.cwd());
    this.version = version || getWallEVersion();
    this.gitBranch = gitBranch || detectGitBranch(this.cwd);
    this.metadata = metadata || {};
    this.enabled = isSessionFilesEnabled();

    const resolved = resolveSessionFile({ sessionId: this.sessionId, cwd: this.cwd, rootDir });
    this.rootDir = resolved.rootDir;
    this.projectDir = resolved.projectDir;
    this.filePath = resolved.filePath;
    this._started = false;
    this._parentUuid = this.enabled ? (lastUuidByFile.get(this.filePath) || readLastEventUuid(this.filePath)) : null;
  }

  ensureStarted() {
    if (!this.enabled || this._started) return null;
    this._started = true;
    if (hasExistingContent(this.filePath)) return null;
    return this._appendRecord(this._baseRecord('session_start', {
      data: {
        channel: this.channel,
        cwd: this.cwd,
        metadata: this.metadata,
      },
    }));
  }

  appendMessage(role, content, extra = {}) {
    if (!this.enabled) return null;
    this.ensureStarted();
    return this._appendRecord(this._baseRecord(role, {
      ...extra,
      message: {
        role,
        content: normalizeContent(content),
        ...(extra.message && typeof extra.message === 'object' ? extra.message : {}),
      },
    }));
  }

  appendProgress(data, extra = {}) {
    if (!this.enabled) return null;
    this.ensureStarted();
    return this._appendRecord(this._baseRecord('progress', {
      ...extra,
      data: normalizeContent(data),
    }));
  }

  appendFrame(frame, extra = {}) {
    if (!this.enabled) return null;
    this.ensureStarted();
    return this._appendRecord(this._baseRecord('frame_update', {
      ...extra,
      data: normalizeContent(frame),
    }));
  }

  appendError(error, extra = {}) {
    if (!this.enabled) return null;
    this.ensureStarted();
    return this._appendRecord(this._baseRecord('error', {
      ...extra,
      error: serializeError(error),
    }));
  }

  _baseRecord(type, fields = {}) {
    const uuid = crypto.randomUUID();
    return {
      parentUuid: this._parentUuid || null,
      isSidechain: false,
      userType: 'external',
      cwd: this.cwd,
      sessionId: this.sessionId,
      version: this.version,
      gitBranch: this.gitBranch || undefined,
      type,
      timestamp: new Date().toISOString(),
      uuid,
      walle: {
        schema: SESSION_SCHEMA,
        channel: this.channel,
        projectSlug: projectSlug(this.cwd),
      },
      ...fields,
    };
  }

  _appendRecord(record) {
    fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
    fs.appendFileSync(this.filePath, JSON.stringify(record) + '\n', 'utf8');
    this._parentUuid = record.uuid;
    lastUuidByFile.set(this.filePath, record.uuid);
    return { uuid: record.uuid, filePath: this.filePath };
  }
}

function createSessionRecorder(opts) {
  return new WallESessionRecorder(opts);
}

function readSessionFile({ sessionId, cwd, rootDir, filePath } = {}) {
  const target = filePath || resolveSessionFile({ sessionId, cwd, rootDir }).filePath;
  let raw = '';
  try { raw = fs.readFileSync(target, 'utf8'); } catch {
    return { filePath: target, events: [] };
  }
  const events = [];
  for (const line of raw.split('\n')) {
    if (!line.trim()) continue;
    try { events.push(JSON.parse(line)); } catch {
      events.push({ type: 'malformed', raw: line });
    }
  }
  return { filePath: target, events };
}

function listSessionFiles({ rootDir, limit = 100 } = {}) {
  const root = rootDir || getSessionRoot();
  const files = [];
  collectJsonlFiles(root, files);
  files.sort((a, b) => b.mtimeMs - a.mtimeMs);
  return files.slice(0, Math.max(1, limit)).map((entry) => ({
    filePath: entry.filePath,
    sessionId: path.basename(entry.filePath, '.jsonl'),
    projectSlug: path.basename(path.dirname(entry.filePath)),
    size: entry.size,
    mtime: new Date(entry.mtimeMs).toISOString(),
  }));
}

function collectJsonlFiles(dir, out) {
  let entries;
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      collectJsonlFiles(fullPath, out);
      continue;
    }
    if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
    try {
      const stat = fs.statSync(fullPath);
      out.push({ filePath: fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
    } catch {}
  }
}

function hasExistingContent(filePath) {
  try { return fs.statSync(filePath).size > 0; } catch { return false; }
}

function readLastEventUuid(filePath) {
  const evt = readLastJsonObject(filePath);
  return evt && typeof evt.uuid === 'string' ? evt.uuid : null;
}

function readLastJsonObject(filePath) {
  let stat;
  try { stat = fs.statSync(filePath); } catch { return null; }
  if (!stat.size) return null;

  const fd = fs.openSync(filePath, 'r');
  try {
    const bytes = Math.min(stat.size, LAST_EVENT_SCAN_BYTES);
    const buffer = Buffer.alloc(bytes);
    fs.readSync(fd, buffer, 0, bytes, stat.size - bytes);
    const text = buffer.toString('utf8');
    const lines = text.split('\n').filter((line) => line.trim());
    for (let i = lines.length - 1; i >= 0; i--) {
      try { return JSON.parse(lines[i]); } catch {}
    }
  } finally {
    fs.closeSync(fd);
  }
  return null;
}

function normalizeContent(value) {
  if (value == null) return value;
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
  if (Array.isArray(value)) return value.map(normalizeContent);
  if (typeof value !== 'object') return String(value);

  const out = {};
  for (const [key, child] of Object.entries(value)) {
    if (key === 'data' && typeof child === 'string' && child.length > 1024 && looksLikeBase64(child)) {
      out.dataSha256 = crypto.createHash('sha256').update(child).digest('hex');
      out.dataBytes = Buffer.byteLength(child, 'utf8');
      out.data = '[base64 omitted: persisted attachment payload is stored outside the session transcript]';
      continue;
    }
    out[key] = normalizeContent(child);
  }
  return out;
}

function looksLikeBase64(value) {
  return /^[A-Za-z0-9+/=\r\n]+$/.test(value);
}

function serializeError(error) {
  if (!error) return { message: 'Unknown error' };
  return {
    name: error.name || 'Error',
    message: error.message || String(error),
    code: error.code || error.providerErrorCode || undefined,
    providerError: error.providerError || undefined,
  };
}

function detectGitBranch(cwd) {
  const key = path.resolve(cwd || process.cwd());
  if (gitBranchByCwd.has(key)) return gitBranchByCwd.get(key);
  let branch = null;
  try {
    branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
      cwd: key,
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'ignore'],
      timeout: 500,
    }).trim() || null;
  } catch {}
  gitBranchByCwd.set(key, branch);
  return branch;
}

function getWallEVersion() {
  try { return require('./package.json').version || 'unknown'; } catch { return 'unknown'; }
}

module.exports = {
  SESSION_SCHEMA,
  WallESessionRecorder,
  createSessionRecorder,
  getSessionRoot,
  isSessionFilesEnabled,
  listSessionFiles,
  projectSlug,
  readSessionFile,
  resolveSessionFile,
  safeSegment,
};
