import { Database } from "bun:sqlite";
import { mkdirSync, readdirSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { initStatsSchema } from "./schema.ts";
import { BYTES_PER_TOKEN_FALLBACK } from "./pricing.ts";

export interface RequestRow {
  timestamp: string;
  provider: string | null;
  project: string | null;
  sessionId: string | null;
  model: string | null;
  rawInputTokens: number;
  compressedInputTokens: number;
  expandedOutputTokens?: number;
  rawOutputTokens: number;
  cacheHitInputTokens: number;
  cacheCreationInputTokens: number;
  latencyMs: number;
}

export interface SavingRow {
  layerId: string;
  kind: string;
  rawBytes: number;
  sentBytes: number;
  meta?: Record<string, unknown>;
}

export interface LegacySavingOpts {
  kind?: string;
  layerId: string;
  tokensBefore: number;
  tokensAfter: number;
  at: number;
  model?: string | null;
}

export interface RequestMeta {
  at: number;
  provider: string | null;
  model: string | null;
  sessionId: string | null;
  project: string | null;
  rawInputTokens: number;
  compressedInputTokens: number;
  expandedOutputTokens?: number;
  rawOutputTokens: number;
  cacheHitInputTokens: number;
  cacheCreationInputTokens: number;
  latencyMs: number;
}

export interface SimpleStats {
  write(row: LegacySavingOpts): number;
  writeRequest(meta: RequestMeta, savings?: SavingRow[]): number;
  /** Apply/refresh PRAGMAs on the live connection (called from reload path). */
  applyPragmas(): void;
  /** Force a WAL checkpoint, truncating WAL back to zero. Best-effort. */
  checkpointTruncate(): void;
}

export function openStatsDb(dir: string, opts: { readonly?: boolean } = {}): Database {
  mkdirSync(dir, { recursive: true });
  const path = join(dir, "stats.db");
  if (opts.readonly) {
    const writable = new Database(path);
    initStatsSchema(writable);
    writable.close();
    return new Database(path, { readonly: true });
  }
  const db = new Database(path);
  initStatsSchema(db);
  return db;
}

export interface PrunePollutedOpts {
  olderThanDays?: number;
  dryRun?: boolean;
}

export interface PrunePollutedResult {
  candidates: string[];
  pruned: string[];
  skipped: { name: string; reason: string }[];
}

export function prunePollutedFiles(dir: string, opts: PrunePollutedOpts = {}): PrunePollutedResult {
  const olderThanDays = opts.olderThanDays ?? 30;
  const dryRun = opts.dryRun ?? false;
  const cutoffMs = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
  const re = /^stats\.db\.polluted-(\d{4})-(\d{2})-(\d{2})/;
  const candidates: string[] = [];
  const pruned: string[] = [];
  const skipped: { name: string; reason: string }[] = [];
  let entries: string[];
  try { entries = readdirSync(dir); } catch { return { candidates, pruned, skipped }; }
  for (const name of entries) {
    const m = re.exec(name);
    if (!m) continue;
    candidates.push(name);
    const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
    const t = Date.UTC(y, mo - 1, d);
    if (Number.isNaN(t)) { skipped.push({ name, reason: "unparseable-date" }); continue; }
    if (t > cutoffMs) { skipped.push({ name, reason: "too-recent" }); continue; }
    const full = join(dir, name);
    if (dryRun) { pruned.push(name); continue; }
    try { unlinkSync(full); pruned.push(name); } catch (e) {
      skipped.push({ name, reason: String(e) });
    }
  }
  return { candidates, pruned, skipped };
}

export function openStats(dir: string): SimpleStats {
  const db = openStatsDb(dir);
  const writer = new StatsWriter(db);
  return {
    write(row: LegacySavingOpts): number {
      const requestId = writer.recordRequest({
        timestamp: new Date(row.at).toISOString(),
        provider: null,
        project: null,
        sessionId: null,
        model: row.model ?? null,
        rawInputTokens: row.tokensBefore,
        compressedInputTokens: row.tokensAfter,
        rawOutputTokens: 0,
        cacheHitInputTokens: 0,
        cacheCreationInputTokens: 0,
        latencyMs: 0,
      });
      writer.recordSavings(requestId, [{
        layerId: row.layerId,
        kind: row.kind ?? "saving",
        rawBytes: Math.round(row.tokensBefore * BYTES_PER_TOKEN_FALLBACK),
        sentBytes: Math.round(row.tokensAfter * BYTES_PER_TOKEN_FALLBACK),
      }]);
      return requestId;
    },
    writeRequest(meta: RequestMeta, savings: SavingRow[] = []): number {
      const requestId = writer.recordRequest({
        timestamp: new Date(meta.at).toISOString(),
        provider: meta.provider,
        project: meta.project,
        sessionId: meta.sessionId,
        model: meta.model,
        rawInputTokens: meta.rawInputTokens,
        compressedInputTokens: meta.compressedInputTokens,
        rawOutputTokens: meta.rawOutputTokens,
        cacheHitInputTokens: meta.cacheHitInputTokens,
        cacheCreationInputTokens: meta.cacheCreationInputTokens,
        latencyMs: meta.latencyMs,
      });
      writer.recordSavings(requestId, savings);
      return requestId;
    },
    applyPragmas(): void {
      db.run("PRAGMA synchronous = NORMAL");
      db.run("PRAGMA foreign_keys = ON");
      // journal_mode is sticky across the DB file — no need to re-set per-connection.
    },
    checkpointTruncate(): void {
      try {
        db.query("PRAGMA wal_checkpoint(TRUNCATE)").all();
      } catch (e) {
        // best-effort; readers may block truncate; falls back to PASSIVE
        void e;
      }
    },
  };
}

export class StatsWriter {
  constructor(private db: Database) {}

  recordRequest(r: RequestRow): number {
    const stmt = this.db.query(`
      INSERT INTO requests (
        timestamp, provider, project, session_id, model,
        raw_input_tokens, compressed_input_tokens,
        raw_output_tokens,
        cache_hit_input_tokens, cache_creation_input_tokens,
        latency_ms
      ) VALUES (?,?,?,?,?,?,?,?,?,?,?)
    `);
    const res = stmt.run(
      r.timestamp, r.provider, r.project, r.sessionId, r.model,
      r.rawInputTokens, r.compressedInputTokens,
      r.rawOutputTokens,
      r.cacheHitInputTokens, r.cacheCreationInputTokens,
      r.latencyMs,
    );
    return Number(res.lastInsertRowid);
  }

  recordSavings(requestId: number, events: SavingRow[]): void {
    if (events.length === 0) return;
    const stmt = this.db.query(`
      INSERT INTO saving_events (request_id, layer_id, kind, raw_bytes, sent_bytes, meta)
      VALUES (?,?,?,?,?,?)
    `);
    const tx = this.db.transaction((evs: SavingRow[]) => {
      for (const e of evs) {
        stmt.run(requestId, e.layerId, e.kind, e.rawBytes, e.sentBytes,
          e.meta ? JSON.stringify(e.meta) : null);
      }
    });
    tx(events);
  }
}
