/* PostgreSQL projection notifications wake SSE readers; the outbox remains the payload authority. */

const WORKBENCH_PROJECTION_CHANNEL = "hwlab_workbench_projection";
const RECONNECT_DELAY_MS = 500;
const hubs = new WeakMap();

export async function subscribeWorkbenchProjectionCommits(store, listener) {
  if (typeof listener !== "function") throw codedError("workbench_projection_listener_invalid", "Projection notification listener must be a function.");
  let hub = hubs.get(store);
  if (!hub) {
    hub = { store, listeners: new Set(), client: null, connectPromise: null, reconnectTimer: null, connectedOnce: false, closing: false };
    hubs.set(store, hub);
  }
  hub.listeners.add(listener);
  hub.closing = false;
  await ensureConnected(hub);
  let subscribed = true;
  return async () => {
    if (!subscribed) return;
    subscribed = false;
    hub.listeners.delete(listener);
    if (hub.listeners.size === 0) await closeHub(hub);
  };
}

async function ensureConnected(hub) {
  if (hub.client || hub.connectPromise || hub.closing || hub.listeners.size === 0) return hub.connectPromise;
  hub.connectPromise = (async () => {
    const queryClient = await hub.store.getQueryClient();
    const client = typeof queryClient?.connect === "function" ? await queryClient.connect() : queryClient;
    if (!client || typeof client.query !== "function" || typeof client.on !== "function") {
      client?.release?.();
      throw codedError("workbench_projection_notify_unsupported", "Postgres runtime adapter requires a dedicated LISTEN connection.");
    }
    const onNotification = (message) => {
      if (message?.channel !== WORKBENCH_PROJECTION_CHANNEL) return;
      const signal = parseSignal(message.payload);
      for (const notify of [...hub.listeners]) {
        try { notify(signal); } catch {}
      }
    };
    const onDisconnect = (error) => disconnectHubClient(hub, client, error);
    client.on("notification", onNotification);
    client.on("error", onDisconnect);
    client.on("end", onDisconnect);
    client.__hwlabProjectionNotifyHandlers = { onNotification, onDisconnect };
    try {
      await client.query(`LISTEN ${WORKBENCH_PROJECTION_CHANNEL}`);
    } catch (error) {
      detachClient(client, error);
      throw error;
    }
    if (hub.closing || hub.listeners.size === 0) {
      detachClient(client);
      return;
    }
    hub.client = client;
    if (hub.connectedOnce) emitRecovery(hub);
    hub.connectedOnce = true;
  })().finally(() => {
    hub.connectPromise = null;
  });
  return hub.connectPromise;
}

function disconnectHubClient(hub, client, error) {
  if (client && hub.client !== client) return;
  if (client) {
    hub.client = null;
    detachClient(client, error);
  }
  scheduleReconnect(hub, error);
}

function scheduleReconnect(hub, error) {
  if (hub.closing || hub.listeners.size === 0 || hub.reconnectTimer) return;
  hub.store.logger?.warn?.({ event: "workbench_projection_notify_disconnected", errorCode: error?.code ?? "UNKNOWN", valuesRedacted: true });
  hub.reconnectTimer = setTimeout(() => {
    hub.reconnectTimer = null;
    void ensureConnected(hub).catch((connectError) => scheduleReconnect(hub, connectError));
  }, RECONNECT_DELAY_MS);
  hub.reconnectTimer.unref?.();
}

async function closeHub(hub) {
  hub.closing = true;
  if (hub.reconnectTimer) clearTimeout(hub.reconnectTimer);
  hub.reconnectTimer = null;
  const client = hub.client;
  hub.client = null;
  if (client) {
    try { await client.query(`UNLISTEN ${WORKBENCH_PROJECTION_CHANNEL}`); } catch {}
    detachClient(client);
  }
  hubs.delete(hub.store);
}

function detachClient(client, error = null) {
  const handlers = client?.__hwlabProjectionNotifyHandlers;
  if (handlers) {
    client.off?.("notification", handlers.onNotification);
    client.off?.("error", handlers.onDisconnect);
    client.off?.("end", handlers.onDisconnect);
    delete client.__hwlabProjectionNotifyHandlers;
  }
  client?.release?.(error || undefined);
}

function emitRecovery(hub) {
  for (const notify of [...hub.listeners]) {
    try { notify({ recovery: true, valuesRedacted: true }); } catch {}
  }
}

function parseSignal(payload) {
  try {
    const value = JSON.parse(String(payload ?? "{}"));
    return {
      sessionId: text(value.sessionId),
      traceId: text(value.traceId),
      outboxSeq: nonNegativeInteger(value.outboxSeq),
      valuesRedacted: true
    };
  } catch {
    return { recovery: true, valuesRedacted: true };
  }
}

function text(value) {
  const normalized = String(value ?? "").trim();
  return normalized || null;
}

function nonNegativeInteger(value) {
  const number = Number(value);
  return Number.isInteger(number) && number >= 0 ? number : 0;
}

function codedError(code, message) {
  const error = new Error(message);
  error.code = code;
  return error;
}

export { WORKBENCH_PROJECTION_CHANNEL };
