'use strict';

const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn, spawnSync } = require('child_process');
const { configDir, readConfig, activeConnection, captureQueryEnabled } = require('./config');

// Max chars of search query persisted on an event. Bounds payload size and
// limits how much of a query string ever leaves the machine.
const MAX_QUERY_LEN = 256;

const EVENT_VERSION = 'cielara_code_usage.v1';
const TELEMETRY_FILE = path.join(configDir(), 'telemetry.ndjson');
const REJECTED_FILE = path.join(configDir(), 'telemetry-rejected.ndjson');
const INSTALL_STATE_FILE = path.join(configDir(), 'telemetry-state.json');
const FLUSHER_PID_FILE = path.join(configDir(), '.telemetry-flusher.pid');
const MAX_BUFFER_BYTES = Number(process.env.CIELARA_TELEMETRY_MAX_BYTES || 50 * 1024 * 1024);
const MAX_BATCH_EVENTS = 500;
const MAX_BATCH_BYTES = 256 * 1024;
const EMPTY_POLLS_BEFORE_EXIT = 5;
const POLL_MS = 30_000;

let pkgVersion = '0.0.0';
try {
  pkgVersion = require('../package.json').version || pkgVersion;
} catch {}

function mkdir() {
  fs.mkdirSync(configDir(), { recursive: true, mode: 0o700 });
}

function readInstallState() {
  try {
    return JSON.parse(fs.readFileSync(INSTALL_STATE_FILE, 'utf8'));
  } catch {
    return {};
  }
}

function writeInstallState(state) {
  mkdir();
  const tmp = `${INSTALL_STATE_FILE}.${process.pid}.tmp`;
  fs.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
  fs.renameSync(tmp, INSTALL_STATE_FILE);
  try { fs.chmodSync(INSTALL_STATE_FILE, 0o600); } catch {}
}

function localInstallationID() {
  const state = readInstallState();
  if (state.installation_id) return state.installation_id;
  const id = `local-${crypto.randomBytes(12).toString('hex')}`;
  writeInstallState({ ...state, installation_id: id });
  return id;
}

function repoSalt() {
  const state = readInstallState();
  if (state.repo_salt) return state.repo_salt;
  const salt = crypto.randomBytes(16).toString('hex');
  writeInstallState({ ...state, repo_salt: salt });
  return salt;
}

function portalMetadata() {
  try {
    const { name, connection } = activeConnection(readConfig());
    return { name, connection };
  } catch {
    return { name: 'default', connection: {} };
  }
}

function installationID() {
  const { connection } = portalMetadata();
  return connection.installationID || connection.installation_id || localInstallationID();
}

function sessionID() {
  return process.env.CIELARA_SESSION_ID
    || process.env.TERM_SESSION_ID
    || `${process.ppid || process.pid}`;
}

function platformTag() {
  return `${process.platform}-${process.arch}`;
}

function latencyBucket(ms) {
  if (ms < 25) return 'lt25';
  if (ms < 50) return 'lt50';
  if (ms < 100) return 'lt100';
  return 'gte100';
}

function git(cwd, args) {
  try {
    const result = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 1000 });
    if (result.status !== 0) return '';
    return result.stdout.trim();
  } catch {
    return '';
  }
}

function repoHash(cwd) {
  const root = git(cwd, ['rev-parse', '--show-toplevel']) || cwd || process.cwd();
  const remote = git(root, ['config', '--get', 'remote.origin.url']) || root;
  return crypto.createHash('sha256').update(`${repoSalt()}:${remote}`).digest('hex').slice(0, 16);
}

// repoName returns a human-readable repo identifier (bare repo basename, e.g.
// "cielara-portal") for display in the portal usage dashboard. Derived from the
// git remote URL when available, else the working-tree directory name. Only the
// final path segment is sent — never the full URL, org, or filesystem path —
// to limit what leaves the machine.
function repoName(cwd) {
  const root = git(cwd, ['rev-parse', '--show-toplevel']) || cwd || process.cwd();
  const remote = git(root, ['config', '--get', 'remote.origin.url']);
  const source = remote || root;
  // Strip trailing slashes + ".git", take the last path/colon segment.
  const cleaned = source.replace(/\.git$/i, '').replace(/[/\\]+$/, '');
  const seg = cleaned.split(/[/\\:]/).filter(Boolean).pop() || '';
  return seg.slice(0, 128);
}

// captureQuery returns the search pattern to persist on an event, or undefined
// when capture is disabled or there is nothing to record. This is the grep
// pattern the agent ran — NOT the natural-language LLM prompt, which the
// connector never sees. Opt-out via `cielara config set capture-query off` or
// CIELARA_TELEMETRY_NO_QUERY=1. Truncated to MAX_QUERY_LEN.
function captureQuery(query) {
  if (!query || !captureQueryEnabled()) return undefined;
  const s = String(query).trim();
  if (!s) return undefined;
  return s.length > MAX_QUERY_LEN ? s.slice(0, MAX_QUERY_LEN) : s;
}

function normalizeRelationships(rel = {}) {
  return {
    callers: Number(rel.callers || rel.relationship_callers || 0) || 0,
    callees: Number(rel.callees || rel.relationship_callees || 0) || 0,
    imports: Number(rel.imports || rel.relationship_imports || 0) || 0,
  };
}

function buildEvent(input = {}) {
  const elapsed = Math.max(0, Math.round(Number(input.latencyMs ?? input.elapsedMs ?? 0) || 0));
  const relationships = normalizeRelationships(input.relationships);
  const filesSurfaced = Math.max(0, Math.round(Number(input.filesSurfaced || input.files_surfaced || 0) || 0));
  const tokens = Math.max(0, Math.round(Number(input.tokens || input.tokens_processed || 0) || 0));
  const enriched = input.enriched != null ? Boolean(input.enriched) : filesSurfaced > 0;
  const cwd = input.cwd || process.cwd();
  return {
    event_uuid: crypto.randomUUID(),
    event_version: EVENT_VERSION,
    ts: new Date().toISOString(),
    installation_id: input.installationID || installationID(),
    cmd: input.cmd || 'unknown',
    agent: input.agent || undefined,
    enriched,
    kg_hits: filesSurfaced,
    files_surfaced: filesSurfaced,
    tokens_processed: tokens,
    relationship_callers: relationships.callers,
    relationship_callees: relationships.callees,
    relationship_imports: relationships.imports,
    latency_ms: elapsed,
    latency_bucket: input.latencyBucket || latencyBucket(elapsed),
    repo_hash: input.repoHash || repoHash(cwd),
    repo_name: input.repoName || repoName(cwd),
    query: captureQuery(input.query),
    session_id: input.sessionID || sessionID(),
    cielara_code_version: pkgVersion,
    exit_code: Number.isFinite(Number(input.exitCode)) ? Number(input.exitCode) : 0,
    platform: platformTag(),
    repo_language: input.repoLanguage || undefined,
  };
}

function appendLine(file, line) {
  mkdir();
  fs.appendFileSync(file, line, { mode: 0o600 });
}

function capBuffer() {
  try {
    const st = fs.statSync(TELEMETRY_FILE);
    if (st.size <= MAX_BUFFER_BYTES) return;
    const raw = fs.readFileSync(TELEMETRY_FILE, 'utf8');
    const lines = raw.split('\n').filter(Boolean);
    const kept = [];
    let bytes = 0;
    for (let i = lines.length - 1; i >= 0; i -= 1) {
      const line = `${lines[i]}\n`;
      bytes += Buffer.byteLength(line);
      if (bytes > Math.floor(MAX_BUFFER_BYTES * 0.8)) break;
      kept.push(line);
    }
    const tmp = `${TELEMETRY_FILE}.${process.pid}.tmp`;
    fs.writeFileSync(tmp, kept.reverse().join(''), { mode: 0o600 });
    fs.renameSync(tmp, TELEMETRY_FILE);
  } catch {}
}

function isProcessAlive(pid) {
  if (!pid) return false;
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

function maybeSpawnFlusher() {
  if (process.env.CIELARA_TELEMETRY_DISABLE_FLUSH === '1') return;
  try {
    const existing = Number(fs.readFileSync(FLUSHER_PID_FILE, 'utf8'));
    if (isProcessAlive(existing)) return;
  } catch {}
  try {
    const bin = path.resolve(__dirname, '..', 'bin', 'cielara.js');
    const child = spawn(process.execPath, [bin, 'telemetry-flush'], {
      detached: true,
      stdio: 'ignore',
      env: { ...process.env, CIELARA_TELEMETRY_CHILD: '1' },
    });
    mkdir();
    fs.writeFileSync(FLUSHER_PID_FILE, String(child.pid), { mode: 0o600 });
    child.unref();
  } catch {}
}

function recordUsageEvent(input = {}, options = {}) {
  if (process.env.CIELARA_TELEMETRY_DISABLED === '1' || process.env.CIELARA_TELEMETRY_DISABLE_WRITE === '1') {
    return null;
  }
  try {
    const event = buildEvent(input);
    appendLine(TELEMETRY_FILE, `${JSON.stringify(event)}\n`);
    capBuffer();
    if (options.spawnFlusher !== false) maybeSpawnFlusher();
    return event;
  } catch {
    return null;
  }
}

function readPending() {
  try {
    return fs.readFileSync(TELEMETRY_FILE, 'utf8').split('\n').filter(Boolean);
  } catch {
    return [];
  }
}

function writePending(lines) {
  mkdir();
  const tmp = `${TELEMETRY_FILE}.${process.pid}.tmp`;
  fs.writeFileSync(tmp, lines.length ? `${lines.join('\n')}\n` : '', { mode: 0o600 });
  fs.renameSync(tmp, TELEMETRY_FILE);
}

function takeBatch(lines) {
  const batch = [];
  let bytes = 0;
  let consumed = 0;
  for (const line of lines) {
    const nextBytes = Buffer.byteLength(line) + 1;
    if (batch.length >= MAX_BATCH_EVENTS || (batch.length > 0 && bytes + nextBytes > MAX_BATCH_BYTES)) break;
    consumed += 1;
    try {
      batch.push(JSON.parse(line));
      bytes += nextBytes;
    } catch {
      appendLine(REJECTED_FILE, `${line}\n`);
    }
  }
  return { batch, consumed };
}

async function postBatch(credentials, events) {
  const portalUrl = (credentials.portalUrl || 'https://cielaracode.com').replace(/\/$/, '');
  const res = await fetch(`${portalUrl}/v1/telemetry`, {
    method: 'POST',
    headers: {
      accept: 'application/json',
      'content-type': 'application/json',
      authorization: `Bearer ${credentials.accessToken}`,
    },
    body: JSON.stringify({ events }),
    signal: AbortSignal.timeout ? AbortSignal.timeout(5000) : undefined,
  });
  const text = await res.text();
  let body = {};
  try { body = text ? JSON.parse(text) : {}; } catch {}
  return { ok: res.ok, status: res.status, body, text };
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function flushOnce() {
  const lines = readPending();
  if (lines.length === 0) return { empty: true, flushed: 0 };
  const { batch, consumed } = takeBatch(lines);
  if (batch.length === 0) {
    writePending(lines.slice(consumed));
    return { empty: true, flushed: 0 };
  }

  const { portalEnsureFresh } = require('./portal-auth');
  const { name } = portalMetadata();
  const { credentials } = await portalEnsureFresh(name);
  const result = await postBatch(credentials, batch);
  if (result.ok) {
    writePending(lines.slice(consumed));
    return { empty: false, flushed: batch.length, status: result.status };
  }
  if (result.status === 401) {
    return { empty: false, flushed: 0, status: result.status, authError: true };
  }
  if (result.status >= 400 && result.status < 500 && result.status !== 401) {
    for (const event of batch) appendLine(REJECTED_FILE, `${JSON.stringify(event)}\n`);
    writePending(lines.slice(consumed));
    return { empty: false, flushed: 0, rejected: batch.length, status: result.status };
  }
  return { empty: false, flushed: 0, status: result.status, retry: true };
}

async function flushTelemetry(options = {}) {
  const once = Boolean(options.once);
  let emptyPolls = 0;
  let backoff = 1000;
  let totalFlushed = 0;
  while (true) {
    try {
      const result = await flushOnce();
      totalFlushed += result.flushed || 0;
      if (result.authError) return { ...result, totalFlushed };
      if (once) return { ...result, totalFlushed };
      if (result.empty) {
        emptyPolls += 1;
        if (emptyPolls >= EMPTY_POLLS_BEFORE_EXIT) return { empty: true, totalFlushed };
        await sleep(POLL_MS);
      } else if (result.retry) {
        await sleep(backoff);
        backoff = Math.min(backoff * 2, 5 * 60 * 1000);
      } else {
        emptyPolls = 0;
        backoff = 1000;
      }
    } catch {
      if (once) return { error: true, totalFlushed };
      await sleep(backoff);
      backoff = Math.min(backoff * 2, 5 * 60 * 1000);
    }
  }
}

module.exports = {
  EVENT_VERSION,
  TELEMETRY_FILE,
  buildEvent,
  flushTelemetry,
  latencyBucket,
  recordUsageEvent,
};
