const fs = require('fs');
const path = require('path');
const { configDir } = require('./config');

const STATS_FILE = path.join(configDir(), 'stats.json');

// Without enrichment the agent would spend ~25% more tokens reaching the same
// context: saved = used/0.8 - used = used * 0.25. Matches the portal analytics.
const TOKEN_OVERHEAD_RATIO = 0.25;

function defaults() {
  return {
    grep_calls: 0,
    enriched_calls: 0,
    files_surfaced: 0,
    relationships: { callers: 0, callees: 0, imports: 0 },
    tokens_processed: 0,
    total_enrichment_ms: 0,
    latency_buckets: { lt25: 0, lt50: 0, lt100: 0, gte100: 0 },
    build_count: 0,
    query_count: 0,
    first_seen: null,
    last_seen: null,
  };
}

function readStats() {
  try {
    const raw = JSON.parse(fs.readFileSync(STATS_FILE, 'utf8'));
    const d = defaults();
    return {
      ...d,
      ...raw,
      relationships: { ...d.relationships, ...(raw.relationships || {}) },
      latency_buckets: { ...d.latency_buckets, ...(raw.latency_buckets || {}) },
    };
  } catch {
    const d = defaults();
    d.first_seen = new Date().toISOString();
    d.last_seen = d.first_seen;
    return d;
  }
}

function writeStats(stats) {
  const dir = configDir();
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
  const tmp = `${STATS_FILE}.tmp-${process.pid}`;
  fs.writeFileSync(tmp, `${JSON.stringify(stats, null, 2)}\n`);
  fs.renameSync(tmp, STATS_FILE);
}

function recordPostGrep({ filesSurfaced, relationships, elapsedMs, tokens }) {
  const s = readStats();
  s.grep_calls += 1;
  if (filesSurfaced > 0) {
    s.enriched_calls += 1;
    s.files_surfaced += filesSurfaced;
    s.tokens_processed += Math.max(0, Math.round(Number(tokens) || 0));
    if (relationships) {
      s.relationships.callers += (relationships.callers || 0);
      s.relationships.callees += (relationships.callees || 0);
      s.relationships.imports += (relationships.imports || 0);
    }
  }
  s.total_enrichment_ms += elapsedMs;
  if (elapsedMs < 25) s.latency_buckets.lt25 += 1;
  else if (elapsedMs < 50) s.latency_buckets.lt50 += 1;
  else if (elapsedMs < 100) s.latency_buckets.lt100 += 1;
  else s.latency_buckets.gte100 += 1;
  s.last_seen = new Date().toISOString();
  if (!s.first_seen) s.first_seen = s.last_seen;
  writeStats(s);
}

function recordBuild() {
  const s = readStats();
  s.build_count += 1;
  s.last_seen = new Date().toISOString();
  writeStats(s);
}

function recordQuery() {
  const s = readStats();
  s.query_count += 1;
  s.last_seen = new Date().toISOString();
  writeStats(s);
}

// ── Display helpers ────────────────────────────────────────────────────────

function pct(n, d) {
  if (d === 0) return '0%';
  return Math.round((n / d) * 100) + '%';
}

function fmt(n) {
  return n.toLocaleString();
}

function approxP95(buckets) {
  const total = buckets.lt25 + buckets.lt50 + buckets.lt100 + buckets.gte100;
  if (total === 0) return '\u2014';
  const t = total * 0.95;
  let cum = buckets.lt25;
  if (cum >= t) return '<25ms';
  cum += buckets.lt50;
  if (cum >= t) return '<50ms';
  cum += buckets.lt100;
  if (cum >= t) return '<100ms';
  return '>100ms';
}

function renderStats(s) {
  const since = s.first_seen ? new Date(s.first_seen).toLocaleDateString() : 'unknown';
  // `_avgMs` is set when the data came from the portal (which reports an
  // average directly); otherwise derive it from the local accumulator.
  const avgMs = typeof s._avgMs === 'number'
    ? s._avgMs
    : (s.grep_calls > 0 ? Math.round(s.total_enrichment_ms / s.grep_calls) : 0);
  const p95 = approxP95(s.latency_buckets);
  const totalRel = s.relationships.callers + s.relationships.callees + s.relationships.imports;
  const tokensSaved = Math.round((s.tokens_processed || 0) * TOKEN_OVERHEAD_RATIO);

  const w = process.stdout.write.bind(process.stdout);
  const THIN = '\u2500'.repeat(58);

  const enrichPct = pct(s.enriched_calls, s.grep_calls);
  w(`\n\x1b[1;36mCielara Code  \u00b7  \x1b[1;33m${enrichPct}\x1b[1;36m searches enriched  \u00b7  ${fmt(s.files_surfaced)} files added  \u00b7  ~${fmt(tokensSaved)} tokens saved\x1b[0m\n`);
  w(`\n`);
  w(`${s._source === 'portal' ? 'portal usage' : 'local usage'} since ${since}\n`);
  w(`${THIN}\n\n`);

  w(`  ${fmt(s.enriched_calls)} out of ${fmt(s.grep_calls)} (${enrichPct}) searches had related files added\n`);
  w(`  ${fmt(s.files_surfaced)} files added to context across all searches\n`);
  if (s.tokens_processed > 0) {
    w(`  ~${fmt(tokensSaved)} tokens saved (~25% vs no enrichment)\n`);
  }
  w(`\n`);

  if (totalRel > 0) {
    w(`  How files were related\n`);
    if (s.relationships.callers > 0)
      w(`    ${pct(s.relationships.callers, totalRel).padStart(4)}  callers of matched file\n`);
    if (s.relationships.callees > 0)
      w(`    ${pct(s.relationships.callees, totalRel).padStart(4)}  callees of matched file\n`);
    if (s.relationships.imports > 0)
      w(`    ${pct(s.relationships.imports, totalRel).padStart(4)}  imports or dependencies\n`);
    w(`\n`);
  }

  // Portal analytics reports only an average; local data has buckets for p95.
  w(`  Latency   avg ${avgMs}ms${s._source === 'portal' ? '' : `   p95 ${p95}`}\n`);
  w(`\n`);
}

// Map the portal analytics payload (TelemetryPayload: total_events,
// enriched_events, files_surfaced, tokens_processed, avg_latency_ms, since)
// into the local stats shape renderStats consumes. The portal aggregate has no
// relationship breakdown or latency buckets, so those render as omitted.
function fromPortalAnalytics(p) {
  const grep = Number(p.total_events) || 0;
  return {
    grep_calls: grep,
    enriched_calls: Number(p.enriched_events) || 0,
    files_surfaced: Number(p.files_surfaced) || 0,
    relationships: { callers: 0, callees: 0, imports: 0 },
    tokens_processed: Number(p.tokens_processed) || 0,
    total_enrichment_ms: 0,
    latency_buckets: { lt25: 0, lt50: 0, lt100: 0, gte100: 0 },
    build_count: 0,
    query_count: 0,
    first_seen: p.since || null,
    last_seen: null,
    _avgMs: Math.round(Number(p.avg_latency_ms) || 0),
    _source: 'portal',
  };
}

// Fetch the user's usage aggregate from the portal. Returns null on any failure
// (not logged in, offline, non-200) so the caller can fall back to local stats.
async function fetchPortalStats() {
  if (typeof fetch !== 'function') return null;
  let credentials;
  try {
    const { portalEnsureFresh } = require('./portal-auth');
    ({ credentials } = await portalEnsureFresh('default'));
  } catch {
    return null;
  }
  const portalUrl = String(credentials?.portalUrl || '').replace(/\/$/, '');
  const token = credentials?.accessToken;
  if (!portalUrl || !token) return null;
  try {
    const res = await fetch(`${portalUrl}/v1/products/cielara-code/analytics`, {
      method: 'GET',
      headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
      signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) return null;
    return fromPortalAnalytics(await res.json());
  } catch {
    return null;
  }
}

// Prefer the portal aggregate (now that grep enrichment is recorded by the
// sidecar and posted to the portal directly); fall back to the local stats.json
// when offline or not logged in, so `cielara stats` still works.
async function printStats() {
  const portal = await fetchPortalStats();
  renderStats(portal || readStats());
}

module.exports = { recordPostGrep, recordBuild, recordQuery, printStats, fromPortalAnalytics };
