/**
 * Native browser / runtime adapter — delegates to `globalThis.RTCPeerConnection`.
 *
 * Bun does not yet expose `RTCPeerConnection` natively (as of Bun 1.3),
 * so server-side users must inject an adapter such as `node-datachannel`
 * or `wrtc` themselves. The factory exported here is the right choice
 * for any browser / Deno / Cloudflare Workers / runtime that ships the
 * standard `RTCPeerConnection`.
 */

import {
  DEFAULT_STUN_SERVERS,
  type IDataChannel,
  type IPeerConnection,
  type IRtcFactory,
  type ShadeIceCandidate,
  type ShadeRtcConfig,
  type ShadeRtcConnectionState,
  type ShadeSessionDescription,
} from './types.js';

interface NativeRtcGlobals {
  RTCPeerConnection?: new (config?: unknown) => unknown;
  RTCIceCandidate?: new (init: ShadeIceCandidate) => unknown;
}

class NativePeerConnection implements IPeerConnection {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private readonly pc: any;

  constructor(config: ShadeRtcConfig) {
    const g = globalThis as unknown as NativeRtcGlobals;
    if (g.RTCPeerConnection === undefined) {
      throw new Error('globalThis.RTCPeerConnection is not available — pass a custom IRtcFactory');
    }
    this.pc = new g.RTCPeerConnection({
      iceServers: config.iceServers ?? DEFAULT_STUN_SERVERS,
      ...(config.iceTransportPolicy !== undefined
        ? { iceTransportPolicy: config.iceTransportPolicy }
        : {}),
      ...(config.bundlePolicy !== undefined ? { bundlePolicy: config.bundlePolicy } : {}),
    });
  }

  get connectionState(): ShadeRtcConnectionState | string {
    return this.pc.connectionState as string;
  }
  get iceConnectionState(): string {
    return this.pc.iceConnectionState as string;
  }

  createDataChannel(
    label: string,
    init?: { ordered?: boolean; maxRetransmits?: number; maxPacketLifeTime?: number },
  ): IDataChannel {
    return new NativeDataChannel(this.pc.createDataChannel(label, init));
  }

  async createOffer(): Promise<ShadeSessionDescription> {
    const desc = await this.pc.createOffer();
    return { type: 'offer', sdp: desc.sdp ?? '' };
  }
  async createAnswer(): Promise<ShadeSessionDescription> {
    const desc = await this.pc.createAnswer();
    return { type: 'answer', sdp: desc.sdp ?? '' };
  }
  async setLocalDescription(desc: ShadeSessionDescription): Promise<void> {
    await this.pc.setLocalDescription({ type: desc.type, sdp: desc.sdp });
  }
  async setRemoteDescription(desc: ShadeSessionDescription): Promise<void> {
    await this.pc.setRemoteDescription({ type: desc.type, sdp: desc.sdp });
  }
  async addIceCandidate(candidate: ShadeIceCandidate | null): Promise<void> {
    if (candidate === null) {
      // End-of-candidates — spec accepts undefined / null here.
      try {
        await this.pc.addIceCandidate();
      } catch {
        /* swallow — some impls reject */
      }
      return;
    }
    const g = globalThis as unknown as NativeRtcGlobals;
    if (g.RTCIceCandidate !== undefined) {
      await this.pc.addIceCandidate(new g.RTCIceCandidate(candidate));
    } else {
      await this.pc.addIceCandidate(candidate);
    }
  }
  close(): void {
    try {
      this.pc.close();
    } catch {
      /* swallow */
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  addEventListener(event: string, cb: any): void {
    if (event === 'icecandidate') {
      this.pc.addEventListener('icecandidate', (ev: { candidate: ShadeIceCandidate | null }) => {
        const c = ev.candidate;
        if (c === null) {
          cb({ candidate: null });
          return;
        }
        cb({
          candidate: {
            candidate: c.candidate,
            sdpMid: c.sdpMid ?? null,
            sdpMLineIndex: c.sdpMLineIndex ?? null,
            ...(c.usernameFragment !== undefined
              ? { usernameFragment: c.usernameFragment ?? null }
              : {}),
          },
        });
      });
      return;
    }
    if (event === 'datachannel') {
      this.pc.addEventListener('datachannel', (ev: { channel: unknown }) => {
        cb({ channel: new NativeDataChannel(ev.channel) });
      });
      return;
    }
    this.pc.addEventListener(event, cb);
  }
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  removeEventListener(event: string, cb: any): void {
    this.pc.removeEventListener(event, cb);
  }
}

class NativeDataChannel implements IDataChannel {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  constructor(private readonly dc: any) {}

  get readyState(): 'connecting' | 'open' | 'closing' | 'closed' {
    return this.dc.readyState as 'connecting' | 'open' | 'closing' | 'closed';
  }
  get label(): string {
    return this.dc.label as string;
  }
  get binaryType(): 'arraybuffer' | 'blob' {
    return this.dc.binaryType as 'arraybuffer' | 'blob';
  }
  set binaryType(v: 'arraybuffer' | 'blob') {
    this.dc.binaryType = v;
  }
  get bufferedAmount(): number {
    return Number(this.dc.bufferedAmount ?? 0);
  }

  send(data: ArrayBuffer | Uint8Array): void {
    if (data instanceof Uint8Array) {
      // Some adapters (Node's wrtc) require ArrayBuffer specifically.
      const ab = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
      this.dc.send(ab);
    } else {
      this.dc.send(data);
    }
  }
  close(): void {
    try {
      this.dc.close();
    } catch {
      /* swallow */
    }
  }
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  addEventListener(event: string, cb: any): void {
    if (event === 'message') {
      this.dc.addEventListener('message', (ev: { data: unknown }) => {
        const data = ev.data;
        if (data instanceof ArrayBuffer) {
          cb({ data });
          return;
        }
        const maybeBlob = data as { arrayBuffer?: () => Promise<ArrayBuffer> };
        if (typeof maybeBlob.arrayBuffer === 'function') {
          maybeBlob
            .arrayBuffer()
            .then((ab) => cb({ data: ab }))
            .catch(() => {});
          return;
        }
        cb({ data: data as ArrayBuffer });
      });
      return;
    }
    this.dc.addEventListener(event, cb);
  }
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  removeEventListener(event: string, cb: any): void {
    this.dc.removeEventListener(event, cb);
  }
}

export class NativeRtcFactory implements IRtcFactory {
  createPeerConnection(config: ShadeRtcConfig): IPeerConnection {
    return new NativePeerConnection(config);
  }
}

export const nativeRtcFactory = (): NativeRtcFactory => new NativeRtcFactory();

/** True if the runtime exposes `globalThis.RTCPeerConnection`. */
export function isNativeRtcAvailable(): boolean {
  const g = globalThis as unknown as NativeRtcGlobals;
  return typeof g.RTCPeerConnection === 'function';
}
