type DebugNetworkMetricDirection = 'inbound' | 'outbound';
type DebugNetworkHandshakeType = 'answer' | 'ice_candidate' | 'offer';

const FILE_RATE_WINDOW_MS = 6_000;

export interface DebugNetworkMetricHandshakeCounts {
  answersReceived: number;
  answersSent: number;
  iceReceived: number;
  iceSent: number;
  offersReceived: number;
  offersSent: number;
}

export interface DebugNetworkMetricTextCounts {
  received: number;
  sent: number;
}

export interface DebugNetworkMetricStreamCounts {
  audio: number;
  video: number;
}

export interface DebugNetworkMetricDownloadRates {
  audioMbps: number | null;
  fileMbps: number | null;
  updatedAt: number | null;
  videoMbps: number | null;
}

export interface DebugNetworkMetricSnapshot {
  connectionDrops: number;
  downloads: DebugNetworkMetricDownloadRates;
  handshake: DebugNetworkMetricHandshakeCounts;
  lastConnectionState: string | null;
  pingMs: number | null;
  streams: DebugNetworkMetricStreamCounts;
  textMessages: DebugNetworkMetricTextCounts;
}

interface DebugNetworkFileSample {
  bytes: number;
  timestamp: number;
}

interface InternalDebugNetworkMetricState extends DebugNetworkMetricSnapshot {
  fileSamples: DebugNetworkFileSample[];
}

function createHandshakeCounts(): DebugNetworkMetricHandshakeCounts {
  return {
    answersReceived: 0,
    answersSent: 0,
    iceReceived: 0,
    iceSent: 0,
    offersReceived: 0,
    offersSent: 0
  };
}

function createTextCounts(): DebugNetworkMetricTextCounts {
  return {
    received: 0,
    sent: 0
  };
}

function createStreamCounts(): DebugNetworkMetricStreamCounts {
  return {
    audio: 0,
    video: 0
  };
}

function createDownloadRates(): DebugNetworkMetricDownloadRates {
  return {
    audioMbps: null,
    fileMbps: null,
    updatedAt: null,
    videoMbps: null
  };
}

function getRecord(value: unknown): Record<string, unknown> | null {
  if (!value || typeof value !== 'object' || Array.isArray(value))
    return null;

  return value as Record<string, unknown>;
}

function getString(record: Record<string, unknown>, key: string): string | null {
  const value = record[key];

  return typeof value === 'string' ? value : null;
}

function isTrackedTextMessageType(type: string | null): boolean {
  return type === 'chat-message' || type === 'message';
}

class DebugNetworkMetricsStore {
  private readonly metrics = new Map<string, InternalDebugNetworkMetricState>();

  recordConnectionState(peerId: string, state: string): void {
    if (!peerId || !state)
      return;

    const metric = this.ensure(peerId);

    if ((state === 'disconnected' || state === 'failed') && metric.lastConnectionState !== state)
      metric.connectionDrops += 1;

    metric.lastConnectionState = state;

    if (state === 'closed' || state === 'failed' || state === 'disconnected') {
      metric.streams.audio = 0;
      metric.streams.video = 0;
    }
  }

  recordPing(peerId: string, pingMs: number): void {
    if (!peerId || !Number.isFinite(pingMs))
      return;

    this.ensure(peerId).pingMs = Math.max(0, Math.round(pingMs));
  }

  recordDataChannelPayload(
    peerId: string,
    payload: Record<string, unknown>,
    direction: DebugNetworkMetricDirection
  ): void {
    if (!peerId)
      return;

    const type = getString(payload, 'type');

    if (isTrackedTextMessageType(type))
      this.incrementText(peerId, direction);
  }

  recordSignalingPayload(payload: unknown, direction: DebugNetworkMetricDirection): void {
    const record = getRecord(payload);

    if (!record)
      return;

    const type = getString(record, 'type');

    if (type !== 'offer' && type !== 'answer' && type !== 'ice_candidate')
      return;

    const peerId = direction === 'outbound'
      ? getString(record, 'targetUserId')
      : getString(record, 'fromUserId');

    if (!peerId)
      return;

    this.incrementHandshake(peerId, type, direction);
  }

  recordStreams(peerId: string, streams: Partial<DebugNetworkMetricStreamCounts>): void {
    if (!peerId)
      return;

    const metric = this.ensure(peerId);

    if (typeof streams.audio === 'number' && Number.isFinite(streams.audio))
      metric.streams.audio = Math.max(0, Math.round(streams.audio));

    if (typeof streams.video === 'number' && Number.isFinite(streams.video))
      metric.streams.video = Math.max(0, Math.round(streams.video));
  }

  recordDownloadRates(
    peerId: string,
    rates: { audioMbps?: number | null; videoMbps?: number | null },
    timestamp: number = Date.now()
  ): void {
    if (!peerId)
      return;

    const metric = this.ensure(peerId);

    if (rates.audioMbps !== undefined)
      metric.downloads.audioMbps = this.sanitizeRate(rates.audioMbps);

    if (rates.videoMbps !== undefined)
      metric.downloads.videoMbps = this.sanitizeRate(rates.videoMbps);

    metric.downloads.updatedAt = timestamp;
  }

  recordFileChunk(peerId: string, bytes: number, timestamp: number = Date.now()): void {
    if (!peerId || !Number.isFinite(bytes) || bytes <= 0)
      return;

    const metric = this.ensure(peerId);

    metric.fileSamples.push({
      bytes,
      timestamp
    });

    metric.fileSamples = metric.fileSamples.filter(
      (sample) => timestamp - sample.timestamp <= FILE_RATE_WINDOW_MS
    );

    metric.downloads.fileMbps = this.calculateFileMbps(metric.fileSamples, timestamp);
  }

  getSnapshot(peerId: string): DebugNetworkMetricSnapshot | null {
    const metric = this.metrics.get(peerId);

    if (!metric)
      return null;

    const now = Date.now();

    metric.fileSamples = metric.fileSamples.filter(
      (sample) => now - sample.timestamp <= FILE_RATE_WINDOW_MS
    );

    metric.downloads.fileMbps = this.calculateFileMbps(metric.fileSamples, now);

    return {
      connectionDrops: metric.connectionDrops,
      downloads: { ...metric.downloads },
      handshake: { ...metric.handshake },
      lastConnectionState: metric.lastConnectionState,
      pingMs: metric.pingMs,
      streams: { ...metric.streams },
      textMessages: { ...metric.textMessages }
    };
  }

  private ensure(peerId: string): InternalDebugNetworkMetricState {
    const existing = this.metrics.get(peerId);

    if (existing)
      return existing;

    const created: InternalDebugNetworkMetricState = {
      connectionDrops: 0,
      downloads: createDownloadRates(),
      fileSamples: [],
      handshake: createHandshakeCounts(),
      lastConnectionState: null,
      pingMs: null,
      streams: createStreamCounts(),
      textMessages: createTextCounts()
    };

    this.metrics.set(peerId, created);

    return created;
  }

  private incrementHandshake(
    peerId: string,
    type: DebugNetworkHandshakeType,
    direction: DebugNetworkMetricDirection,
    count = 1
  ): void {
    if (!peerId || count <= 0)
      return;

    const metric = this.ensure(peerId);

    switch (type) {
      case 'offer':
        if (direction === 'outbound')
          metric.handshake.offersSent += count;
        else
          metric.handshake.offersReceived += count;

        return;

      case 'answer':
        if (direction === 'outbound')
          metric.handshake.answersSent += count;
        else
          metric.handshake.answersReceived += count;

        return;

      case 'ice_candidate':
        if (direction === 'outbound')
          metric.handshake.iceSent += count;
        else
          metric.handshake.iceReceived += count;

        return;
    }
  }

  private incrementText(
    peerId: string,
    direction: DebugNetworkMetricDirection,
    count = 1
  ): void {
    if (!peerId || count <= 0)
      return;

    const metric = this.ensure(peerId);

    if (direction === 'outbound') {
      metric.textMessages.sent += count;
      return;
    }

    metric.textMessages.received += count;
  }

  private calculateFileMbps(samples: DebugNetworkFileSample[], now: number): number | null {
    if (samples.length === 0)
      return null;

    const totalBytes = samples.reduce((sum, sample) => sum + sample.bytes, 0);
    const earliestTimestamp = samples[0]?.timestamp ?? now;
    const durationMs = Math.max(1_000, now - earliestTimestamp);

    return this.sanitizeRate(totalBytes * 8 / durationMs / 1000);
  }

  private sanitizeRate(value: number | null | undefined): number | null {
    if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
      return null;

    return Math.round(value * 1000) / 1000;
  }
}

const debugNetworkMetricsStore = new DebugNetworkMetricsStore();

export function recordDebugNetworkConnectionState(peerId: string, state: string): void {
  debugNetworkMetricsStore.recordConnectionState(peerId, state);
}

export function recordDebugNetworkPing(peerId: string, pingMs: number): void {
  debugNetworkMetricsStore.recordPing(peerId, pingMs);
}

export function recordDebugNetworkDataChannelPayload(
  peerId: string,
  payload: Record<string, unknown>,
  direction: DebugNetworkMetricDirection
): void {
  debugNetworkMetricsStore.recordDataChannelPayload(peerId, payload, direction);
}

export function recordDebugNetworkSignalingPayload(
  payload: unknown,
  direction: DebugNetworkMetricDirection
): void {
  debugNetworkMetricsStore.recordSignalingPayload(payload, direction);
}

export function recordDebugNetworkStreams(
  peerId: string,
  streams: Partial<DebugNetworkMetricStreamCounts>
): void {
  debugNetworkMetricsStore.recordStreams(peerId, streams);
}

export function recordDebugNetworkDownloadRates(
  peerId: string,
  rates: { audioMbps?: number | null; videoMbps?: number | null },
  timestamp?: number
): void {
  debugNetworkMetricsStore.recordDownloadRates(peerId, rates, timestamp);
}

export function recordDebugNetworkFileChunk(
  peerId: string,
  bytes: number,
  timestamp?: number
): void {
  debugNetworkMetricsStore.recordFileChunk(peerId, bytes, timestamp);
}

export function getDebugNetworkMetricSnapshot(peerId: string): DebugNetworkMetricSnapshot | null {
  return debugNetworkMetricsStore.getSnapshot(peerId);
}
