/**
 * N-ary `ITransferTransport` fallback chain.
 *
 * Generalises the two-arg {@link FallbackTransferTransport} so a Shade
 * client can wire `[WebRTC, WebSocket, HTTP]` and have the engine try
 * them in order — switching sticky once a transport raises a
 * `TransferTransportError`.
 *
 * The chain advances exactly once per failure and never tries to "fall
 * back up" — once HTTP wins, P2P stops being attempted for that
 * `MultiTransportFallback` instance. Re-create the wrapper to re-try
 * the upper layers (which is what the SDK does on `configureWebRTC()`).
 */

import { TransferTransportError } from '../errors.js';
import type {
  ChunkAck,
  ChunkSendOptions,
  ITransferTransport,
  TransferResumeState,
} from './transport.js';

export interface NamedTransport {
  name: string;
  transport: ITransferTransport;
}

export class MultiTransportFallback implements ITransferTransport {
  /** Index into `transports` of the currently-active layer. */
  private cursor = 0;
  /** Recorded failures per name — purely diagnostic. */
  private readonly failureLog: Array<{ name: string; error: string }> = [];
  private readonly switchListeners = new Set<(from: string, to: string) => void>();

  constructor(private readonly transports: NamedTransport[]) {
    if (transports.length === 0) {
      throw new Error('MultiTransportFallback: must supply at least one transport');
    }
  }

  /** Name of the currently-active transport. */
  get activeName(): string {
    return this.transports[this.cursor]!.name;
  }

  /** Has the chain demoted at least once? */
  get hasFallenBack(): boolean {
    return this.cursor > 0;
  }

  /** Diagnostic snapshot of the failure log. */
  get failures(): readonly { name: string; error: string }[] {
    return this.failureLog;
  }

  onSwitch(cb: (from: string, to: string) => void): () => void {
    this.switchListeners.add(cb);
    return () => this.switchListeners.delete(cb);
  }

  async probe(peerAddress: string): Promise<void> {
    while (this.cursor < this.transports.length) {
      const layer = this.transports[this.cursor]!;
      try {
        await layer.transport.probe(peerAddress);
        return;
      } catch (err) {
        if (this.demoteIfTransportError(layer.name, err)) {
          continue;
        }
        throw err;
      }
    }
    throw new TransferTransportError(
      `MultiTransportFallback: all ${this.transports.length} transports unreachable`,
    );
  }

  async sendChunk(
    peerAddress: string,
    streamId: string,
    laneId: number,
    seq: number | bigint,
    bytes: Uint8Array,
    options?: ChunkSendOptions,
  ): Promise<ChunkAck> {
    while (this.cursor < this.transports.length) {
      const layer = this.transports[this.cursor]!;
      try {
        return await layer.transport.sendChunk(
          peerAddress,
          streamId,
          laneId,
          seq,
          bytes,
          options,
        );
      } catch (err) {
        if (this.demoteIfTransportError(layer.name, err)) {
          continue;
        }
        throw err;
      }
    }
    throw new TransferTransportError(
      `MultiTransportFallback: sendChunk failed across all ${this.transports.length} transports`,
    );
  }

  async fetchResumeState(
    peerAddress: string,
    streamId: string,
  ): Promise<TransferResumeState | null> {
    while (this.cursor < this.transports.length) {
      const layer = this.transports[this.cursor]!;
      try {
        return await layer.transport.fetchResumeState(peerAddress, streamId);
      } catch (err) {
        if (this.demoteIfTransportError(layer.name, err)) {
          continue;
        }
        throw err;
      }
    }
    return null;
  }

  private demoteIfTransportError(name: string, err: unknown): boolean {
    if (!(err instanceof TransferTransportError)) return false;
    this.failureLog.push({ name, error: (err as Error).message });
    const next = this.cursor + 1;
    if (next >= this.transports.length) {
      // Already on the last layer — re-throw upstream.
      return false;
    }
    const prevName = this.transports[this.cursor]!.name;
    this.cursor = next;
    const newName = this.transports[this.cursor]!.name;
    for (const cb of this.switchListeners) {
      try {
        cb(prevName, newName);
      } catch (cbErr) {
        console.warn('[MultiTransportFallback] onSwitch callback threw:', cbErr);
      }
    }
    return true;
  }
}
