import type { ShadeBridge } from '../integration/shade-bridge.js';
import {
  encodeEnvelope,
  looksLikeFileEnvelope,
  tryParseEnvelope,
  type ClassifiedEnvelope,
} from '../protocol/envelope-codec.js';
import type { RpcEnvelope } from '../schemas/envelope.js';

/**
 * Routing hooks called by the channel when it receives different envelope
 * classes. M-Files-1 ships the skeleton; M-Files-2 wires up real handlers
 * (server-side dispatcher, client-side response routing).
 */
export interface RpcChannelHooks {
  onRequest?: (from: string, env: ClassifiedEnvelope & { kind: 'request' }) => Promise<void>;
  onResponse?: (from: string, env: ClassifiedEnvelope & { kind: 'response' }) => void;
  onError?: (from: string, env: ClassifiedEnvelope & { kind: 'error' }) => void;
  onCancel?: (from: string, env: ClassifiedEnvelope & { kind: 'cancel' }) => void;
}

/**
 * `ShadeFileRpcChannel` rides on top of `Shade.send`/`Shade.onMessage`
 * and routes `shade.fs.*` JSON envelopes to registered hooks.
 *
 * Counterpart to `ShadeControlChannel` from `@shade/sdk` — but with
 * request-response semantics instead of fire-and-forget.
 *
 * One channel per `Shade` instance; the SDK's `files` getter memoizes it.
 * Both server and client roles share the same channel: the same Shade can
 * both serve files and consume them from peers.
 */
export class ShadeFileRpcChannel {
  private hooks: RpcChannelHooks = {};
  private readonly unsubscribe: () => void;
  private destroyed = false;

  constructor(private readonly shade: ShadeBridge) {
    this.unsubscribe = shade.onMessage(async (from, plaintext) => {
      if (!looksLikeFileEnvelope(plaintext)) return;
      const classified = tryParseEnvelope(plaintext);
      if (classified === null) return;
      await this.dispatch(from, classified);
    });
  }

  /**
   * Merge hooks into the current set. Returns the previous values for the
   * keys being set so callers can restore them on cleanup. Other hook
   * slots are left unchanged — the same channel can simultaneously serve
   * (`onRequest`+`onCancel`) and consume (`onResponse`+`onError`).
   */
  setHooks(hooks: RpcChannelHooks): RpcChannelHooks {
    if (this.destroyed) throw new Error('ShadeFileRpcChannel: destroyed');
    const prev: RpcChannelHooks = {};
    const keys = ['onRequest', 'onResponse', 'onError', 'onCancel'] as const;
    for (const key of keys) {
      if (key in hooks) {
        prev[key] = this.hooks[key] as never;
        this.hooks[key] = hooks[key] as never;
      }
    }
    return prev;
  }

  /**
   * Send an envelope to a peer. Encrypts via `Shade.send` (Double Ratchet)
   * then delivers the ratchet envelope to the peer's `transferRoute()`
   * control endpoint via the SDK's configured envelope transport.
   */
  async send(peerAddress: string, envelope: RpcEnvelope): Promise<void> {
    if (this.destroyed) throw new Error('ShadeFileRpcChannel: destroyed');
    const plaintext = encodeEnvelope(envelope);
    const ratchetEnvelope = await this.shade.send(peerAddress, plaintext);
    if (this.shade.deliverControlEnvelope === undefined) {
      throw new Error(
        'ShadeFileRpcChannel: shade.deliverControlEnvelope is required — call shade.configureTransfers({ resolveBaseUrl }) before using the files namespace.',
      );
    }
    await this.shade.deliverControlEnvelope(peerAddress, ratchetEnvelope);
  }

  destroy(): void {
    if (this.destroyed) return;
    this.destroyed = true;
    this.unsubscribe();
    this.hooks = {};
  }

  private async dispatch(from: string, classified: ClassifiedEnvelope): Promise<void> {
    try {
      switch (classified.kind) {
        case 'request':
          if (this.hooks.onRequest !== undefined) {
            await this.hooks.onRequest(from, classified);
          }
          return;
        case 'response':
          this.hooks.onResponse?.(from, classified);
          return;
        case 'error':
          this.hooks.onError?.(from, classified);
          return;
        case 'cancel':
          this.hooks.onCancel?.(from, classified);
          return;
      }
    } catch (err) {
      console.error('[ShadeFileRpcChannel] dispatch error:', err);
    }
  }
}
