import TrackerClient, {
  PeerConnection,
  TrackerClientEvents,
} from "bittorrent-tracker";
import { CoreEventMap, StreamConfig, StreamWithSegments } from "../types.js";
import debug from "debug";
import * as PeerUtil from "../utils/peer.js";
import * as LoggerUtils from "../utils/logger.js";
import { Peer } from "./peer.js";
import { EventTarget } from "../utils/event-target.js";
import { utf8ToUintArray } from "../utils/utils.js";

type PeerItem = {
  peer?: Peer;
  potentialConnections: Set<PeerConnection>;
};

type P2PTrackerClientEventHandlers = {
  onPeerConnected: (peer: Peer) => void;
  onSegmentRequested: (
    peer: Peer,
    segmentExternalId: number,
    requestId: number,
    bytesFrom?: number,
  ) => void;
  onSegmentsAnnouncement: () => void;
};

function isSafariOrWkWebview() {
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
  const isWkWebview =
    /\b(iPad|iPhone|Macintosh).*AppleWebKit(?!.*Safari)/i.test(
      navigator.userAgent,
    );

  return isSafari || isWkWebview;
}

export class P2PTrackerClient {
  private static readonly PEER_ID_BY_INFO_HASH = new Map<string, string>();

  private readonly streamShortId: string;
  private readonly client: TrackerClient;
  private readonly _peers = new Map<string, PeerItem>();
  private readonly logger = debug("p2pml-core:p2p-tracker-client");

  constructor(
    streamSwarmId: string,
    private readonly stream: StreamWithSegments,
    private readonly eventHandlers: P2PTrackerClientEventHandlers,
    private readonly config: StreamConfig,
    private readonly eventTarget: EventTarget<CoreEventMap>,
  ) {
    const streamHash = PeerUtil.getStreamHash(streamSwarmId);
    this.streamShortId = LoggerUtils.getStreamString(stream);

    let peerId = P2PTrackerClient.PEER_ID_BY_INFO_HASH.get(streamHash);
    if (!peerId) {
      peerId = PeerUtil.generatePeerId(config.trackerClientVersionPrefix);
      P2PTrackerClient.PEER_ID_BY_INFO_HASH.set(streamHash, peerId);
    }

    this.client = new TrackerClient({
      infoHash: utf8ToUintArray(streamHash),
      peerId: utf8ToUintArray(peerId),
      announce: isSafariOrWkWebview()
        ? config.announceTrackers.slice(0, 1) // Safari has issues with multiple trackers
        : config.announceTrackers,
      rtcConfig: this.config.rtcConfig,
    });
    this.client.on("peer", this.onReceivePeerConnection);
    this.client.on("warning", this.onTrackerClientWarning);
    this.client.on("error", this.onTrackerClientError);
    this.logger(
      `create new client; \nstream: ${this.streamShortId}; hash: ${streamHash}\npeerId: ${peerId}`,
    );
  }

  start() {
    this.client.start();
  }

  destroy() {
    this.client.destroy();

    for (const { peer, potentialConnections } of this._peers.values()) {
      peer?.destroy();
      for (const connection of potentialConnections) {
        connection.destroy();
      }
    }

    this._peers.clear();
    this.logger("destroy client; stream:", this.streamShortId);
  }

  private onReceivePeerConnection: TrackerClientEvents["peer"] = (
    peerConnection,
  ) => {
    const itemId = Peer.getPeerIdFromConnection(peerConnection);
    let peerItem = this._peers.get(itemId);

    if (peerItem?.peer) {
      peerConnection.destroy();
      return;
    }

    if (!peerItem) {
      peerItem = { potentialConnections: new Set() };
      peerConnection.idUtf8 = itemId;
      this._peers.set(itemId, peerItem);
    }

    peerItem.potentialConnections.add(peerConnection);

    peerConnection.on("connect", () => {
      if (peerItem.peer) return;

      for (const connection of peerItem.potentialConnections) {
        if (connection !== peerConnection) connection.destroy();
      }

      peerItem.potentialConnections.clear();

      peerItem.peer = new Peer(
        peerConnection,
        {
          onPeerClosed: this.onPeerClosed,
          onSegmentRequested: this.eventHandlers.onSegmentRequested,
          onSegmentsAnnouncement: this.eventHandlers.onSegmentsAnnouncement,
        },
        this.config,
        this.stream.type,
        this.eventTarget,
      );
      this.logger(
        `connected with peer: ${peerItem.peer.id} ${this.streamShortId}`,
      );
      this.eventHandlers.onPeerConnected(peerItem.peer);
    });
  };

  private onTrackerClientWarning: TrackerClientEvents["warning"] = (
    warning,
  ) => {
    this.logger("tracker warning %s:", this.streamShortId, warning);
    this.eventTarget.getEventDispatcher("onTrackerWarning")({
      streamType: this.stream.type,
      warning,
    });
  };

  private onTrackerClientError: TrackerClientEvents["error"] = (error) => {
    this.logger("tracker error in stream %s:", this.streamShortId, error);
    this.eventTarget.getEventDispatcher("onTrackerError")({
      streamType: this.stream.type,
      error,
    });
  };

  *peers() {
    for (const peerItem of this._peers.values()) {
      if (peerItem.peer) yield peerItem.peer;
    }
  }

  private onPeerClosed = (peer: Peer) => {
    this.logger(`peer closed: ${peer.id}`);
    this._peers.delete(peer.id);
  };

  static clearPeerIdCache() {
    P2PTrackerClient.PEER_ID_BY_INFO_HASH.clear();
  }
}
