/**
 * Telemetry — aggregate counters ONLY.
 *
 * This module is deliberately, auditably minimal. It stores six integers
 * (and the same six bucketed by calendar day for a trend line) and NOTHING
 * else. There is no field anywhere in this file for an IP address, a
 * User-Agent string, a session, a prompt, a token count, a duration, a
 * compute cost, or any per-visitor identifier. The privacy policy is a
 * promise the code keeps by construction: what is not represented here
 * cannot be collected.
 *
 * Bot classification inspects the User-Agent header in memory for the life
 * of one request and discards it immediately; only the resulting +1 to
 * either `hits` or `botHits` survives.
 */
import fs from 'node:fs';
import path from 'node:path';

export interface Counters {
  hits: number;             // human page views
  botHits: number;          // automated-crawler page views
  pluginDownloads: number;  // extension .zip downloads
  runsExecuted: number;     // pipeline runs started
  runsSucceeded: number;    // runs that reached a green complete
  runsFailed: number;       // runs that terminated failed
}

const KEYS: (keyof Counters)[] = [
  'hits', 'botHits', 'pluginDownloads', 'runsExecuted', 'runsSucceeded', 'runsFailed',
];

const empty = (): Counters => ({
  hits: 0, botHits: 0, pluginDownloads: 0, runsExecuted: 0, runsSucceeded: 0, runsFailed: 0,
});

// Conservative: anything that self-identifies as automation, plus the
// no-User-Agent case, is a bot. We would rather under-count "humans".
const BOT_RE = /bot|crawl|spider|slurp|bingpreview|facebookexternalhit|embedly|quora|pinterest|slackbot|vkshare|w3c_validator|whatsapp|telegram|discord|googlebot|semrush|ahrefs|mj12|dotbot|petalbot|headless|phantom|puppeteer|playwright|python-requests|python-httpx|curl|wget|libwww|go-http-client|okhttp|axios|node-fetch|httpclient|scrapy/i;

export function isBot(ua: string | undefined): boolean {
  if (!ua || !ua.trim()) return true;
  return BOT_RE.test(ua);
}

interface Persisted {
  totals: Counters;
  daily: Record<string, Counters>; // 'YYYY-MM-DD' -> counters
}

const DAILY_RETENTION_DAYS = 120;

export class Telemetry {
  private file: string;
  private totals: Counters = empty();
  private daily: Record<string, Counters> = {};
  private dirty = false;

  constructor(dataDir: string) {
    fs.mkdirSync(dataDir, { recursive: true });
    this.file = path.join(dataDir, 'telemetry.json');
    this.load();
    const t = setInterval(() => this.flush(), 5_000);
    t.unref();
  }

  private load(): void {
    try {
      const raw = JSON.parse(fs.readFileSync(this.file, 'utf8')) as Persisted;
      this.totals = { ...empty(), ...raw.totals };
      this.daily = raw.daily ?? {};
    } catch {
      /* first run — start clean */
    }
  }

  private flush(): void {
    if (!this.dirty) return;
    this.prune();
    try {
      fs.writeFileSync(this.file, JSON.stringify({ totals: this.totals, daily: this.daily }, null, 2));
      this.dirty = false;
    } catch {
      /* best effort; never crash a request over a counter write */
    }
  }

  private prune(): void {
    const cutoff = new Date(Date.now() - DAILY_RETENTION_DAYS * 86_400_000).toISOString().slice(0, 10);
    for (const day of Object.keys(this.daily)) {
      if (day < cutoff) delete this.daily[day];
    }
  }

  private bump(key: keyof Counters): void {
    this.totals[key]++;
    const day = new Date().toISOString().slice(0, 10);
    (this.daily[day] ??= empty())[key]++;
    this.dirty = true;
  }

  recordHit(ua: string | undefined): void { this.bump(isBot(ua) ? 'botHits' : 'hits'); }
  recordDownload(): void { this.bump('pluginDownloads'); }
  recordRunStart(): void { this.bump('runsExecuted'); }
  recordRunEnd(success: boolean): void { this.bump(success ? 'runsSucceeded' : 'runsFailed'); }

  /** Snapshot for the admin dashboard. Read-only copy. */
  snapshot(): Persisted & { keys: (keyof Counters)[] } {
    // Sort daily ascending for charting.
    const daily: Record<string, Counters> = {};
    for (const day of Object.keys(this.daily).sort()) daily[day] = { ...this.daily[day] };
    return { totals: { ...this.totals }, daily, keys: KEYS };
  }

  /**
   * SET 30a — zero every counter (totals + daily trend) and flush to disk
   * immediately. Owner-only, password-gated at the route layer
   * (src/server/globalStatsAdmin.ts) — this method itself trusts its caller.
   */
  reset(): void {
    this.totals = empty();
    this.daily = {};
    this.dirty = true;
    this.flush();
  }
}
