'use strict';

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

const { createSessionIngestService } = require('../memory/session-ingest-service');
const { collectIngestRecords } = require('../sources/base');
const { ensureBuiltinSourceAdapters } = require('../sources/builtin');
const sourceRegistry = require('../sources/registry');

const SAVE_INTERVAL = 15;
const SUPPORTED_HARNESSES = new Set(['claude-code', 'codex', 'walle']);

function getHookStateDir() {
  return process.env.WALL_E_HOOK_STATE_DIR || path.join(os.homedir(), '.walle', 'hook_state');
}

function parseHarnessInput(data = {}, harness = 'walle') {
  if (!SUPPORTED_HARNESSES.has(harness)) throw new Error(`Unsupported harness: ${harness}`);
  const transcriptPath = data.transcript_path
    || data.transcriptPath
    || data.transcript
    || data.session_file
    || data.sessionFile
    || data.path
    || data.file
    || '';
  const cwd = data.cwd || data.workspace || data.project_dir || data.projectDir || '';
  return {
    harness,
    transcriptPath,
    cwd,
    sessionId: data.session_id || data.sessionId || data.id || '',
    stopHookActive: Boolean(data.stop_hook_active || data.stopHookActive),
  };
}

function validateTranscriptPath(transcriptPath) {
  if (!transcriptPath || typeof transcriptPath !== 'string') {
    throw new Error('transcript path is required');
  }
  if (transcriptPath.split(/[\\/]+/).includes('..')) {
    throw new Error('transcript path must not contain traversal segments');
  }
  if (!transcriptPath.endsWith('.jsonl')) {
    throw new Error('transcript path must end in .jsonl');
  }
  return path.resolve(transcriptPath);
}

function adapterIdForHarness(harness, transcriptPath = '') {
  if (harness === 'codex') return 'codex-jsonl';
  if (harness === 'claude-code') return 'claude-code-jsonl';
  if (harness === 'walle') return 'walle-jsonl';
  if (transcriptPath.includes(`${path.sep}.codex${path.sep}`)) return 'codex-jsonl';
  if (transcriptPath.includes(`${path.sep}.claude${path.sep}`)) return 'claude-code-jsonl';
  return 'walle-jsonl';
}

function sourceRefFromHarness(data = {}, harness = 'walle') {
  const parsed = parseHarnessInput(data, harness);
  const sourceFile = validateTranscriptPath(parsed.transcriptPath);
  const adapterId = adapterIdForHarness(parsed.harness, sourceFile);
  return {
    adapterId,
    uri: sourceFile,
    sourceFile,
    sourceId: parsed.sessionId ? `${adapterId}:${parsed.sessionId}` : undefined,
    cwd: parsed.cwd,
    metadata: { harness: parsed.harness },
  };
}

async function countHumanMessages(sourceRef) {
  ensureBuiltinSourceAdapters();
  const adapter = sourceRegistry.instantiate(sourceRef.adapterId);
  if (!adapter) throw new Error(`Unknown source adapter: ${sourceRef.adapterId}`);
  const { records } = await collectIngestRecords(adapter, sourceRef);
  return records.filter((record) => record.type === 'memory_record' && record.role === 'user').length;
}

function readState(sourceRef) {
  const statePath = stateFilePath(sourceRef);
  try {
    return JSON.parse(fs.readFileSync(statePath, 'utf8'));
  } catch {
    return { lastHumanCount: 0 };
  }
}

function writeState(sourceRef, state) {
  const statePath = stateFilePath(sourceRef);
  fs.mkdirSync(path.dirname(statePath), { recursive: true });
  fs.writeFileSync(statePath, JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2));
}

async function handleSessionStart(data = {}, { harness = 'walle' } = {}) {
  const sourceRef = sourceRefFromHarness(data, harness);
  writeState(sourceRef, { lastHumanCount: 0, sourceRef });
  return { ok: true, action: 'session_start', sourceRef };
}

async function handleStop(data = {}, {
  harness = 'walle',
  saveInterval = SAVE_INTERVAL,
  service = null,
  background = false,
} = {}) {
  const parsed = parseHarnessInput(data, harness);
  if (parsed.stopHookActive) return { ok: true, action: 'skip_active_stop_hook' };
  const sourceRef = sourceRefFromHarness(data, harness);
  const humanCount = await countHumanMessages(sourceRef);
  const state = readState(sourceRef);
  const sinceLast = humanCount - Number(state.lastHumanCount || 0);
  if (humanCount === 0 || sinceLast < saveInterval) {
    return { ok: true, action: 'skip_interval', humanCount, sinceLast, sourceRef };
  }

  writeState(sourceRef, { ...state, lastHumanCount: humanCount, sourceRef });
  if (background && !service) {
    const spawned = spawnBackgroundIngest(sourceRef);
    return { ok: true, action: 'background_ingest', humanCount, sinceLast, pid: spawned.pid, sourceRef };
  }
  const ingest = await runLockedIngest(sourceRef, { service });
  return { ok: true, action: 'ingested', humanCount, sinceLast, ingest, sourceRef };
}

async function handlePrecompact(data = {}, { harness = 'walle', service = null } = {}) {
  const sourceRef = sourceRefFromHarness(data, harness);
  const ingest = await runLockedIngest(sourceRef, { service });
  return { ok: true, action: 'precompact_ingested', ingest, sourceRef };
}

async function runLockedIngest(sourceRef, { service = null, dryRun = false } = {}) {
  const release = acquireLock(sourceRef);
  if (!release.acquired) return { skipped: true, reason: 'lock_exists', lockPath: release.lockPath };
  try {
    const ingestService = service || createSessionIngestService();
    return await ingestService.ingestSource(sourceRef, { dryRun });
  } finally {
    release();
  }
}

function spawnBackgroundIngest(sourceRef) {
  const cli = path.join(__dirname, 'cli.js');
  const child = spawn(process.execPath, [
    cli,
    'ingest-file',
    '--adapter',
    sourceRef.adapterId,
    '--file',
    sourceRef.sourceFile,
    '--source-id',
    sourceRef.sourceId || '',
    '--cwd',
    sourceRef.cwd || '',
  ], {
    detached: true,
    stdio: 'ignore',
  });
  child.unref();
  return child;
}

function acquireLock(sourceRef) {
  const lockPath = lockFilePath(sourceRef);
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
  let fd = null;
  try {
    fd = fs.openSync(lockPath, 'wx');
    fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), sourceRef }));
  } catch (err) {
    if (err.code === 'EEXIST') {
      const skipped = () => false;
      skipped.acquired = false;
      skipped.lockPath = lockPath;
      return skipped;
    }
    throw err;
  } finally {
    if (fd != null) fs.closeSync(fd);
  }
  const release = () => {
    try { fs.unlinkSync(lockPath); } catch {}
    return true;
  };
  release.acquired = true;
  release.lockPath = lockPath;
  return release;
}

function stateFilePath(sourceRef) {
  return path.join(getHookStateDir(), `${hashSource(sourceRef)}.json`);
}

function lockFilePath(sourceRef) {
  return path.join(getHookStateDir(), `${hashSource(sourceRef)}.lock`);
}

function hashSource(sourceRef) {
  return crypto.createHash('sha256')
    .update(`${sourceRef.adapterId}:${sourceRef.sourceFile || sourceRef.uri || sourceRef.sourceId}`)
    .digest('hex')
    .slice(0, 24);
}

module.exports = {
  SAVE_INTERVAL,
  SUPPORTED_HARNESSES,
  acquireLock,
  adapterIdForHarness,
  countHumanMessages,
  getHookStateDir,
  handlePrecompact,
  handleSessionStart,
  handleStop,
  parseHarnessInput,
  runLockedIngest,
  sourceRefFromHarness,
  validateTranscriptPath,
};
