import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import webPush from 'web-push';
import { logger } from './logger.js';

/**
 * VAPID key material for Web Push (RFC 8292).
 * Spec: docs/superpowers/specs/2026-05-28-browser-notifications-v2-webpush.md.
 *
 * - `keyId` identifies which key generation a subscription was created under,
 *   so after rotation we can still push to old subscriptions until they fail
 *   with 410 and self-clean.
 * - `subject` MUST be a `mailto:` or `https:` URL per RFC 8292; FCM has been
 *   observed to rate-limit generic `mailto:` values, so an operations URL is
 *   the recommended default.
 */
export interface VapidKeyMaterial {
  keyId: string;
  publicKey: string;
  privateKey: string;
  subject: string;
  createdAt: string;
}

const FILE_MODE = 0o600;

function newKeyId(): string {
  const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
  const suffix = Math.random().toString(36).slice(2, 8);
  return `v1-${date}-${suffix}`;
}

function readKeyFile(path: string): VapidKeyMaterial {
  const raw = readFileSync(path, 'utf-8');
  const parsed = JSON.parse(raw) as Partial<VapidKeyMaterial>;
  if (!parsed.keyId || !parsed.publicKey || !parsed.privateKey || !parsed.subject) {
    throw new Error(
      `[vapid-store] malformed key file at ${path} (missing keyId/publicKey/privateKey/subject)`,
    );
  }
  return {
    keyId: parsed.keyId,
    publicKey: parsed.publicKey,
    privateKey: parsed.privateKey,
    subject: parsed.subject,
    createdAt: parsed.createdAt ?? new Date().toISOString(),
  };
}

function writeKeyFile(path: string, material: VapidKeyMaterial): void {
  const dir = dirname(path);
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
  writeFileSync(path, JSON.stringify(material, null, 2), { mode: FILE_MODE });
}

/**
 * Manages VAPID key lifecycle: load-or-generate, history retrieval, rotation.
 *
 * Operational model:
 * - `currentPath` holds the active keypair used for new subscriptions.
 * - `historyDir` accumulates retired keys so push to old subscriptions still
 *   works during the grace period.
 * - Rotation is a manual CLI step (`scripts/vapid-rotate.ts`); subscriptions
 *   carry the `keyId` they were created under so we can pick the right key
 *   per-send.
 */
export class VapidKeyStore {
  private current: VapidKeyMaterial | null = null;
  private history = new Map<string, VapidKeyMaterial>();

  constructor(
    private readonly currentPath: string,
    private readonly historyDir: string,
  ) {}

  /**
   * Load the current key from disk, generating a fresh one if missing.
   * Idempotent — repeated calls return the in-memory copy.
   */
  loadOrGenerate(subject: string): VapidKeyMaterial {
    if (this.current) return this.current;
    if (existsSync(this.currentPath)) {
      this.current = readKeyFile(this.currentPath);
      logger.info(`[vapid-store] loaded VAPID key keyId=${this.current.keyId}`);
      return this.current;
    }
    const generated = webPush.generateVAPIDKeys();
    this.current = {
      keyId: newKeyId(),
      publicKey: generated.publicKey,
      privateKey: generated.privateKey,
      subject,
      createdAt: new Date().toISOString(),
    };
    writeKeyFile(this.currentPath, this.current);
    logger.info(
      `[vapid-store] generated new VAPID key keyId=${this.current.keyId} path=${this.currentPath}`,
    );
    return this.current;
  }

  /** Current key (must call loadOrGenerate first). */
  getCurrent(): VapidKeyMaterial {
    if (!this.current) {
      throw new Error('[vapid-store] getCurrent called before loadOrGenerate');
    }
    return this.current;
  }

  /**
   * Look up a specific key by ID. Returns the current key if it matches,
   * otherwise loads from history (cached after first lookup). Returns null
   * when the key is unknown — caller should treat the subscription as
   * needing re-subscribe.
   */
  getKey(keyId: string): VapidKeyMaterial | null {
    if (this.current && this.current.keyId === keyId) return this.current;
    const cached = this.history.get(keyId);
    if (cached) return cached;
    const historyPath = join(this.historyDir, `${keyId}.json`);
    if (!existsSync(historyPath)) return null;
    const material = readKeyFile(historyPath);
    this.history.set(keyId, material);
    return material;
  }

  /**
   * Rotate to a fresh keypair. The old current key is moved to history
   * (filename = old keyId). Returns the new material.
   *
   * Subscriptions in the DB are *not* touched here — they continue to carry
   * the old keyId, and the next push for each will be sent with the
   * preserved old key (until 410 / re-subscribe clears them).
   */
  rotate(subject: string): VapidKeyMaterial {
    const old = this.loadOrGenerate(subject);
    if (!existsSync(this.historyDir)) {
      mkdirSync(this.historyDir, { recursive: true, mode: 0o700 });
    }
    const historyPath = join(this.historyDir, `${old.keyId}.json`);
    renameSync(this.currentPath, historyPath);
    this.history.set(old.keyId, old);
    this.current = null;
    const fresh = this.loadOrGenerate(subject);
    logger.info(
      `[vapid-store] rotated VAPID key oldKeyId=${old.keyId} newKeyId=${fresh.keyId}`,
    );
    return fresh;
  }
}
