/**
 * ipc.js — Cross-worker Redis Pub/Sub bridge for PM2 cluster mode.
 *
 * In cluster mode each PM2 worker has isolated memory (userSockets, activeMatches).
 * When two matched players land on different workers, this module routes:
 *  - Outbound WS frames: owning match worker → target player's worker → player WS
 *  - Inbound match.input: player's worker → owning match worker → match.onInput()
 *  - match.reconnect notifications: new worker → owning match worker
 *
 * If Redis is unavailable (in-memory fallback) or REDIS_URL is not set,
 * all IPC calls are no-ops — the server degrades gracefully to single-worker behaviour.
 */

import { createClient } from 'redis';
import { config } from './config.js';

export const WORKER_ID = String(process.env.pm_id ?? '0');

function ipcChannel(workerId) {
  return `${config.redisKeyPrefix}ipc:w${workerId}`;
}

function userWorkerKey(userId) {
  return `${config.redisKeyPrefix}ws:w:${userId}`;
}

function matchWorkerKey(matchId) {
  return `${config.redisKeyPrefix}match:w:${matchId}`;
}

/**
 * Create a dedicated subscriber Redis client and subscribe to this worker's IPC channel.
 * Returns the subscriber client (or null when Redis is unavailable).
 * @param {object} redisCommandClient - the already-connected Redis command client
 * @param {function} onMessage - called with the parsed JSON payload for each IPC message
 */
export async function setupIpcSubscriber(redisCommandClient, onMessage) {
  if (!redisCommandClient || redisCommandClient.mode === 'memory') return null;
  if (!config.redisUrl) return null;

  const sub = createClient({
    url: config.redisUrl,
    socket: { connectTimeout: 3000, reconnectStrategy: (retries) => Math.min(retries * 500, 5000) },
  });
  sub.on('error', (err) => console.error('[ipc] subscriber error', err));

  try {
    await sub.connect();
  } catch (err) {
    console.warn('[ipc] subscriber connect failed, cross-worker routing disabled:', err.message);
    return null;
  }

  const channel = ipcChannel(WORKER_ID);
  await sub.subscribe(channel, (raw) => {
    try {
      onMessage(JSON.parse(raw));
    } catch (e) {
      console.error('[ipc] malformed message', e);
    }
  });

  console.log(`[ipc] worker ${WORKER_ID} subscribed to ${channel}`);
  return sub;
}

/**
 * Publish an IPC payload to a specific worker's channel.
 */
export async function ipcSend(redis, targetWorkerId, payload) {
  if (!redis || redis.mode === 'memory') return;
  try {
    await redis.publish(ipcChannel(targetWorkerId), JSON.stringify(payload));
  } catch (e) {
    console.error('[ipc] publish error', e);
  }
}

/** Register that userId's WebSocket lives on this worker. TTL 2 h. */
export async function registerUserWorker(redis, userId) {
  if (!redis || redis.mode === 'memory') return;
  try {
    await redis.set(userWorkerKey(userId), WORKER_ID, { EX: 7200 });
  } catch (e) {
    console.error('[ipc] registerUserWorker error', e);
  }
}

/** Remove worker registration when user disconnects. */
export async function unregisterUserWorker(redis, userId) {
  if (!redis || redis.mode === 'memory') return;
  try {
    await redis.del(userWorkerKey(userId));
  } catch (e) {
    console.error('[ipc] unregisterUserWorker error', e);
  }
}

/** Look up which worker currently holds userId's WS. Returns null if unknown. */
export async function getUserWorker(redis, userId) {
  if (!redis || redis.mode === 'memory') return null;
  try {
    return await redis.get(userWorkerKey(userId));
  } catch {
    return null;
  }
}

/** Register that matchId's Match object lives on this worker. TTL 4 h. */
export async function registerMatchWorker(redis, matchId) {
  if (!redis || redis.mode === 'memory') return;
  try {
    await redis.set(matchWorkerKey(matchId), WORKER_ID, { EX: 14400 });
  } catch (e) {
    console.error('[ipc] registerMatchWorker error', e);
  }
}

/** Remove ownership mapping when match ends. */
export async function unregisterMatchWorker(redis, matchId) {
  if (!redis || redis.mode === 'memory') return;
  try {
    await redis.del(matchWorkerKey(matchId));
  } catch (e) {
    console.error('[ipc] unregisterMatchWorker error', e);
  }
}

/** Look up which worker owns matchId's Match object. Returns null if unknown. */
export async function getMatchWorker(redis, matchId) {
  if (!redis || redis.mode === 'memory') return null;
  try {
    return await redis.get(matchWorkerKey(matchId));
  } catch {
    return null;
  }
}
