/**
 * src/push/subscription-store.ts (N-push, A1) — persistent store of browser
 * Web Push subscriptions.
 *
 * Holds the `PushSubscriptionRecord`s the push-service signs and sends to. The
 * file is SENSITIVE (endpoint + keys) — persisted with 0600 permissions and
 * NEVER exposed via any GET route (SEC-M2). A `PUSH_MAX_SUBS` FIFO cap bounds
 * memory/disk against a flood of subscribe calls (SEC-M1).
 *
 * All mutations are immutable: the internal array is REPLACED with a new array,
 * never mutated in place; `list()` returns a frozen shallow copy.
 *
 * Loading is best-effort (A1): a missing file → empty; malformed JSON → empty
 * (logged); invalid entries are filtered out. The push-service stays usable.
 */

import { readFileSync } from 'node:fs';
import { chmod, writeFile } from 'node:fs/promises';
import type { PushSubscriptionRecord } from '../types.js';

/** 0600 — owner read/write only; the file holds subscription secrets (SEC-M2). */
const FILE_MODE = 0o600;

export interface SubscriptionStore {
  /** Frozen snapshot of current subscriptions (immutable to callers). */
  list(): readonly PushSubscriptionRecord[];
  /** Add a record (dedup by endpoint, FIFO-capped at maxSubs). Throws on invalid input. */
  add(record: PushSubscriptionRecord): void;
  /** Remove by endpoint; no-op when absent. */
  remove(endpoint: string): void;
  /** Remove a batch of dead endpoints (404/410 from the push service). */
  prune(deadEndpoints: readonly string[]): void;
  /** Persist to disk (0600). Best-effort: logs and resolves on failure. */
  persist(): Promise<void>;
}

/** Type guard: a structurally valid, persistable subscription record. */
export function isValidSubscriptionRecord(x: unknown): x is PushSubscriptionRecord {
  if (x === null || typeof x !== 'object') return false;
  const r = x as Record<string, unknown>;
  if (typeof r['endpoint'] !== 'string' || r['endpoint'].length === 0) return false;
  const keys = r['keys'];
  if (keys === null || typeof keys !== 'object') return false;
  const k = keys as Record<string, unknown>;
  if (typeof k['p256dh'] !== 'string' || typeof k['auth'] !== 'string') return false;
  if (typeof r['createdAt'] !== 'number' || !Number.isFinite(r['createdAt'])) return false;
  return true;
}

function loadRecords(filePath: string): PushSubscriptionRecord[] {
  let raw: string;
  try {
    raw = readFileSync(filePath, 'utf8');
  } catch (err) {
    if ((err as NodeJS.ErrnoException)?.code !== 'ENOENT') {
      console.error('subscription-store: load failed, starting empty', err);
    }
    return [];
  }
  try {
    const parsed: unknown = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed.filter(isValidSubscriptionRecord);
  } catch (err) {
    console.error('subscription-store: malformed JSON, starting empty', err);
    return [];
  }
}

/**
 * Load (or initialise empty) a subscription store backed by `filePath`.
 * @param filePath absolute path to the JSON store (cfg.pushStorePath)
 * @param maxSubs FIFO cap on stored subscriptions (cfg.pushMaxSubs)
 */
export function loadSubscriptionStore(filePath: string, maxSubs: number): SubscriptionStore {
  let records: PushSubscriptionRecord[] = loadRecords(filePath);

  return {
    list(): readonly PushSubscriptionRecord[] {
      return Object.freeze(records.slice());
    },

    add(record: PushSubscriptionRecord): void {
      if (!isValidSubscriptionRecord(record)) {
        throw new TypeError('subscription-store: invalid subscription record');
      }
      const deduped = records.filter((r) => r.endpoint !== record.endpoint);
      const next = [...deduped, record];
      records = next.length > maxSubs ? next.slice(next.length - maxSubs) : next;
    },

    remove(endpoint: string): void {
      records = records.filter((r) => r.endpoint !== endpoint);
    },

    prune(deadEndpoints: readonly string[]): void {
      if (deadEndpoints.length === 0) return;
      const dead = new Set(deadEndpoints);
      records = records.filter((r) => !dead.has(r.endpoint));
    },

    async persist(): Promise<void> {
      try {
        await writeFile(filePath, JSON.stringify(records, null, 2), { mode: FILE_MODE });
        // writeFile's mode only applies on creation; enforce 0600 if the file pre-existed.
        await chmod(filePath, FILE_MODE);
      } catch (err) {
        console.error('subscription-store: persist failed', err);
      }
    },
  };
}
