import type { OfflineMutation, OfflineQueueStore } from "./index.ts";

export interface IndexedDbMigration {
  version: number;
  migrate(db: IDBDatabase, transaction: IDBTransaction): void;
}
export function openPwaDatabase(
  name: string,
  migrations: IndexedDbMigration[],
  factory: IDBFactory = indexedDB,
): Promise<IDBDatabase> {
  if (!name.trim()) throw new Error("IndexedDB name is required");
  const ordered = [...migrations].sort((a, b) => a.version - b.version);
  if (ordered.some((migration, index) => migration.version !== index + 1))
    throw new Error("WRN-PWA-IDB-MIGRATIONS: versions must be contiguous from 1");
  return new Promise((resolve, reject) => {
    const request = factory.open(name, ordered.at(-1)?.version ?? 1);
    request.onerror = () => reject(request.error);
    request.onblocked = () => reject(new Error("WRN-PWA-IDB-BLOCKED"));
    request.onupgradeneeded = (event) => {
      const transaction = request.transaction!;
      for (const migration of ordered)
        if (migration.version > event.oldVersion && migration.version <= event.newVersion!)
          migration.migrate(request.result, transaction);
    };
    request.onsuccess = () => resolve(request.result);
  });
}

function idbRequest<T>(request: IDBRequest<T>): Promise<T> {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}
export function indexedDbOfflineQueueStore(
  db: IDBDatabase,
  storeName = "mutations",
): OfflineQueueStore {
  return {
    async list() {
      return (
        (await idbRequest(
          db.transaction(storeName, "readonly").objectStore(storeName).getAll(),
        )) as OfflineMutation[]
      ).sort((a, b) => a.createdAt - b.createdAt);
    },
    async put(item) {
      await idbRequest(
        db.transaction(storeName, "readwrite").objectStore(storeName).put(structuredClone(item)),
      );
    },
    async remove(id) {
      await idbRequest(db.transaction(storeName, "readwrite").objectStore(storeName).delete(id));
    },
  };
}
export const offlineQueueMigration: IndexedDbMigration = {
  version: 1,
  migrate(db) {
    if (!db.objectStoreNames.contains("mutations"))
      db.createObjectStore("mutations", { keyPath: "id" });
  },
};

export interface StoredPushSubscription {
  id: string;
  userId: string;
  endpoint: string;
  expirationTime?: number | null;
  keys: { p256dh: string; auth: string };
  createdAt: number;
}
export interface PushSubscriptionStore {
  put(value: StoredPushSubscription): Promise<void>;
  remove(id: string): Promise<void>;
  list(userId: string): Promise<StoredPushSubscription[]>;
}
export function memoryPushSubscriptionStore(): PushSubscriptionStore {
  const values = new Map<string, StoredPushSubscription>();
  return {
    async put(value) {
      values.set(value.id, structuredClone(value));
    },
    async remove(id) {
      values.delete(id);
    },
    async list(userId) {
      return [...values.values()]
        .filter((value) => value.userId === userId)
        .map((value) => structuredClone(value));
    },
  };
}
export interface PushSqlClient {
  query<T = Record<string, unknown>>(sql: string, parameters?: unknown[]): Promise<{ rows: T[] }>;
}
export function postgresPushSubscriptionStore(db: PushSqlClient): PushSubscriptionStore {
  return {
    async put(value) {
      await db.query(
        `INSERT INTO wrnexus_push_subscriptions (id,user_id,endpoint,expiration_time,p256dh,auth,created_at) VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (id) DO UPDATE SET user_id=$2,endpoint=$3,expiration_time=$4,p256dh=$5,auth=$6`,
        [
          value.id,
          value.userId,
          value.endpoint,
          value.expirationTime ?? null,
          value.keys.p256dh,
          value.keys.auth,
          value.createdAt,
        ],
      );
    },
    async remove(id) {
      await db.query(`DELETE FROM wrnexus_push_subscriptions WHERE id=$1`, [id]);
    },
    async list(userId) {
      const result = await db.query<any>(
        `SELECT id,user_id AS "userId",endpoint,expiration_time AS "expirationTime",json_build_object('p256dh',p256dh,'auth',auth) AS keys,created_at AS "createdAt" FROM wrnexus_push_subscriptions WHERE user_id=$1 ORDER BY created_at`,
        [userId],
      );
      return result.rows;
    },
  };
}
export const POSTGRES_PUSH_SUBSCRIPTION_SCHEMA = `CREATE TABLE IF NOT EXISTS wrnexus_push_subscriptions (id text PRIMARY KEY,user_id text NOT NULL,endpoint text NOT NULL,expiration_time bigint,p256dh text NOT NULL,auth text NOT NULL,created_at bigint NOT NULL); CREATE INDEX IF NOT EXISTS wrnexus_push_user ON wrnexus_push_subscriptions (user_id);`;
export function createPushSubscriptionService(
  store: PushSubscriptionStore,
  options: { now?: () => number; maxPerUser?: number } = {},
) {
  const now = options.now ?? Date.now;
  const max = options.maxPerUser ?? 20;
  return {
    async subscribe(
      userId: string,
      value: {
        endpoint: string;
        expirationTime?: number | null;
        keys: { p256dh: string; auth: string };
      },
    ) {
      if (!userId.trim()) throw new Error("Push subscription user is required");
      const endpoint = new URL(value.endpoint);
      if (endpoint.protocol !== "https:") throw new Error("Push endpoint must use HTTPS");
      if (
        !value.keys?.p256dh ||
        !value.keys.auth ||
        value.keys.p256dh.length > 1024 ||
        value.keys.auth.length > 1024
      )
        throw new Error("Invalid push subscription keys");
      const existing = await store.list(userId);
      const id = await crypto.subtle
        .digest("SHA-256", new TextEncoder().encode(value.endpoint))
        .then((bytes) =>
          [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join(""),
        );
      if (!existing.some((entry) => entry.id === id) && existing.length >= max)
        throw new Error("WRN-PWA-PUSH-CAPACITY");
      const record = { id, userId, ...value, keys: { ...value.keys }, createdAt: now() };
      await store.put(record);
      return record;
    },
    unsubscribe: (id: string) => store.remove(id),
    list: (userId: string) => store.list(userId),
  };
}

const esc = (value: unknown) =>
  String(value ?? "").replace(
    /[&<>"']/g,
    (character) =>
      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]!,
  );
export function renderOfflineQueueReview(
  items: OfflineMutation[],
  conflicts: Array<{ id: string; message: string }> = [],
): string {
  return `<section data-wrn-pwa-review><h2>Offline changes</h2><ul>${items.map((item) => `<li data-mutation-id="${esc(item.id)}"><strong>${esc(item.method)}</strong> ${esc(item.endpoint)} · ${item.attempts} attempts <button type="button" data-pwa-retry="${esc(item.id)}">Retry</button><button type="button" data-pwa-remove="${esc(item.id)}">Remove</button></li>`).join("") || "<li>No pending changes</li>"}</ul><h3>Conflicts</h3><ul>${conflicts.map((item) => `<li data-conflict-id="${esc(item.id)}">${esc(item.message)} <button type="button" data-pwa-client="${esc(item.id)}">Keep local</button><button type="button" data-pwa-server="${esc(item.id)}">Use server</button></li>`).join("") || "<li>No conflicts</li>"}</ul></section>`;
}
export const PWA_REVIEW_RUNTIME = `document.addEventListener("click",event=>{const button=event.target.closest("[data-pwa-retry],[data-pwa-remove],[data-pwa-client],[data-pwa-server]");if(!button)return;const action=button.hasAttribute("data-pwa-retry")?"retry":button.hasAttribute("data-pwa-remove")?"remove":button.hasAttribute("data-pwa-client")?"client":"server";const id=button.getAttribute("data-pwa-"+action);dispatchEvent(new CustomEvent("wrnexus:pwa-review",{detail:{action,id}}))});`;
