// src/core/messaging/RequestReplyHelper.ts
//
// Request/Reply Helper for Internal RPC
//
// This helper uses IRequestReplyEnvelope - a SIMPLIFIED envelope format for
// intra-root RPC. This is distinct from MxEnvelopeV1 which is the FULL
// envelope format for cross-root federation.
//
// Envelope Hierarchy:
// ┌─────────────────────────────────────────────────────────────────┐
// │ MxEnvelopeV1 (packages/matrix-transport)                       │
// │   - Full cross-root envelope with ver, msgType, from, to, etc  │
// │   - Used by: FederationPeer, CrossRootClient                   │
// ├─────────────────────────────────────────────────────────────────┤
// │ IRequestReplyEnvelope (this file)                              │
// │   - Simplified { correlationId, replyTo, params }              │
// │   - Used by: browser elements, MatrixActor, internal RPC       │
// │   - Translation happens at FederationPeer ingress              │
// └─────────────────────────────────────────────────────────────────┘
//
// The FederationPeer unwraps MxEnvelopeV1 → IRequestReplyEnvelope at ingress,
// and wraps IRequestReplyResponse → MxEnvelopeV1 at egress.

import type { ITransportAdapter } from '../../engine/remoting/ITransportAdapter';
import type { IRequestReplyEnvelope } from '../../engine/remoting/IRequestReplyEnvelope';
import { isRequestReplyEnvelope } from '../../engine/remoting/IRequestReplyEnvelope';
import type { IRequestReplyResponse } from '../../engine/remoting/IRequestReplyResponse';
import { TopicRouter } from '../../engine/messaging/TopicRouter';
import type { MxEnvelope } from '../../engine/messaging/MxEnvelope';
import { getActiveTraceContext } from '../../telemetry/tracing';

const textEncoder = new TextEncoder();

interface INatsReplyPublisherLike {
  publish(subject: string, data?: Uint8Array): void;
}

function isNatsReplyPublisherLike(value: unknown): value is INatsReplyPublisherLike {
  if (!value || typeof value !== 'object') {
    return false;
  }
  const candidate = value as {
    publish?: unknown;
  };
  return typeof candidate.publish === 'function';
}

function publishReplyPayload(
  transport: ITransportAdapter,
  topic: string,
  payload: IRequestReplyResponse | IRequestReplyResponse<unknown>,
): void {
  // Detect raw NATS reply subjects from cross-root requests.
  // DNS format: {root}.$reply.{cid}; protocol request replies use _INBOX.*.
  // NATS service imports can rewrite protocol replies to _R_.*.
  if (topic.includes('.$reply.') || topic.startsWith('_INBOX.') || topic.startsWith('_R_.')) {
    const rawClient = transport.getUnderlyingClient();
    if (isNatsReplyPublisherLike(rawClient)) {
      rawClient.publish(topic, textEncoder.encode(JSON.stringify(payload)));
      return;
    }
  }
  transport.publish(topic, payload);
}

// Re-export the types for convenience
export type { IRequestReplyEnvelope, IRequestReplyResponse };
export { isRequestReplyEnvelope };

/**
 * Helper for Request/Reply pattern (internal RPC).
 *
 * Eliminates duplicate R/R handling code throughout MatrixActor.
 * Provides a single source of truth for request/reply semantics.
 *
 * NOTE: This uses the simplified IRequestReplyEnvelope format.
 * For cross-root federation, see MxEnvelopeV1 in packages/matrix-transport.
 */
export class RequestReplyHelper {
  /**
   * Check if payload is a request/reply envelope.
   * Delegates to the canonical type guard in ITransportAdapter.
   */
  static isEnvelope(payload: unknown): payload is IRequestReplyEnvelope {
    return isRequestReplyEnvelope(payload);
  }

  /**
   * Execute handler with automatic reply handling.
   *
   * If payload is a RequestReplyEnvelope:
   * - Extracts params from IRequestReplyEnvelope.params or MxEnvelope.payload
   * - Calls handler with params
   * - Publishes success/error to replyTo
   *
   * If payload is plain:
   * - Calls handler with payload directly
   *
   * @param transport - Transport adapter for publishing replies
   * @param payload - The incoming message payload
   * @param handler - The handler function to execute
   * @returns The handler result (or void for fire-and-forget)
   */
  static async executeWithReply<T>(
    transport: ITransportAdapter,
    payload: unknown,
    handler: (params: unknown) => Promise<T>
  ): Promise<T | void> {
    if (!isRequestReplyEnvelope(payload)) {
      return handler(payload);
    }

    const { correlationId, replyTo } = payload;
    const params = 'params' in payload
      ? payload.params
      : ('payload' in payload ? payload.payload : undefined);

    try {
      const result = await handler(params);

      publishReplyPayload(transport, replyTo, {
        ok: true,
        correlationId,
        result,
      } as IRequestReplyResponse<T>);

      return result;
    } catch (err) {
      publishReplyPayload(transport, replyTo, {
        ok: false,
        correlationId,
        error: {
          message: err instanceof Error ? err.message : String(err),
        },
      } as IRequestReplyResponse);

      throw err;
    }
  }

  /**
   * Execute handler with reply, but don't rethrow errors.
   *
   * Same as executeWithReply but catches errors and returns undefined.
   * Use when the caller doesn't need to handle errors.
   */
  static async executeWithReplySafe<T>(
    transport: ITransportAdapter,
    payload: unknown,
    handler: (params: unknown) => Promise<T>
  ): Promise<T | void> {
    try {
      return await RequestReplyHelper.executeWithReply(transport, payload, handler);
    } catch {
      // Error already sent to replyTo, just swallow here
      return undefined;
    }
  }

  /**
   * Send a request and wait for reply.
   *
   * Creates a temporary subscription for the reply, sends the request
   * as MxEnvelope to the target's $inbox, and returns a promise that
   * resolves with the response.
   *
   * @param transport - Transport adapter for pub/sub
   * @param targetMount - The mount path of the target component
   * @param op - The command name (operation)
   * @param params - Request parameters (payload)
   * @param timeoutMs - Timeout in milliseconds (default 5000)
   * @returns Promise resolving to the result
   */
  static async request<T>(
    transport: ITransportAdapter,
    targetMount: string,
    op: string,
    params?: unknown,
    timeoutMs = 5000
  ): Promise<T> {
    const correlationId = `req-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    const replyTo = `$replies.${correlationId}`;

    return new Promise<T>((resolve, reject) => {
      const timeout = setTimeout(() => {
        unsub();
        reject(new Error(`Request to ${targetMount}/${op} timed out after ${timeoutMs}ms`));
      }, timeoutMs);

      const unsub = transport.subscribe(replyTo, (response: unknown) => {
        clearTimeout(timeout);
        unsub();

        const res = response as IRequestReplyResponse<T>;
        if (res.ok) {
          resolve(res.result as T);
        } else {
          reject(new Error(res.error?.message ?? 'Unknown error'));
        }
      });

      // Publish as MxEnvelope to target's $inbox
      const inboxTopic = TopicRouter.inbox(targetMount);
      const traceCtx = getActiveTraceContext();
      transport.publish(inboxTopic, {
        op,
        payload: params ?? {},
        correlationId,
        replyTo,
        ...(traceCtx ? { traceId: traceCtx.traceId, spanId: traceCtx.spanId } : {}),
      } as MxEnvelope);
    });
  }

  /**
   * Send a success reply for a request/reply envelope.
   *
   * @param transport - Transport adapter
   * @param envelope - The original request envelope
   * @param result - The result to send
   */
  static sendSuccessReply<T>(
    transport: ITransportAdapter,
    envelope: IRequestReplyEnvelope,
    result: T
  ): void {
    publishReplyPayload(transport, envelope.replyTo, {
      ok: true,
      correlationId: envelope.correlationId,
      result,
    } as IRequestReplyResponse<T>);
  }

  /**
   * Send an error reply for a request/reply envelope.
   *
   * @param transport - Transport adapter
   * @param envelope - The original request envelope
   * @param error - The error to send
   */
  static sendErrorReply(
    transport: ITransportAdapter,
    envelope: IRequestReplyEnvelope,
    error: Error | string
  ): void {
    publishReplyPayload(transport, envelope.replyTo, {
      ok: false,
      correlationId: envelope.correlationId,
      error: {
        message: error instanceof Error ? error.message : error,
      },
    } as IRequestReplyResponse);
  }
}
