import type Database from 'better-sqlite3';
import { mkdir, rename, stat, readdir, unlink, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { applyGfsRetention } from './backupRotation.js';
import { KST_OFFSET_MS } from '../../shared/util/kstDate.js';
const MARKER_FILENAME = '.last-snapshot';

function toKstDateKey(d: Date): string {
  const k = new Date(d.getTime() + KST_OFFSET_MS);
  return k.toISOString().slice(0, 10);
}

export interface SnapshotResult {
  path: string;
  bytes: number;
}

export interface RotateResult {
  kept: string[];
  removed: string[];
}

export interface DailyResult {
  snapshotted: boolean;
  reason?: string;
  path?: string;
  bytes?: number;
  kept?: string[];
  removed?: string[];
}

export class BackupService {
  constructor(
    private db: Database.Database,
    private backupDir: string,
    private now: () => Date = () => new Date()
  ) {}

  /**
   * Atomic snapshot of the SQLite DB to `inkling-YYYY-MM-DD.sqlite` (KST).
   *
   * NOT safe for concurrent calls — two parallel calls race on the same
   * tmp/final path. Callers should serialize via {@link runDaily}, which
   * gates on the `.last-snapshot` marker.
   */
  async snapshot(): Promise<SnapshotResult> {
    await mkdir(this.backupDir, { recursive: true });
    const dateKey = toKstDateKey(this.now());
    const finalPath = join(this.backupDir, `inkling-${dateKey}.sqlite`);
    const tmpPath = `${finalPath}.tmp`;
    try {
      await this.db.backup(tmpPath);
      await rename(tmpPath, finalPath);
    } catch (e) {
      await unlink(tmpPath).catch(() => { /* tmp may not exist */ });
      throw e;
    }
    const st = await stat(finalPath);
    return { path: finalPath, bytes: st.size };
  }

  async rotate(): Promise<RotateResult> {
    let entries: string[];
    try {
      entries = await readdir(this.backupDir);
    } catch (e: unknown) {
      if ((e as NodeJS.ErrnoException).code === 'ENOENT') return { kept: [], removed: [] };
      throw e;
    }
    const decision = applyGfsRetention(entries, this.now());
    for (const name of decision.remove) {
      await unlink(join(this.backupDir, name));
    }
    return { kept: decision.keep, removed: decision.remove };
  }

  async lastSnapshotAt(): Promise<string | null> {
    try {
      const raw = await readFile(join(this.backupDir, MARKER_FILENAME), 'utf8');
      return raw.trim() || null;
    } catch (e: unknown) {
      if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;
      throw e;
    }
  }

  async runDaily(): Promise<DailyResult> {
    const today = toKstDateKey(this.now());
    const last = await this.lastSnapshotAt();
    if (last === today) {
      return { snapshotted: false, reason: 'already snapshotted today' };
    }
    const snap = await this.snapshot();
    await writeFile(join(this.backupDir, MARKER_FILENAME), today, 'utf8');
    const rot = await this.rotate();
    return {
      snapshotted: true,
      path: snap.path,
      bytes: snap.bytes,
      kept: rot.kept,
      removed: rot.removed
    };
  }
}
