#!/usr/bin/env node
// otel-export.mjs — Stop-event hook (CC v2.1.105+)
//
// Reads ${CLAUDE_PLUGIN_DATA}/trek*-stats.jsonl, applies field-allowlist,
// and exports either Prometheus textfile (default off) or OTLP/HTTP.
//
// Strict opt-in via VOYAGE_EXPORT_MODE env-var:
//   off       (default) — silent exit, no work done
//   textfile  — write voyage.prom to VOYAGE_TEXTFILE_DIR or CLAUDE_PLUGIN_DATA
//   otlp      — POST OTLP/JSON to VOYAGE_OTEL_ENDPOINT (https required for non-private)
//
// Hard invariants:
//   - Outer try/catch with process.exit(0). Stats failures MUST NOT block Stop.
//   - Tail-latency NFR: textfile <5ms p99, otlp <1500ms (AbortController).
//   - Allowlist redaction MUST happen before any export (CWE-212).
//   - Path / endpoint validation MUST happen before any I/O (CWE-22, CWE-918).
//   - All stderr prefixed with [voyage].
//   - EXDEV mitigation: tmp file in same dir as target (do NOT use atomicWriteJson).

import { readFileSync, existsSync, writeFileSync, renameSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { transformToPrometheus } from '../../lib/exporters/textfile-format.mjs';
import { transformToOtlpJson } from '../../lib/exporters/otlp-format.mjs';
import { validateTextfilePath } from '../../lib/exporters/path-validator.mjs';
import { validateOtlpEndpoint } from '../../lib/exporters/endpoint-validator.mjs';
import { applyFieldAllowlist } from '../../lib/exporters/field-allowlist.mjs';

const VALID_MODES = new Set(['off', 'textfile', 'otlp']);
const TEXTFILE_NAME = 'voyage.prom';
const TEXTFILE_TMP_NAME = '.voyage-prom.tmp';
const OTLP_TIMEOUT_MS = 1500;

// Map JSONL filename → schema_id for allowlist + exporter grouping
const STATS_FILES = [
  { file: 'trekbrief-stats.jsonl', schema: 'trekbrief' },
  { file: 'trekresearch-stats.jsonl', schema: 'trekresearch' },
  { file: 'trekplan-stats.jsonl', schema: 'trekplan' },
  { file: 'trekexecute-stats.jsonl', schema: 'trekexecute' },
  { file: 'trekreview-stats.jsonl', schema: 'trekreview' },
  { file: 'trekcontinue-stats.jsonl', schema: 'trekcontinue' },
];

function loadAndAllowlist(dataDir) {
  const out = [];
  for (const { file, schema } of STATS_FILES) {
    const path = join(dataDir, file);
    if (!existsSync(path)) continue;
    let text;
    try { text = readFileSync(path, 'utf-8'); }
    catch { continue; }
    const lines = text.split('\n').filter(l => l.trim());
    for (const line of lines) {
      let record;
      try { record = JSON.parse(line); }
      catch { continue; }

      let actualSchema = schema;
      if (schema === 'trekexecute') {
        if ('event' in record) actualSchema = 'event-emit';
        else if ('command_excerpt' in record || 'session_id' in record) actualSchema = 'post-bash-stats';
      }

      out.push(applyFieldAllowlist(record, actualSchema));
    }
  }
  return out;
}

async function exportTextfile(records, env) {
  const targetDir = env.VOYAGE_TEXTFILE_DIR || env.CLAUDE_PLUGIN_DATA;
  if (!targetDir) {
    process.stderr.write('[voyage] otel-export: textfile mode requires VOYAGE_TEXTFILE_DIR or CLAUDE_PLUGIN_DATA\n');
    return;
  }

  const targetPath = join(targetDir, TEXTFILE_NAME);
  const allowedRoots = [];
  if (env.VOYAGE_TEXTFILE_DIR) allowedRoots.push(env.VOYAGE_TEXTFILE_DIR);
  if (env.CLAUDE_PLUGIN_DATA) allowedRoots.push(env.CLAUDE_PLUGIN_DATA);
  const pathCheck = validateTextfilePath(targetPath, { allowedRoots });
  if (!pathCheck.valid) {
    process.stderr.write(`[voyage] otel-export: invalid textfile path: ${pathCheck.errors[0].message}\n`);
    return;
  }

  const text = transformToPrometheus(records);
  const tmpPath = join(dirname(targetPath), TEXTFILE_TMP_NAME);
  try {
    writeFileSync(tmpPath, text);
    renameSync(tmpPath, targetPath);
  } catch (e) {
    process.stderr.write(`[voyage] otel-export: textfile write failed: ${e.message}\n`);
  }
}

async function exportOtlp(records, env) {
  const url = env.VOYAGE_OTEL_ENDPOINT;
  if (!url) {
    process.stderr.write('[voyage] otel-export: otlp mode requires VOYAGE_OTEL_ENDPOINT\n');
    return;
  }
  const epCheck = validateOtlpEndpoint(url, { env });
  if (!epCheck.valid) {
    process.stderr.write(`[voyage] otel-export: invalid OTLP endpoint: ${epCheck.errors[0].message}\n`);
    return;
  }

  const payload = transformToOtlpJson(records);
  const body = JSON.stringify(payload);
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS);

  try {
    const res = await fetch(epCheck.parsed.url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'User-Agent': 'voyage/4.1.0' },
      body,
      signal: controller.signal,
    });
    if (!res.ok) {
      process.stderr.write(`[voyage] otel-export: OTLP endpoint returned ${res.status}\n`);
    }
  } catch (e) {
    if (e.name === 'AbortError') {
      process.stderr.write(`[voyage] otel-export: OTLP request timed out after ${OTLP_TIMEOUT_MS}ms\n`);
    } else {
      process.stderr.write(`[voyage] otel-export: OTLP send failed: ${e.message}\n`);
    }
  } finally {
    clearTimeout(timer);
  }
}

(async () => {
  try {
    const env = process.env;
    const mode = (env.VOYAGE_EXPORT_MODE || 'off').toLowerCase();

    if (mode === 'off') return;
    if (!VALID_MODES.has(mode)) {
      process.stderr.write(`[voyage] otel-export: unknown VOYAGE_EXPORT_MODE="${mode}", expected one of [${[...VALID_MODES].join(', ')}]\n`);
      return;
    }

    const dataDir = env.CLAUDE_PLUGIN_DATA;
    if (!dataDir) {
      return;
    }

    const records = loadAndAllowlist(dataDir);
    if (records.length === 0) return;

    if (mode === 'textfile') await exportTextfile(records, env);
    else if (mode === 'otlp') await exportOtlp(records, env);
  } catch (e) {
    try { process.stderr.write(`[voyage] otel-export: unexpected error: ${e.message}\n`); } catch {}
  }
  process.exit(0);
})();
