/**
 * Agent audit log — append-only JSONL with size-based rotation.
 *
 * Every gated tool call writes a row. Rows deliberately carry NO argument
 * values and NO response bodies — the agent already saw both, and logging
 * them would create a parallel on-disk copy of the user's email. We store
 * a truncated sha256 hash of the args instead so "same call repeated" is
 * observable without content leakage.
 *
 * File: ~/.mailpouch-agent-audit.jsonl (0600)
 * Rotation: when current file exceeds ROTATE_BYTES, rename to .1.gz and
 *           start fresh. Keep KEEP_GENERATIONS compressed generations.
 */

import { appendFileSync, existsSync, statSync, renameSync, writeFileSync, readFileSync, unlinkSync } from "fs";
import { createHash } from "crypto";
import { gzipSync } from "zlib";
import type { AuditRow } from "./types.js";
import { logger } from "../utils/logger.js";

const ROTATE_BYTES = 10 * 1024 * 1024;  // 10 MB per generation
const KEEP_GENERATIONS = 3;

export interface AuditDeps {
  /** Absolute path to the active log file. */
  path: string;
  /** Now() override for deterministic tests. */
  now?: () => number;
}

export class AgentAuditLog {
  private readonly path: string;
  private readonly now: () => number;

  constructor(deps: AuditDeps) {
    this.path = deps.path;
    this.now = deps.now ?? Date.now;
  }

  /** Append a row. Best-effort — logging failures are warned, not thrown. */
  write(row: AuditRow): void {
    try {
      this.rotateIfNeeded();
      appendFileSync(this.path, JSON.stringify(row) + "\n", { encoding: "utf-8", mode: 0o600 });
    } catch (err) {
      logger.warn(`AgentAuditLog: append failed for ${this.path}`, "AgentAuditLog", err);
    }
  }

  private rotateIfNeeded(): void {
    if (!existsSync(this.path)) return;
    let sizeBytes = 0;
    try { sizeBytes = statSync(this.path).size; } catch { /* swallow */ return; }
    if (sizeBytes < ROTATE_BYTES) return;

    // Age the existing .N.gz files up by one.
    for (let i = KEEP_GENERATIONS - 1; i >= 1; i--) {
      const from = `${this.path}.${i}.gz`;
      const to   = `${this.path}.${i + 1}.gz`;
      if (existsSync(from)) {
        try { renameSync(from, to); } catch (err) { logger.debug(`rotate rename ${from} → ${to} failed`, "AgentAuditLog", err); }
      }
    }
    // PERM-012: rename the live file out of the way FIRST, then gzip the
    // renamed copy out-of-line. `rename(2)` is atomic on POSIX, so any
    // concurrent `appendFileSync(this.path, …)` lands in a fresh file rather
    // than in the snapshot we're about to compress — closing the
    // read→truncate window where a row written between `readFileSync` and the
    // truncating `writeFileSync` was silently lost.
    const staged = `${this.path}.rotating`;
    try {
      renameSync(this.path, staged);
    } catch (err) {
      logger.warn(`AgentAuditLog: rotation rename failed, keeping current file`, "AgentAuditLog", err);
      return;
    }
    try {
      const raw = readFileSync(staged);
      writeFileSync(`${this.path}.1.gz`, gzipSync(raw), { encoding: undefined, mode: 0o600 });
      unlinkSync(staged);
    } catch (err) {
      // Compression failed after the live file was renamed away. Don't leave the
      // rows orphaned in `${path}.rotating` — the NEXT rotation's rename would
      // clobber them. If no new live file exists yet (the common case), move the
      // staged copy back so appends continue it; otherwise park it under a unique
      // name for manual recovery rather than losing it.
      try {
        if (!existsSync(this.path)) {
          renameSync(staged, this.path);
        } else {
          renameSync(staged, `${this.path}.recover-${this.now()}`);
        }
      } catch { /* best-effort recovery */ }
      logger.warn(`AgentAuditLog: rotation compress failed; staged copy recovered`, "AgentAuditLog", err);
    }
    // Evict generations beyond KEEP_GENERATIONS.
    for (let i = KEEP_GENERATIONS + 1; i < KEEP_GENERATIONS + 5; i++) {
      const p = `${this.path}.${i}.gz`;
      if (existsSync(p)) {
        try { unlinkSync(p); } catch { /* ignore */ }
      }
    }
  }

  /**
   * Read the last `limit` rows. Used by the (forthcoming) Log UI tab.
   * Cheap for typical sizes because the file is append-only JSONL; for
   * very large logs consider tailing with a seek, but we're at most 10 MB
   * so a full load + slice is fine.
   */
  readTail(limit = 200): AuditRow[] {
    if (!existsSync(this.path)) return [];
    try {
      const raw = readFileSync(this.path, "utf-8");
      const lines = raw.split("\n").filter(l => l.length > 0);
      const slice = lines.slice(-limit);
      const rows: AuditRow[] = [];
      for (const line of slice) {
        try { rows.push(JSON.parse(line) as AuditRow); } catch { /* skip malformed */ }
      }
      return rows;
    } catch (err) {
      logger.warn(`AgentAuditLog: readTail failed`, "AgentAuditLog", err);
      return [];
    }
  }
}

/**
 * Truncated sha256 of JSON.stringify(args). Gives us a stable "same args"
 * fingerprint without storing the content. 16 hex chars = 64 bits of
 * collision resistance, plenty for a dedup hint at mailpouch scale.
 */
export function hashArgs(args: unknown): string {
  if (args === undefined || args === null) return "";
  try {
    const s = JSON.stringify(args);
    if (!s) return "";
    return createHash("sha256").update(s, "utf-8").digest("hex").slice(0, 16);
  } catch {
    return "";
  }
}
