import log from 'loglevel';
import {
  mediaDevices,
  RTCPeerConnection,
  RTCSessionDescription,
  MediaStream,
} from 'react-native-webrtc';
import type { MediaTrackConstraints } from 'react-native-webrtc/lib/typescript/Constraints';
import type RTCIceCandidateEvent from 'react-native-webrtc/lib/typescript/RTCIceCandidateEvent';
import type { RTCSessionDescriptionInit } from 'react-native-webrtc/lib/typescript/RTCSessionDescription';
import type RTCTrackEvent from 'react-native-webrtc/lib/typescript/RTCTrackEvent';
import type { CallOptions } from './call-options';
import type { DeferredPromise } from './promise';
import { createDeferredPromise } from './promise';
import type { WebRTCReporter } from './webrtc-reporter';

type MediaConstraints = {
  audio?: boolean | MediaTrackConstraints;
  video?: boolean | MediaTrackConstraints;
};

export type TrickleIceCandidate = {
  candidate: string;
  sdpMid?: string | null;
  sdpMLineIndex?: number | null;
};

export class Peer {
  public static createOffer = async (callOptions: CallOptions) => {
    const peer = await new Peer(callOptions)
      .createPeerConnection()
      .attachLocalStream({ audio: true, video: false })
      .then((peer) => peer.createOffer());

    if (!peer.useTrickleIce) {
      await peer.waitForIceGatheringComplete();
    }

    return peer;
  };

  public mediaConstraints: MediaConstraints;
  private instance: RTCPeerConnection | null;
  private options: CallOptions;
  private iceGatheringComplete: DeferredPromise<boolean> | null;
  private reporter: WebRTCReporter | null = null;
  private pendingRemoteCandidates: TrickleIceCandidate[] = [];
  private pendingLocalCandidates: TrickleIceCandidate[] = [];
  private pendingLocalEndOfCandidates = false;
  private localCandidateDispatchEnabled = false;

  /** Callback for local Trickle ICE candidates. Call owns signaling transport. */
  public onLocalCandidate: ((candidate: TrickleIceCandidate) => void) | null = null;

  /** Callback for local ICE completion when Trickle ICE is enabled. */
  public onLocalEndOfCandidates: (() => void) | null = null;

  /** Callback for logging peer events to call report collector */
  public onPeerEventLog: ((event: string, context: Record<string, unknown>) => void) | null = null;

  constructor(options: CallOptions) {
    this.instance = null;
    this.iceGatheringComplete = null;
    this.mediaConstraints = { audio: true, video: false };
    this.options = options;
  }

  /**
   * Set the WebRTC reporter for debug stats collection
   */
  public setReporter(reporter: WebRTCReporter | null): void {
    this.reporter = reporter;
  }

  /**
   * Get the underlying RTCPeerConnection instance
   */
  public getPeerConnection(): RTCPeerConnection | null {
    return this.instance;
  }

  /**
   * Get the ICE servers configuration
   */
  public getIceServers(): RTCIceServer[] {
    return this.getIceServersInternal();
  }

  public close = () => {
    log.debug('[Peer] Closing peer connection');
    if (this.instance) {
      this.instance.close();
      this.instance = null;
    }
    if (this.iceGatheringComplete) {
      // Inbound pre-accept flows may never await ICE completion. Attach a handler
      // before rejecting so close() is marked handled while awaited callers still reject.
      this.iceGatheringComplete.promise.catch(() => {});
      this.iceGatheringComplete.reject(new Error('Peer connection closed'));
      this.iceGatheringComplete = null;
    }
    this.options.localStream?.getTracks().forEach((track) => {
      track.stop();
    });
    this.options.localStream = undefined;
    this.options.remoteStream?.getTracks().forEach((track) => {
      track.stop();
    });
    this.options.remoteStream = undefined;
    this.pendingRemoteCandidates = [];
    this.pendingLocalCandidates = [];
    this.pendingLocalEndOfCandidates = false;
    this.localCandidateDispatchEnabled = false;
    return this;
  };

  public setRemoteDescription = async (session: RTCSessionDescriptionInit) => {
    if (!this.instance) {
      throw new Error('[Peer] Peer connection not created');
    }
    log.debug('[Peer] Setting remote description', session);
    await this.instance.setRemoteDescription(new RTCSessionDescription(session)).catch((error) => {
      log.error('[Peer] Error setting remote description', error);
    });
    await this.flushPendingRemoteCandidates();
    return this;
  };
  public get remoteDescription() {
    if (!this.instance) {
      throw new Error('[Peer] Peer connection not created');
    }
    return this.instance.remoteDescription;
  }

  public get localDescription() {
    if (!this.instance) {
      throw new Error('[Peer] Peer connection not created');
    }
    return this.instance.localDescription;
  }

  public createPeerConnection = () => {
    log.debug('[Peer] Creating peer connection');

    this.iceGatheringComplete = createDeferredPromise(1000);
    const instance = new RTCPeerConnection({
      bundlePolicy: this.options.peerConnectionOptions?.bundlePolicy || 'max-compat',
      iceServers: this.getIceServers(),
      iceTransportPolicy: this.options.peerConnectionOptions?.iceTransportPolicy || 'all',
      rtcpMuxPolicy: this.options.peerConnectionOptions?.rtcpMuxPolicy || 'require',
      iceCandidatePoolSize: this.options.peerConnectionOptions?.prefetchIceCandidates ? 10 : 0,
    });

    this.instance = instance;
    this.instance.addEventListener('icecandidate', this.onIceCandidate);
    this.instance.addEventListener('icecandidateerror', this.onIceCandidateError);
    this.instance.addEventListener('icegatheringstatechange', this.onIceGatheringStateChange);

    this.instance.addEventListener('connectionstatechange', this.onConnectionStateChange);
    this.instance.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
    this.instance.addEventListener('signalingstatechange', this.onSignalingStateChange);
    this.instance.addEventListener('track', this.onTrackEvent);

    return this;
  };

  public createAnswer = async () => {
    if (!this.instance) {
      throw new Error('[Peer] Cannot create answer before peer connection');
    }
    const answer = await this.instance.createAnswer();
    await this.instance.setLocalDescription(answer).catch((error) => {
      log.error('[Peer] Error setting local description', error);
    });

    return this;
  };
  public createOffer = async () => {
    if (!this.instance) {
      throw new Error('[Peer] Cannot create offer before peer connection');
    }
    const offer = await this.instance.createOffer({
      offerToReceiveAudio: true,
      offerToReceiveVideo: false,
    });

    await this.instance.setLocalDescription(offer).catch((error) => {
      log.error('[Peer] Error setting local description', error);
    });

    return this;
  };

  public attachLocalStream = async (constraints: MediaConstraints) => {
    log.debug('[Peer] Attaching local stream', constraints);
    if (!this.instance) {
      throw new Error('[Peer] Peer connection not created');
    }
    this.mediaConstraints = constraints;

    if (this.options.localStream == null) {
      this.options.localStream = await mediaDevices.getUserMedia(constraints);
    }

    this.options.localStream.getTracks().forEach((track) => {
      log.debug('[Peer] Adding track', track);
      this.instance?.addTrack(track, this.options.localStream!);
    });

    return this;
  };

  public waitForIceGatheringComplete = async () => {
    if (!this.iceGatheringComplete) {
      throw new Error('Ice gathering not started');
    }
    await this.iceGatheringComplete.promise;
    log.debug('[Peer] ICE gathering complete');
    return this;
  };

  public get useTrickleIce(): boolean {
    return Boolean(this.options.peerConnectionOptions?.useTrickleIce);
  }

  public flushPendingLocalCandidates = () => {
    this.localCandidateDispatchEnabled = true;
    while (this.pendingLocalCandidates.length > 0) {
      const candidate = this.pendingLocalCandidates.shift()!;
      this.onLocalCandidate?.(candidate);
    }
    if (this.pendingLocalEndOfCandidates) {
      this.pendingLocalEndOfCandidates = false;
      this.onLocalEndOfCandidates?.();
    }
    return this;
  };

  public clearPendingLocalCandidates = () => {
    this.pendingLocalCandidates = [];
    this.pendingLocalEndOfCandidates = false;
    return this;
  };

  public addRemoteCandidate = async (candidate: TrickleIceCandidate) => {
    if (!this.instance) {
      throw new Error('[Peer] Peer connection not created');
    }
    if (!this.instance.remoteDescription) {
      log.debug('[Peer] Queueing remote ICE candidate until remote description is set');
      this.pendingRemoteCandidates.push(candidate);
      return this;
    }
    // react-native-webrtc accepts RTCIceCandidateInit here, but its TS surface
    // can lag the native implementation.
    await (this.instance as any).addIceCandidate(candidate).catch((error: unknown) => {
      log.error('[Peer] Error adding remote ICE candidate', error);
    });
    return this;
  };

  public addRemoteEndOfCandidates = async () => {
    if (!this.instance) {
      throw new Error('[Peer] Peer connection not created');
    }
    if (!this.instance.remoteDescription) {
      return this;
    }
    if (typeof (this.instance as any).addIceCandidate === 'function') {
      // Some RN WebRTC versions ignore or reject null end-of-candidates.
      // Treat that as non-fatal because candidates have already been delivered.
      await (this.instance as any).addIceCandidate(null).catch((error: unknown) => {
        log.debug('[Peer] Remote end-of-candidates ignored by peer connection', error);
      });
    }
    return this;
  };

  private flushPendingRemoteCandidates = async () => {
    while (this.pendingRemoteCandidates.length > 0) {
      const candidate = this.pendingRemoteCandidates.shift()!;
      await this.addRemoteCandidate(candidate);
    }
  };

  private emitOrQueueLocalCandidate(candidate: TrickleIceCandidate) {
    if (this.localCandidateDispatchEnabled) {
      this.onLocalCandidate?.(candidate);
      return;
    }
    this.pendingLocalCandidates.push(candidate);
  }

  private emitOrQueueLocalEndOfCandidates() {
    if (this.localCandidateDispatchEnabled) {
      this.onLocalEndOfCandidates?.();
      return;
    }
    this.pendingLocalEndOfCandidates = true;
  }

  public setMediaStreamState = (stream: MediaStream, enabled: boolean) => {
    log.debug('[Peer] Setting media stream state', { enabled, stream });
    if (!stream) {
      throw new Error('[Peer] No media stream to set state');
    }
    stream.getTracks().forEach((track) => {
      log.debug('[Peer] Track state set', { track, enabled });
      track.enabled = enabled;
    });
    return this;
  };

  private onIceGatheringStateChange = () => {
    const iceGatheringState = this.instance?.iceGatheringState;
    log.debug('[Peer] ICE gathering state change', iceGatheringState);

    // Report to WebRTCReporter if available
    if (this.reporter && iceGatheringState) {
      this.reporter.onIceGatheringStateChange(iceGatheringState);
    }

    // Log to call report collector
    if (iceGatheringState) {
      this.onPeerEventLog?.('ICE gathering state changed', { state: iceGatheringState });
    }

    if (iceGatheringState === 'complete') {
      log.debug('[Peer] ICE gathering complete');
      this.iceGatheringComplete?.resolve(true);
      this.iceGatheringComplete = null;
    }
  };

  private onIceCandidate = (ev: RTCIceCandidateEvent<'icecandidate'>) => {
    log.debug('[Peer] ICE candidate', ev);

    // Report to WebRTCReporter if available
    if (this.reporter && ev.candidate) {
      this.reporter.onIceCandidate(ev.candidate as unknown as RTCIceCandidate);
    }

    if (this.useTrickleIce) {
      if (ev.candidate) {
        const candidate = ev.candidate as unknown as TrickleIceCandidate;
        this.emitOrQueueLocalCandidate({
          candidate: candidate.candidate,
          sdpMid: candidate.sdpMid,
          sdpMLineIndex: candidate.sdpMLineIndex,
        });
      } else {
        this.emitOrQueueLocalEndOfCandidates();
        this.iceGatheringComplete?.resolve(true);
      }
      return;
    }

    this.iceGatheringComplete?.resolve(true);
    return;
  };

  private onIceCandidateError = (event: RTCIceCandidateEvent<'icecandidateerror'>) => {
    log.error('[Peer] ICE candidate error', event);
  };

  private getIceServersInternal() {
    if (
      this.options.peerConnectionOptions?.iceServers &&
      this.options.peerConnectionOptions?.iceServers.length > 0
    ) {
      return this.options.peerConnectionOptions.iceServers;
    }
    return [
      {
        urls: 'stun:stun.l.google.com:19302',
      },
    ];
  }

  private onTrackEvent = (event: RTCTrackEvent<'track'>) => {
    log.debug('[Peer] Remote track event', event);
    if (event.streams && event.streams.length > 0) {
      this.options.remoteStream = event.streams[0];
    }

    // Report to WebRTCReporter if available
    if (this.reporter && event.track) {
      this.reporter.onTrack(event.track as unknown as MediaStreamTrack);
    }
  };

  private onSignalingStateChange = () => {
    const signalingState = this.instance?.signalingState;
    log.debug('[Peer] Signaling state change', signalingState);

    // Report to WebRTCReporter if available
    if (this.reporter && signalingState) {
      this.reporter.onSignalingStateChange(signalingState);
    }

    // Log to call report collector
    if (signalingState) {
      this.onPeerEventLog?.('Signaling state changed', { state: signalingState });
    }
  };

  private onIceConnectionStateChange = () => {
    const iceConnectionState = this.instance?.iceConnectionState;
    log.debug('[Peer] ICE connection state change', iceConnectionState);

    // Report to WebRTCReporter if available
    if (this.reporter && iceConnectionState) {
      this.reporter.onIceConnectionStateChange(iceConnectionState);
    }

    // Log to call report collector
    if (iceConnectionState) {
      this.onPeerEventLog?.('ICE connection state changed', { state: iceConnectionState });
    }
  };

  private onConnectionStateChange = () => {
    log.debug('[Peer] Connection state change', this.instance?.connectionState);
  };
}
