/* eslint-disable @typescript-eslint/no-explicit-any */
import { DataChannelInitConfig, SelectedCandidateInfo } from '../lib/types';
import { PeerConnection } from '../lib/index';
import RTCSessionDescription from './RTCSessionDescription';
import RTCDataChannel from './RTCDataChannel';
import RTCIceCandidate from './RTCIceCandidate';
import { RTCDataChannelEvent, RTCPeerConnectionIceEvent } from './Events';
import RTCSctpTransport from './RTCSctpTransport';
import * as exceptions from './Exception';
import RTCCertificate from './RTCCertificate';

// extend RTCConfiguration with peerIdentity
interface RTCConfiguration extends globalThis.RTCConfiguration {
  peerIdentity?: string;
  peerConnection?: PeerConnection;
}

export default class RTCPeerConnection extends EventTarget implements globalThis.RTCPeerConnection {
  static async generateCertificate(): Promise<RTCCertificate> {
    throw new DOMException('Not implemented');
  }

  #peerConnection: PeerConnection;
  #localOffer: ReturnType<typeof createDeferredPromise>;
  #localAnswer: ReturnType<typeof createDeferredPromise>;
  #dataChannels: Set<globalThis.RTCDataChannel>;
  #dataChannelsClosed = 0;
  #config: globalThis.RTCConfiguration;
  #canTrickleIceCandidates: boolean | null = null;
  #sctp: globalThis.RTCSctpTransport;
  #announceNegotiation = false;

  #localCandidates: globalThis.RTCIceCandidate[] = [];
  #remoteCandidates: globalThis.RTCIceCandidate[] = [];

  // events
  onconnectionstatechange: globalThis.RTCPeerConnection['onconnectionstatechange'] = null;
  // For ondatachannel we need to define type manually
  ondatachannel: ((this: globalThis.RTCPeerConnection, ev: RTCDataChannelEvent) => any) | null;
  onicecandidate: globalThis.RTCPeerConnection['onicecandidate'] = null;
  onicecandidateerror: globalThis.RTCPeerConnection['onicecandidateerror'] = null;
  oniceconnectionstatechange: globalThis.RTCPeerConnection['oniceconnectionstatechange'] = null;
  onicegatheringstatechange: globalThis.RTCPeerConnection['onicegatheringstatechange'] = null;
  onnegotiationneeded: globalThis.RTCPeerConnection['onnegotiationneeded'] = null;
  onsignalingstatechange: globalThis.RTCPeerConnection['onsignalingstatechange'] = null;
  ontrack: globalThis.RTCPeerConnection['ontrack'] = null;

  private _checkConfiguration(config: globalThis.RTCConfiguration): void {
    if (config && config.iceServers === undefined) config.iceServers = [];
    if (config && config.iceTransportPolicy === undefined) config.iceTransportPolicy = 'all';

    if (config?.iceServers === null) throw new TypeError('IceServers cannot be null');

    // Check for all the properties of iceServers
    if (Array.isArray(config?.iceServers)) {
      for (let i = 0; i < config.iceServers.length; i++) {
        if (config.iceServers[i] === null) throw new TypeError('IceServers cannot be null');
        if (config.iceServers[i] === undefined)
          throw new TypeError('IceServers cannot be undefined');
        if (Object.keys(config.iceServers[i]).length === 0)
          throw new TypeError('IceServers cannot be empty');

        // If iceServers is string convert to array
        if (typeof config.iceServers[i].urls === 'string')
          config.iceServers[i].urls = [config.iceServers[i].urls as string];

        // urls can not be empty
        if ((config.iceServers[i].urls as string[])?.some((url) => url == ''))
          throw new exceptions.SyntaxError('IceServers urls cannot be empty');

        // urls should be valid URLs and match the protocols "stun:|turn:|turns:"
        if (
          (config.iceServers[i].urls as string[])?.some((url) => {
            try {
              const parsedURL = new URL(url);

              return !/^(stun:|turn:|turns:)$/.test(parsedURL.protocol);
            } catch (error) {
              return true;
            }
          })
        )
          throw new exceptions.SyntaxError('IceServers urls wrong format');

        // If this is a turn server check for username and credential
        if ((config.iceServers[i].urls as string[])?.some((url) => url.startsWith('turn'))) {
          if (!config.iceServers[i].username)
            throw new exceptions.InvalidAccessError('IceServers username cannot be null');
          if (!config.iceServers[i].credential)
            throw new exceptions.InvalidAccessError('IceServers username cannot be undefined');
        }

        // length of urls can not be 0
        if (config.iceServers[i].urls?.length === 0)
          throw new exceptions.SyntaxError('IceServers urls cannot be empty');
      }
    }

    if (
      config &&
      config.iceTransportPolicy &&
      config.iceTransportPolicy !== 'all' &&
      config.iceTransportPolicy !== 'relay'
    )
      throw new TypeError('IceTransportPolicy must be either "all" or "relay"');
  }

  setConfiguration(config: globalThis.RTCConfiguration): void {
    this._checkConfiguration(config);
    this.#config = config;
  }

  constructor(config: RTCConfiguration = { iceServers: [], iceTransportPolicy: 'all' }) {
    super();

    this._checkConfiguration(config);
    this.#config = config;
    this.#localOffer = createDeferredPromise();
    this.#localAnswer = createDeferredPromise();
    this.#dataChannels = new Set();
    this.#canTrickleIceCandidates = null;

    try {
      const peerIdentity = (config as any)?.peerIdentity ?? `peer-${getRandomString(7)}`;
      this.#peerConnection =
        config?.peerConnection ??
        new PeerConnection(peerIdentity, {
          ...config,
          iceServers:
            config?.iceServers
              ?.map((server) => {
                const urls = Array.isArray(server.urls) ? server.urls : [server.urls];

                return urls.map((url) => {
                  if (server.username && server.credential) {
                    const [protocol, rest] = url.split(/:(.*)/);
                    return `${protocol}:${server.username}:${server.credential}@${rest}`;
                  }
                  return url;
                });
              })
              .flat() ?? [],
        });
    } catch (error) {
      if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error');
      throw new exceptions.SyntaxError(error.message);
    }

    // forward peerConnection events
    this.#peerConnection.onStateChange(() => {
      this.dispatchEvent(new Event('connectionstatechange'));
    });

    this.#peerConnection.onIceStateChange(() => {
      this.dispatchEvent(new Event('iceconnectionstatechange'));
    });

    this.#peerConnection.onSignalingStateChange(() => {
      this.dispatchEvent(new Event('signalingstatechange'));
    });

    this.#peerConnection.onGatheringStateChange(() => {
      this.dispatchEvent(new Event('icegatheringstatechange'));
    });

    this.#peerConnection.onDataChannel((channel) => {
      const dc = new RTCDataChannel(channel);
      this.#dataChannels.add(dc);
      this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel: dc }));
    });

    this.#peerConnection.onLocalDescription((sdp, type) => {
      if (type === 'offer') {
        this.#localOffer.resolve(new RTCSessionDescription({ sdp, type }));
      }

      if (type === 'answer') {
        this.#localAnswer.resolve(new RTCSessionDescription({ sdp, type }));
      }
    });

    this.#peerConnection.onLocalCandidate((candidate, sdpMid) => {
      if (sdpMid === 'unspec') {
        this.#localAnswer.reject(new Error(`Invalid description type ${sdpMid}`));
        return;
      }

      this.#localCandidates.push(new RTCIceCandidate({ candidate, sdpMid }));
      this.dispatchEvent(new RTCPeerConnectionIceEvent(new RTCIceCandidate({ candidate, sdpMid })));
    });

    // forward events to properties
    this.addEventListener('connectionstatechange', (e) => {
      this.onconnectionstatechange?.(e);
    });
    this.addEventListener('signalingstatechange', (e) => {
      this.onsignalingstatechange?.(e);
    });
    this.addEventListener('iceconnectionstatechange', (e) => {
      this.oniceconnectionstatechange?.(e);
    });
    this.addEventListener('icegatheringstatechange', (e) => {
      this.onicegatheringstatechange?.(e);
    });
    this.addEventListener('datachannel', (e) => {
      this.ondatachannel?.(e as RTCDataChannelEvent);
    });
    this.addEventListener('icecandidate', (e) => {
      this.onicecandidate?.(e as globalThis.RTCPeerConnectionIceEvent);
    });
    this.addEventListener('track', (e) => {
      this.ontrack?.(e as RTCTrackEvent);
    });
    this.addEventListener('negotiationneeded', (e) => {
      this.#announceNegotiation = true;
      this.onnegotiationneeded?.(e);
    });

    this.#sctp = new RTCSctpTransport({
      pc: this,
    });
  }

  // Extra FUnctions
  get ext_maxDataChannelId(): number {
    return this.#peerConnection.maxDataChannelId();
  }

  get ext_maxMessageSize(): number {
    return this.#peerConnection.maxMessageSize();
  }

  get ext_localCandidates(): globalThis.RTCIceCandidate[] {
    return this.#localCandidates;
  }

  get ext_remoteCandidates(): globalThis.RTCIceCandidate[] {
    return this.#remoteCandidates;
  }

  selectedCandidatePair(): {
    local: SelectedCandidateInfo;
    remote: SelectedCandidateInfo;
  } | null {
    return this.#peerConnection.getSelectedCandidatePair();
  }

  get canTrickleIceCandidates(): boolean | null {
    return this.#canTrickleIceCandidates;
  }

  get connectionState(): globalThis.RTCPeerConnectionState {
    return this.#peerConnection.state();
  }

  get iceConnectionState(): globalThis.RTCIceConnectionState {
    let state = this.#peerConnection.iceState();
    // libdatachannel uses 'completed' instead of 'connected'
    // see /webrtc/getstats.html
    if (state == 'completed') state = 'connected';
    return state;
  }

  get iceGatheringState(): globalThis.RTCIceGatheringState {
    return this.#peerConnection.gatheringState();
  }

  get currentLocalDescription(): globalThis.RTCSessionDescription {
    return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
  }

  get currentRemoteDescription(): globalThis.RTCSessionDescription {
    return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
  }

  get localDescription(): globalThis.RTCSessionDescription {
    return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
  }

  get pendingLocalDescription(): globalThis.RTCSessionDescription {
    return new RTCSessionDescription(this.#peerConnection.localDescription() as any);
  }

  get pendingRemoteDescription(): globalThis.RTCSessionDescription {
    return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
  }

  get remoteDescription(): globalThis.RTCSessionDescription {
    return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any);
  }

  get sctp(): globalThis.RTCSctpTransport {
    return this.#sctp;
  }

  get signalingState(): globalThis.RTCSignalingState {
    return this.#peerConnection.signalingState();
  }

  async addIceCandidate(candidate?: globalThis.RTCIceCandidateInit | null): Promise<void> {
    if (!candidate || !candidate.candidate) {
      return;
    }

    if (candidate.sdpMid === null && candidate.sdpMLineIndex === null) {
      throw new TypeError('sdpMid must be set');
    }

    if (candidate.sdpMid === undefined && candidate.sdpMLineIndex == undefined) {
      throw new TypeError('sdpMid must be set');
    }

    // Reject if sdpMid format is not valid
    // ??
    if (candidate.sdpMid && candidate.sdpMid.length > 3) {
      // console.log(candidate.sdpMid);
      throw new exceptions.OperationError('Invalid sdpMid format');
    }

    // We don't care about sdpMLineIndex, just for test
    if (!candidate.sdpMid && candidate.sdpMLineIndex > 1) {
      throw new exceptions.OperationError('This is only for test case.');
    }

    try {
      this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid ?? '0');
      this.#remoteCandidates.push(
        new RTCIceCandidate({
          candidate: candidate.candidate,
          sdpMid: candidate.sdpMid ?? '0',
        }),
      );
    } catch (error) {
      if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error');

      // Check error Message if contains specific message
      if (error.message.includes('remote candidate without remote description'))
        throw new exceptions.InvalidStateError(error.message);
      if (error.message.includes('Invalid candidate format'))
        throw new exceptions.OperationError(error.message);

      throw new exceptions.NotFoundError(error.message);
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  addTrack(_track, ..._streams): globalThis.RTCRtpSender {
    throw new DOMException('Not implemented');
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  addTransceiver(_trackOrKind, _init): globalThis.RTCRtpTransceiver {
    throw new DOMException('Not implemented');
  }

  close(): void {
    this.#peerConnection.close();
  }

  createAnswer(): Promise<globalThis.RTCSessionDescriptionInit | any> {
    return this.#localAnswer;
  }

  createDataChannel(label: string, opts: globalThis.RTCDataChannelInit = {}): RTCDataChannel {
    const nativeOpts: DataChannelInitConfig = {
      ...opts,
      // libdatachannel uses "unordered", opposite of RTCDataChannelInit.ordered
      unordered: opts.ordered === false ? true : false,
    };

    const channel = this.#peerConnection.createDataChannel(label, nativeOpts);
    const dataChannel = new RTCDataChannel(channel, opts);

    // ensure we can close all channels when shutting down
    this.#dataChannels.add(dataChannel);
    dataChannel.addEventListener('close', () => {
      this.#dataChannels.delete(dataChannel);
      this.#dataChannelsClosed++;
    });

    if (this.#announceNegotiation == null) {
      this.#announceNegotiation = false;
      this.dispatchEvent(new Event('negotiationneeded'));
    }

    return dataChannel;
  }

  createOffer(): Promise<globalThis.RTCSessionDescriptionInit | any> {
    return this.#localOffer;
  }

  getConfiguration(): globalThis.RTCConfiguration {
    return this.#config;
  }

  getReceivers(): globalThis.RTCRtpReceiver[] {
    throw new DOMException('Not implemented');
  }

  getSenders(): globalThis.RTCRtpSender[] {
    throw new DOMException('Not implemented');
  }

  getStats(): Promise<globalThis.RTCStatsReport> | any {
    return new Promise((resolve) => {
      const report = new Map();
      const cp = this.#peerConnection?.getSelectedCandidatePair();
      const bytesSent = this.#peerConnection?.bytesSent();
      const bytesReceived = this.#peerConnection?.bytesReceived();
      const rtt = this.#peerConnection?.rtt();

      if (!cp) {
        return resolve(report);
      }

      const localIdRs = getRandomString(8);
      const localId = 'RTCIceCandidate_' + localIdRs;
      report.set(localId, {
        id: localId,
        type: 'local-candidate',
        timestamp: Date.now(),
        candidateType: cp.local.type,
        ip: cp.local.address,
        port: cp.local.port,
      });

      const remoteIdRs = getRandomString(8);
      const remoteId = 'RTCIceCandidate_' + remoteIdRs;
      report.set(remoteId, {
        id: remoteId,
        type: 'remote-candidate',
        timestamp: Date.now(),
        candidateType: cp.remote.type,
        ip: cp.remote.address,
        port: cp.remote.port,
      });

      const candidateId = 'RTCIceCandidatePair_' + localIdRs + '_' + remoteIdRs;
      report.set(candidateId, {
        id: candidateId,
        type: 'candidate-pair',
        timestamp: Date.now(),
        localCandidateId: localId,
        remoteCandidateId: remoteId,
        state: 'succeeded',
        nominated: true,
        writable: true,
        bytesSent: bytesSent,
        bytesReceived: bytesReceived,
        totalRoundTripTime: rtt,
        currentRoundTripTime: rtt,
      });

      const transportId = 'RTCTransport_0_1';
      report.set(transportId, {
        id: transportId,
        timestamp: Date.now(),
        type: 'transport',
        bytesSent: bytesSent,
        bytesReceived: bytesReceived,
        dtlsState: 'connected',
        selectedCandidatePairId: candidateId,
        selectedCandidatePairChanges: 1,
      });

      // peer-connection'
      report.set('P', {
        id: 'P',
        type: 'peer-connection',
        timestamp: Date.now(),
        dataChannelsOpened: this.#dataChannels.size,
        dataChannelsClosed: this.#dataChannelsClosed,
      });

      return resolve(report);
    });
  }

  getTransceivers(): globalThis.RTCRtpTransceiver[] {
    return []; // throw new DOMException('Not implemented');
  }

  removeTrack(): void {
    throw new DOMException('Not implemented');
  }

  restartIce(): Promise<void> {
    throw new DOMException('Not implemented');
  }

  async setLocalDescription(description: globalThis.RTCSessionDescriptionInit): Promise<void> {
    if (description?.type !== 'offer') {
      // any other type causes libdatachannel to throw
      return;
    }

    this.#peerConnection.setLocalDescription(description?.type as any);
  }

  async setRemoteDescription(description: globalThis.RTCSessionDescriptionInit): Promise<void> {
    if (description.sdp == null) {
      throw new DOMException('Remote SDP must be set');
    }

    this.#peerConnection.setRemoteDescription(description.sdp, description.type as any);
  }
}

function createDeferredPromise(): any {
  let resolve: any, reject: any;

  const promise = new Promise(function (_resolve, _reject) {
    resolve = _resolve;
    reject = _reject;
  });

  (promise as any).resolve = resolve;
  (promise as any).reject = reject;
  return promise;
}

function getRandomString(length: number): string {
  return Math.random()
    .toString(36)
    .substring(2, 2 + length);
}
