/**
 * Web Push Notifications module.
 *
 * Manages VAPID key generation/persistence, push subscription storage,
 * and sending push notifications when Claude needs user approval.
 */

import { existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { createLogger } from "./logger.js";
import { ZENCODE_DIR } from "./config.js";
import { ensureSecureDir, writeSecureFile } from "./fs-permissions.js";

const log = createLogger("push-notifications");

// ─── Types ────────────────────────────────────────────────────────────────────

export interface PushSubscriptionData {
  endpoint: string;
  keys: {
    p256dh: string;
    auth: string;
  };
}

interface VapidKeys {
  publicKey: string;
  privateKey: string;
  subject: string;
}

interface PersistedSubscriptions {
  subscriptions: PushSubscriptionData[];
}

// ─── Paths ────────────────────────────────────────────────────────────────────

const VAPID_KEYS_PATH = join(ZENCODE_DIR, "vapid-keys.json");
const SUBSCRIPTIONS_PATH = join(ZENCODE_DIR, "push-subscriptions.json");

// ─── State ────────────────────────────────────────────────────────────────────

let vapidKeys: VapidKeys | null = null;
/** Subscriptions keyed by endpoint URL for dedup. */
const subscriptions = new Map<string, PushSubscriptionData>();
let webPush: typeof import("web-push") | null = null;

// ─── Lazy web-push import ─────────────────────────────────────────────────────

async function getWebPush(): Promise<typeof import("web-push")> {
  if (webPush) return webPush;
  webPush = await import("web-push");
  return webPush;
}

// ─── VAPID Key Management ─────────────────────────────────────────────────────

function loadVapidKeys(): VapidKeys | null {
  try {
    if (existsSync(VAPID_KEYS_PATH)) {
      const raw = readFileSync(VAPID_KEYS_PATH, "utf-8");
      const parsed = JSON.parse(raw) as VapidKeys;
      if (parsed.publicKey && parsed.privateKey && parsed.subject) {
        return parsed;
      }
    }
  } catch (err) {
    log.warn({ err }, "Failed to load VAPID keys");
  }
  return null;
}

async function generateAndSaveVapidKeys(): Promise<VapidKeys> {
  const wp = await getWebPush();
  const generated = wp.generateVAPIDKeys();
  const keys: VapidKeys = {
    publicKey: generated.publicKey,
    privateKey: generated.privateKey,
    subject: "mailto:zencode@localhost",
  };
  ensureSecureDir(dirname(VAPID_KEYS_PATH));
  writeSecureFile(VAPID_KEYS_PATH, JSON.stringify(keys, null, 2));
  log.info("Generated and saved new VAPID keys");
  return keys;
}

export async function getVapidPublicKey(): Promise<string> {
  if (!vapidKeys) {
    vapidKeys = loadVapidKeys();
    if (!vapidKeys) {
      vapidKeys = await generateAndSaveVapidKeys();
    }
  }
  return vapidKeys.publicKey;
}

async function ensureVapidConfigured(): Promise<void> {
  if (!vapidKeys) {
    vapidKeys = loadVapidKeys();
    if (!vapidKeys) {
      vapidKeys = await generateAndSaveVapidKeys();
    }
  }
  const wp = await getWebPush();
  wp.setVapidDetails(vapidKeys.subject, vapidKeys.publicKey, vapidKeys.privateKey);
}

// ─── Subscription Management ──────────────────────────────────────────────────

function loadSubscriptions(): void {
  try {
    if (existsSync(SUBSCRIPTIONS_PATH)) {
      const raw = readFileSync(SUBSCRIPTIONS_PATH, "utf-8");
      const parsed = JSON.parse(raw) as PersistedSubscriptions;
      if (Array.isArray(parsed.subscriptions)) {
        for (const sub of parsed.subscriptions) {
          if (sub.endpoint && sub.keys?.p256dh && sub.keys?.auth) {
            subscriptions.set(sub.endpoint, sub);
          }
        }
      }
    }
  } catch (err) {
    log.warn({ err }, "Failed to load push subscriptions");
  }
}

function persistSubscriptions(): void {
  try {
    ensureSecureDir(dirname(SUBSCRIPTIONS_PATH));
    const data: PersistedSubscriptions = {
      subscriptions: Array.from(subscriptions.values()),
    };
    writeSecureFile(SUBSCRIPTIONS_PATH, JSON.stringify(data, null, 2));
  } catch (err) {
    log.warn({ err }, "Failed to persist push subscriptions");
  }
}

// Load subscriptions on module init
loadSubscriptions();

export function addSubscription(sub: PushSubscriptionData): void {
  subscriptions.set(sub.endpoint, sub);
  persistSubscriptions();
  log.info({ endpoint: sub.endpoint.slice(0, 60) }, "Push subscription added");
}

export function removeSubscription(endpoint: string): boolean {
  const removed = subscriptions.delete(endpoint);
  if (removed) {
    persistSubscriptions();
    log.info({ endpoint: endpoint.slice(0, 60) }, "Push subscription removed");
  }
  return removed;
}

export function getSubscriptionCount(): number {
  return subscriptions.size;
}

// ─── Push Sending ─────────────────────────────────────────────────────────────

interface PushPayload {
  title: string;
  body: string;
  tag?: string;
  url?: string;
}

async function sendToSubscription(sub: PushSubscriptionData, payload: PushPayload): Promise<boolean> {
  try {
    await ensureVapidConfigured();
    const wp = await getWebPush();
    await wp.sendNotification(
      {
        endpoint: sub.endpoint,
        keys: sub.keys,
      },
      JSON.stringify(payload),
      { TTL: 60 * 10 }, // 10 minutes
    );
    return true;
  } catch (err: unknown) {
    const statusCode = (err as { statusCode?: number })?.statusCode;
    // 404 or 410 means the subscription is no longer valid
    if (statusCode === 404 || statusCode === 410) {
      log.info({ endpoint: sub.endpoint.slice(0, 60) }, "Push subscription expired, removing");
      subscriptions.delete(sub.endpoint);
      persistSubscriptions();
    } else {
      log.warn({ err, endpoint: sub.endpoint.slice(0, 60) }, "Failed to send push notification");
    }
    return false;
  }
}

/**
 * Send push notifications to all registered subscriptions.
 * Called when a permission request arrives and no browsers are connected.
 */
export async function notifyPermissionNeeded(
  sessionId: string,
  sessionName: string,
  toolName: string,
): Promise<number> {
  if (subscriptions.size === 0) return 0;

  const displayName = sessionName || sessionId.slice(0, 8);
  const payload: PushPayload = {
    title: "Permission Required",
    body: `${displayName}: ${toolName} needs approval`,
    tag: `perm-${sessionId}`,
    url: `/session/${sessionId}`,
  };

  const results = await Promise.allSettled(
    Array.from(subscriptions.values()).map((sub) => sendToSubscription(sub, payload)),
  );

  const sent = results.filter((r) => r.status === "fulfilled" && r.value).length;
  log.info({ sessionId, toolName, sent, total: subscriptions.size }, "Push notifications sent");
  return sent;
}

/**
 * Send a test push notification to verify the setup works.
 */
export async function sendTestNotification(): Promise<number> {
  if (subscriptions.size === 0) return 0;

  const payload: PushPayload = {
    title: "Zencode Test",
    body: "Push notifications are working!",
    tag: "test",
    url: "/settings",
  };

  const results = await Promise.allSettled(
    Array.from(subscriptions.values()).map((sub) => sendToSubscription(sub, payload)),
  );

  return results.filter((r) => r.status === "fulfilled" && r.value).length;
}
