import { Database } from "bun:sqlite";
import { chmodSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Adapter, SavingsDelta, AbsoluteTotals } from "../adapters/Adapter.ts";
import {
  ensureSecureDir,
  SECURE_FILE_MODE,
  secureAtomicWriteSync,
} from "../install/blocks/secureWrite.ts";

interface AdapterCounters {
  bytesIn: number;
  bytesOut: number;
  tokensIn: number;
  tokensOut: number;
  hits: number;
}

const FT_DIR = join(homedir(), ".fewtok");
const DB_PATH = join(FT_DIR, "savings.sqlite");
const SNAPSHOT_PATH = join(FT_DIR, "statusline.json");

export class SavingsLedger {
  private readonly counters = new Map<string, AdapterCounters>();
  private db: Database | null = null;
  private snapshotPending = false;
  private snapshotTimer: ReturnType<typeof setTimeout> | null = null;
  private readonly sessionId: string;
  private persistCounter = 0;
  private readonly baselines = new Map<string, AbsoluteTotals>();
  private readonly pendingBatch = new Map<string, AdapterCounters>();

  constructor(sessionId: string) {
    this.sessionId = sessionId;
    try {
      ensureSecureDir(FT_DIR);
    } catch {
      // stats must not crash on chmod/mkdir failure
    }
    this.db = new Database(DB_PATH);
    try {
      chmodSync(DB_PATH, SECURE_FILE_MODE);
    } catch {
      // stats must not crash on chmod failure
    }
    // N=2: both children open this shared savings DB at boot — serialize the WAL/DDL
    // write lock with a busy timeout so the race loser waits instead of throwing.
    this.db.exec("PRAGMA busy_timeout = 5000");
    this.db.exec("PRAGMA journal_mode = WAL");
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS adapter_savings (
        adapter_id TEXT NOT NULL,
        session_id TEXT NOT NULL,
        ts INTEGER NOT NULL,
        bytes_in INTEGER NOT NULL,
        bytes_out INTEGER NOT NULL,
        tokens_in INTEGER NOT NULL,
        tokens_out INTEGER NOT NULL,
        hits INTEGER NOT NULL,
        PRIMARY KEY (adapter_id, session_id, ts)
      );
      CREATE INDEX IF NOT EXISTS idx_adapter ON adapter_savings(adapter_id);
      CREATE TABLE IF NOT EXISTS adapter_baselines (
        adapter_id TEXT PRIMARY KEY,
        bytes_in INTEGER NOT NULL,
        bytes_out INTEGER NOT NULL,
        tokens_in INTEGER NOT NULL,
        tokens_out INTEGER NOT NULL,
        hits INTEGER NOT NULL,
        updated_at INTEGER NOT NULL
      );
    `);
    this.loadCumulative();
    this.loadBaselines();
  }

  private loadBaselines(): void {
    if (!this.db) return;
    const rows = this.db.query(
      `SELECT adapter_id, bytes_in, bytes_out, tokens_in, tokens_out, hits FROM adapter_baselines`,
    ).all() as Array<{ adapter_id: string; bytes_in: number; bytes_out: number; tokens_in: number; tokens_out: number; hits: number }>;
    for (const r of rows) {
      this.baselines.set(r.adapter_id, {
        bytesIn: r.bytes_in, bytesOut: r.bytes_out,
        tokensIn: r.tokens_in, tokensOut: r.tokens_out,
        hits: r.hits,
      });
    }
  }

  private saveBaseline(adapterId: string, totals: AbsoluteTotals): void {
    if (!this.db) return;
    try {
      this.db.run(
        `INSERT OR REPLACE INTO adapter_baselines
         (adapter_id, bytes_in, bytes_out, tokens_in, tokens_out, hits, updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?)`,
        [adapterId, totals.bytesIn, totals.bytesOut, totals.tokensIn, totals.tokensOut, totals.hits, Date.now()],
      );
    } catch { /* swallow */ }
  }

  pollAdapter(adapter: Adapter): void {
    if (!adapter.readSavings || !adapter.isInstalled()) return;
    const cur = adapter.readSavings();
    if (!cur) return;
    const prev = this.baselines.get(adapter.id);
    if (!prev) {
      this.baselines.set(adapter.id, cur);
      this.saveBaseline(adapter.id, cur);
      return;
    }
    const delta: SavingsDelta = {
      bytesIn: Math.max(0, cur.bytesIn - prev.bytesIn),
      bytesOut: Math.max(0, cur.bytesOut - prev.bytesOut),
      tokensIn: Math.max(0, cur.tokensIn - prev.tokensIn),
      tokensOut: Math.max(0, cur.tokensOut - prev.tokensOut),
      hits: Math.max(0, cur.hits - prev.hits),
    };
    const hasActivity =
      delta.bytesIn > 0 || delta.bytesOut > 0
      || (delta.tokensIn ?? 0) > 0 || (delta.tokensOut ?? 0) > 0
      || delta.hits > 0;

    if (hasActivity) {
      const c = this.counters.get(adapter.id) ?? { bytesIn: 0, bytesOut: 0, tokensIn: 0, tokensOut: 0, hits: 0 };
      c.bytesIn += delta.bytesIn;
      c.bytesOut += delta.bytesOut;
      c.tokensIn += delta.tokensIn ?? 0;
      c.tokensOut += delta.tokensOut ?? 0;
      c.hits += delta.hits;
      this.counters.set(adapter.id, c);
      this.scheduleSnapshot();
    }

    const isReset =
      cur.bytesIn < prev.bytesIn || cur.bytesOut < prev.bytesOut
      || cur.tokensIn < prev.tokensIn || cur.tokensOut < prev.tokensOut
      || cur.hits < prev.hits;

    if (!hasActivity && !isReset) return;

    if (this.db) {
      const ts = Date.now();
      try {
        this.db.transaction(() => {
          if (hasActivity) {
            this.db!.run(
              `INSERT INTO adapter_savings (adapter_id, session_id, ts, bytes_in, bytes_out, tokens_in, tokens_out, hits)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
              [adapter.id, this.sessionId, ts, delta.bytesIn, delta.bytesOut, delta.tokensIn ?? 0, delta.tokensOut ?? 0, delta.hits],
            );
          }
          this.db!.run(
            `INSERT OR REPLACE INTO adapter_baselines
             (adapter_id, bytes_in, bytes_out, tokens_in, tokens_out, hits, updated_at)
             VALUES (?, ?, ?, ?, ?, ?, ?)`,
            [adapter.id, cur.bytesIn, cur.bytesOut, cur.tokensIn, cur.tokensOut, cur.hits, ts],
          );
        })();
        this.baselines.set(adapter.id, cur);
      } catch { /* swallow */ }
    } else {
      this.baselines.set(adapter.id, cur);
    }
  }

  private loadCumulative(): void {
    if (!this.db) return;
    const rows = this.db.query(`
      SELECT adapter_id,
             SUM(bytes_in) AS bytes_in, SUM(bytes_out) AS bytes_out,
             SUM(tokens_in) AS tokens_in, SUM(tokens_out) AS tokens_out,
             SUM(hits) AS hits
      FROM adapter_savings
      GROUP BY adapter_id
    `).all() as Array<{ adapter_id: string; bytes_in: number; bytes_out: number; tokens_in: number; tokens_out: number; hits: number }>;
    for (const r of rows) {
      this.counters.set(r.adapter_id, {
        bytesIn: r.bytes_in, bytesOut: r.bytes_out,
        tokensIn: r.tokens_in, tokensOut: r.tokens_out,
        hits: r.hits,
      });
    }
  }

  record(adapterId: string, delta: SavingsDelta): void {
    const c = this.counters.get(adapterId) ?? { bytesIn: 0, bytesOut: 0, tokensIn: 0, tokensOut: 0, hits: 0 };
    c.bytesIn += delta.bytesIn;
    c.bytesOut += delta.bytesOut;
    c.tokensIn += delta.tokensIn ?? 0;
    c.tokensOut += delta.tokensOut ?? 0;
    c.hits += delta.hits;
    this.counters.set(adapterId, c);
    this.scheduleSnapshot();
    this.maybePersist(adapterId, delta);
  }

  private maybePersist(adapterId: string, delta: SavingsDelta): void {
    if (!this.db) return;
    const b = this.pendingBatch.get(adapterId) ?? { bytesIn: 0, bytesOut: 0, tokensIn: 0, tokensOut: 0, hits: 0 };
    b.bytesIn += delta.bytesIn;
    b.bytesOut += delta.bytesOut;
    b.tokensIn += delta.tokensIn ?? 0;
    b.tokensOut += delta.tokensOut ?? 0;
    b.hits += delta.hits;
    this.pendingBatch.set(adapterId, b);
    this.persistCounter++;
    if (this.persistCounter < 100) return;
    this.persistCounter = 0;
    this.persistBatch();
  }

  private persistBatch(): void {
    if (!this.db || this.pendingBatch.size === 0) return;
    const ts = Date.now();
    try {
      for (const [adapterId, b] of this.pendingBatch) {
        this.db.run(
          `INSERT INTO adapter_savings (adapter_id, session_id, ts, bytes_in, bytes_out, tokens_in, tokens_out, hits)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
          [adapterId, this.sessionId, ts, b.bytesIn, b.bytesOut, b.tokensIn, b.tokensOut, b.hits],
        );
      }
      this.pendingBatch.clear();
    } catch { /* swallow */ }
  }

  private scheduleSnapshot(): void {
    if (this.snapshotPending) return;
    this.snapshotPending = true;
    this.snapshotTimer = setTimeout(() => {
      this.snapshotPending = false;
      this.writeSnapshot();
    }, 1000);
  }

  writeSnapshot(): void {
    // statusline.json is a SHARED single file. Under N=2 (Phase 3) both children run
    // a ledger; if both wrote, they'd clobber to one child's in-memory share (each
    // counter only sees the requests THAT process served). So: only the PRIMARY writes
    // (live env check — a promoted child flips FT_PRIMARY="1"), and it sources the
    // snapshot from the DB SUM across BOTH children's persisted rows (mirrors
    // loadCumulative) rather than its own counters. The not-yet-persisted pendingBatch
    // (this process, ≤100 rows) is added on top so a low-traffic primary isn't stale;
    // the sibling's unflushed ≤100 rows lag invisibly on a statusline.
    if (process.env.FT_PRIMARY === "0") return; // secondary: primary owns the file
    const out: Record<string, AdapterCounters> = {};
    if (this.db) {
      try {
        const rows = this.db.query(`
          SELECT adapter_id,
                 SUM(bytes_in) AS bytes_in, SUM(bytes_out) AS bytes_out,
                 SUM(tokens_in) AS tokens_in, SUM(tokens_out) AS tokens_out,
                 SUM(hits) AS hits
          FROM adapter_savings
          GROUP BY adapter_id
        `).all() as Array<{ adapter_id: string; bytes_in: number; bytes_out: number; tokens_in: number; tokens_out: number; hits: number }>;
        for (const r of rows) {
          out[r.adapter_id] = {
            bytesIn: r.bytes_in, bytesOut: r.bytes_out,
            tokensIn: r.tokens_in, tokensOut: r.tokens_out, hits: r.hits,
          };
        }
      } catch { /* fall through with whatever DB rows we got */ }
      for (const [id, b] of this.pendingBatch) {
        const c = out[id] ?? { bytesIn: 0, bytesOut: 0, tokensIn: 0, tokensOut: 0, hits: 0 };
        c.bytesIn += b.bytesIn; c.bytesOut += b.bytesOut;
        c.tokensIn += b.tokensIn; c.tokensOut += b.tokensOut; c.hits += b.hits;
        out[id] = c;
      }
    } else {
      for (const [id, c] of this.counters) out[id] = c;
    }
    try {
      secureAtomicWriteSync(SNAPSHOT_PATH, JSON.stringify({ ts: Date.now(), counters: out }));
    } catch { /* swallow */ }
  }

  flush(): void {
    if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
    this.persistBatch();
    this.writeSnapshot();
    this.db?.close();
    this.db = null;
  }
}
