/**
 * Socket message contract.
 *
 * Two message directions:
 *   Intent (GM → all):   scrying-pool.visibility.set    { opId, userId, targetState, baseRevision }
 *   Echo   (all ← GM):   scrying-pool.visibility.updated { opId, userId, state, revision }
 *
 * Validated at both send and receive. Payload ≥ 4096 bytes → throw before emit.
 *
 * @module contracts/socket-message
 */

/** @typedef {'scrying-pool.visibility.set'|'scrying-pool.visibility.updated'|'scrying-pool.preset.apply'|'scrying-pool.preset.applied'} SocketEventName */

/**
 * @typedef {Object} SocketIntentPayload
 * @property {string} opId - Unique operation ID (non-empty string).
 * @property {string} userId - Target participant userId (non-empty string).
 * @property {string} targetState - Desired VisibilityState.
 * @property {number} baseRevision - Revision counter when the GM issued the intent (for latest-revision-wins guard).
 */

/**
 * @typedef {Object} SocketEchoPayload
 * @property {string} opId - Matches the originating intent opId.
 * @property {string} userId - Affected participant userId.
 * @property {string} state - Authoritative VisibilityState after this operation.
 * @property {number} revision - Monotonically increasing revision counter.
 */

/**
 * @typedef {Object} SocketMessage
 * @property {SocketEventName} event - Socket event name.
 * @property {SocketIntentPayload|SocketEchoPayload} payload - Message payload.
 */

export const SOCKET_EVENTS = Object.freeze({
  VISIBILITY_SET: "scrying-pool.visibility.set",
  VISIBILITY_UPDATED: "scrying-pool.visibility.updated",
  PRESET_APPLY: "scrying-pool.preset.apply",
  PRESET_APPLIED: "scrying-pool.preset.applied",
});

export const MAX_PAYLOAD_BYTES = 4096;

/**
 * Creates a socket intent message (GM → all clients).
 * @param {string} opId - Unique operation ID.
 * @param {string} userId - Target participant userId.
 * @param {string} targetState - Desired VisibilityState.
 * @param {number} baseRevision - Revision counter at time of intent.
 * @returns {SocketMessage}
 */
export function createSocketIntentMessage(opId, userId, targetState, baseRevision) {
  return {
    event: SOCKET_EVENTS.VISIBILITY_SET,
    payload: { opId, userId, targetState, baseRevision },
  };
}

/**
 * Creates a socket echo message (authoritative broadcast to all clients).
 * @param {string} opId - Originating intent opId.
 * @param {string} userId - Affected participant userId.
 * @param {string} state - Authoritative VisibilityState.
 * @param {number} revision - Revision counter.
 * @returns {SocketMessage}
 */
export function createSocketEchoMessage(opId, userId, state, revision) {
  return {
    event: SOCKET_EVENTS.VISIBILITY_UPDATED,
    payload: { opId, userId, state, revision },
  };
}

/**
 * Validates a SocketMessage DTO. Throws TypeError on any violation.
 * @param {unknown} data - Value to validate.
 * @returns {SocketMessage} The validated message.
 * @throws {TypeError} If data fails validation.
 */
export function isValidSocketMessage(data) {
  if (data === null || typeof data !== "object") {
    throw new TypeError("SocketMessage: must be an object");
  }
  const obj = /** @type {Record<string, unknown>} */ (data);
  const { event, payload, ...rest } = obj;
  if (Object.keys(rest).length > 0) {
    throw new TypeError(`SocketMessage: unknown keys: ${Object.keys(rest).join(", ")}`);
  }
  if (!Object.values(SOCKET_EVENTS).includes(/** @type {any} */ (event))) {
    throw new TypeError(`SocketMessage: unknown event "${event}"`);
  }
  if (payload === null || typeof payload !== "object") {
    throw new TypeError("SocketMessage: payload must be an object");
  }
  const p = /** @type {Record<string, unknown>} */ (payload);
  // Validate intent payload
  if (event === SOCKET_EVENTS.VISIBILITY_SET) {
    const { opId, userId, targetState, baseRevision, ...payloadRest } = p;
    if (Object.keys(payloadRest).length > 0) {
      throw new TypeError(`SocketMessage intent: unknown payload keys: ${Object.keys(payloadRest).join(", ")}`);
    }
    if (typeof opId !== "string" || opId.length === 0) {
      throw new TypeError("SocketMessage: opId must be a non-empty string");
    }
    if (typeof userId !== "string" || userId.length === 0) {
      throw new TypeError("SocketMessage: userId must be a non-empty string");
    }
    if (typeof targetState !== "string" || targetState.length === 0) {
      throw new TypeError("SocketMessage: targetState must be a non-empty string");
    }
    if (typeof baseRevision !== "number" || !Number.isFinite(baseRevision) || baseRevision < 0) {
      throw new TypeError("SocketMessage: baseRevision must be a finite non-negative number");
    }
  }
  // Validate echo payload
  if (event === SOCKET_EVENTS.VISIBILITY_UPDATED) {
    const { opId, userId, state, revision, ...payloadRest } = p;
    if (Object.keys(payloadRest).length > 0) {
      throw new TypeError(`SocketMessage echo: unknown payload keys: ${Object.keys(payloadRest).join(", ")}`);
    }
    if (typeof opId !== "string" || opId.length === 0) {
      throw new TypeError("SocketMessage: opId must be a non-empty string");
    }
    if (typeof userId !== "string" || userId.length === 0) {
      throw new TypeError("SocketMessage: userId must be a non-empty string");
    }
    if (typeof state !== "string" || state.length === 0) {
      throw new TypeError("SocketMessage: state must be a non-empty string");
    }
    if (typeof revision !== "number" || !Number.isFinite(revision) || revision < 0) {
      throw new TypeError("SocketMessage: revision must be a finite non-negative number");
    }
  }
  return /** @type {SocketMessage} */ (data);
}
