/* eslint-disable padding-line-between-statements */
import {
  Component,
  ElementRef,
  OnDestroy,
  computed,
  effect,
  inject,
  input,
  viewChild
} from '@angular/core';
import { CommonModule } from '@angular/common';
import cytoscape, { type Core, type ElementDefinition } from 'cytoscape';

import {
  type DebugNetworkEdge,
  type DebugNetworkMessageGroup,
  type DebugNetworkNode,
  type DebugNetworkSnapshot
} from '../../../../core/services/debugging.service';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';

@Component({
  selector: 'app-debug-console-network-map',
  standalone: true,
  imports: [CommonModule, ...APP_TRANSLATE_IMPORTS],
  templateUrl: './debug-console-network-map.component.html',
  host: {
    style: 'display: flex; min-height: 0; overflow: hidden;'
  }
})
export class DebugConsoleNetworkMapComponent implements OnDestroy {
  readonly snapshot = input.required<DebugNetworkSnapshot>();
  readonly graphRef = viewChild<ElementRef<HTMLDivElement>>('graph');
  readonly statusNodes = computed(() => {
    const clientNodes = this.snapshot().nodes
      .filter((node) => node.kind === 'local-client' || node.kind === 'remote-client');
    const remoteNodes = clientNodes.filter((node) => node.kind === 'remote-client');
    const visibleNodes = remoteNodes.length > 0
      ? remoteNodes
      : clientNodes;

    return visibleNodes
      .sort((nodeA, nodeB) => {
        if (nodeA.isActive !== nodeB.isActive)
          return nodeA.isActive ? -1 : 1;

        return nodeA.label.localeCompare(nodeB.label);
      });
  });
  readonly connectionEdges = computed(() => {
    return [...this.snapshot().edges].sort((edgeA, edgeB) => {
      if (edgeA.isActive !== edgeB.isActive)
        return edgeA.isActive ? -1 : 1;

      if (edgeA.kind !== edgeB.kind)
        return this.getEdgeOrder(edgeA.kind) - this.getEdgeOrder(edgeB.kind);

      return edgeB.lastSeen - edgeA.lastSeen;
    });
  });

  private readonly appI18n = inject(AppI18nService);
  private cytoscapeInstance: Core | null = null;
  private resizeObserver: ResizeObserver | null = null;
  private lastStructureKey = '';

  constructor() {
    effect(() => {
      const container = this.graphRef()?.nativeElement;
      const snapshot = this.snapshot();

      if (!container)
        return;

      this.ensureGraph(container);
      requestAnimationFrame(() => this.renderGraph(snapshot));
    });
  }

  ngOnDestroy(): void {
    this.resizeObserver?.disconnect();
    this.resizeObserver = null;
    this.cytoscapeInstance?.destroy();
    this.cytoscapeInstance = null;
  }

  formatAge(timestamp: number): string {
    const deltaMs = Math.max(0, Date.now() - timestamp);

    if (deltaMs < 1_000)
      return this.appI18n.instant('shared.debugConsole.age.justNow');

    const seconds = Math.floor(deltaMs / 1_000);

    if (seconds < 60)
      return this.appI18n.instant('shared.debugConsole.age.secondsAgo', { seconds });

    const minutes = Math.floor(seconds / 60);

    if (minutes < 60)
      return this.appI18n.instant('shared.debugConsole.age.minutesAgo', { minutes });

    const hours = Math.floor(minutes / 60);

    return this.appI18n.instant('shared.debugConsole.age.hoursAgo', { hours });
  }

  formatEdgeHeading(edge: DebugNetworkEdge): string {
    return `${edge.sourceLabel} -> ${edge.targetLabel}`;
  }

  formatMessageGroup(group: DebugNetworkMessageGroup): string {
    const direction = group.direction === 'outbound' ? '↑' : '↓';

    return `${direction} ${group.type} ×${group.count}`;
  }

  formatHandshakeSummary(node: DebugNetworkNode): string {
    const offerSummary = `${node.handshake.offersSent}/${node.handshake.offersReceived}`;
    const answerSummary = `${node.handshake.answersSent}/${node.handshake.answersReceived}`;
    const iceSummary = `${node.handshake.iceSent}/${node.handshake.iceReceived}`;

    return `O ${offerSummary} · A ${answerSummary} · ICE ${iceSummary}`;
  }

  formatTextSummary(node: DebugNetworkNode): string {
    return `Text ↑${node.textMessages.sent} ↓${node.textMessages.received}`;
  }

  formatStreamSummary(node: DebugNetworkNode): string {
    return `Streams A${node.streams.audio} V${node.streams.video}`;
  }

  formatDownloadSummary(node: DebugNetworkNode): string {
    const metrics = [
      `F ${this.formatMbps(node.downloads.fileMbps)}`,
      `A ${this.formatMbps(node.downloads.audioMbps)}`,
      `V ${this.formatMbps(node.downloads.videoMbps)}`
    ];

    return `↓ ${metrics.join(' · ')}`;
  }

  formatClientId(node: DebugNetworkNode): string {
    return node.userId ?? node.identity ?? this.appI18n.instant('shared.debugConsole.networkMap.unavailable');
  }

  formatPeerIdentity(node: DebugNetworkNode): string | null {
    if (!node.identity || node.identity === node.userId)
      return null;

    return node.identity;
  }

  hasDownloadMetrics(node: DebugNetworkNode): boolean {
    return node.downloads.fileMbps !== null || node.downloads.audioMbps !== null || node.downloads.videoMbps !== null;
  }

  formatMbps(value: number | null): string {
    if (value === null)
      return '-';

    return value >= 10 ? value.toFixed(1) : value.toFixed(2);
  }

  getConnectionBadgeClass(edge: DebugNetworkEdge): string {
    const base = 'rounded-full border px-2 py-0.5 text-[10px] font-medium';

    if (!edge.isActive)
      return base + ' border-border text-muted-foreground';

    switch (edge.kind) {
      case 'signaling':
        return base + ' border-orange-400/40 bg-orange-500/10 text-orange-300';
      case 'peer':
        return base + ' border-emerald-400/40 bg-emerald-500/10 text-emerald-300';
      case 'membership':
        return base + ' border-violet-400/40 bg-violet-500/10 text-violet-300';
    }
  }

  getMessageBadgeClass(group: DebugNetworkMessageGroup): string {
    const base = 'rounded-full border px-2 py-0.5 text-[10px] font-medium';

    if (group.scope === 'signaling')
      return base + ' border-orange-400/30 bg-orange-500/10 text-orange-200';

    if (group.direction === 'outbound')
      return base + ' border-sky-400/30 bg-sky-500/10 text-sky-200';

    return base + ' border-cyan-400/30 bg-cyan-500/10 text-cyan-200';
  }

  getStatusBadgeClass(node: DebugNetworkNode): string {
    const base = 'rounded-full border px-2 py-0.5 text-[10px] font-medium';

    if (node.isSpeaking)
      return base + ' border-emerald-400/40 bg-emerald-500/10 text-emerald-300';

    if (node.isTyping)
      return base + ' border-amber-400/40 bg-amber-500/10 text-amber-300';

    if (node.isStreaming)
      return base + ' border-fuchsia-400/40 bg-fuchsia-500/10 text-fuchsia-300';

    if (node.isMuted)
      return base + ' border-rose-400/40 bg-rose-500/10 text-rose-300';

    return base + ' border-border text-muted-foreground';
  }

  getNodeActivityLabel(node: DebugNetworkNode): string {
    if (node.isSpeaking)
      return this.appI18n.instant('shared.debugConsole.activity.speaking');

    if (node.isTyping)
      return this.appI18n.instant('shared.debugConsole.activity.typing');

    if (node.isStreaming)
      return this.appI18n.instant('shared.debugConsole.activity.streaming');

    if (node.isMuted)
      return this.appI18n.instant('shared.debugConsole.activity.muted');

    return this.appI18n.instant('shared.debugConsole.activity.active');
  }

  getEdgeKindLabel(edge: DebugNetworkEdge): string {
    return this.appI18n.instant(`shared.debugConsole.edgeKind.${edge.kind}`);
  }

  getVisibleMessageGroups(edge: DebugNetworkEdge): DebugNetworkMessageGroup[] {
    return edge.messageGroups.slice(0, 8);
  }

  getHiddenMessageGroupCount(edge: DebugNetworkEdge): number {
    return Math.max(0, edge.messageGroups.length - 8);
  }

  private ensureGraph(container: HTMLDivElement): void {
    if (this.cytoscapeInstance)
      return;

    this.cytoscapeInstance = cytoscape({
      container,
      boxSelectionEnabled: false,
      minZoom: 0.45,
      maxZoom: 1.8,
      wheelSensitivity: 0.2,
      autoungrabify: true,
      style: this.buildGraphStyles() as never
    });
    this.resizeObserver = new ResizeObserver(() => {
      this.cytoscapeInstance?.resize();
    });
    this.resizeObserver.observe(container);
  }

  private renderGraph(snapshot: DebugNetworkSnapshot): void {
    if (!this.cytoscapeInstance)
      return;

    const structureKey = this.buildStructureKey(snapshot);
    const elements = this.buildGraphElements(snapshot);

    this.cytoscapeInstance.elements().remove();
    this.cytoscapeInstance.add(elements);
    this.cytoscapeInstance.resize();

    if (structureKey !== this.lastStructureKey) {
      this.cytoscapeInstance.fit(undefined, 48);
      this.lastStructureKey = structureKey;
    }
  }

  private buildStructureKey(snapshot: DebugNetworkSnapshot): string {
    return JSON.stringify({
      edgeIds: snapshot.edges.map((edge) => edge.id),
      nodeIds: snapshot.nodes.map((node) => node.id)
    });
  }

  private buildGraphElements(snapshot: DebugNetworkSnapshot): ElementDefinition[] {
    const positions = this.buildNodePositions(snapshot.nodes);

    return [
      ...snapshot.nodes.map((node) => ({
        data: {
          id: node.id,
          label: this.buildNodeLabel(node)
        },
        position: positions.get(node.id) ?? { x: 0,
          y: 0 },
        classes: this.buildNodeClasses(node)
      })),
      ...snapshot.edges.map((edge) => {
        const label = this.buildEdgeLabel(edge);

        return {
          data: {
            id: edge.id,
            source: edge.sourceId,
            target: edge.targetId,
            label
          },
          classes: this.buildEdgeClasses(edge, label)
        };
      })
    ];
  }

  private buildNodePositions(nodes: DebugNetworkNode[]): Map<string, { x: number; y: number }> {
    const positions = new Map<string, { x: number; y: number }>();
    const localNodes = nodes.filter((node) => node.kind === 'local-client');
    const signalingNodes = nodes.filter((node) => node.kind === 'signaling-server');
    const serverNodes = nodes.filter((node) => node.kind === 'app-server');
    const remoteNodes = nodes.filter((node) => node.kind === 'remote-client');

    for (const node of localNodes) {
      positions.set(node.id, {
        x: 140,
        y: 320
      });
    }

    this.applyColumnPositions(positions, signalingNodes, 470, 170, 112);
    this.applyColumnPositions(positions, serverNodes, 470, 470, 112);
    this.applyColumnPositions(positions, remoteNodes, 820, 320, 186);

    return positions;
  }

  private applyColumnPositions(
    positions: Map<string, { x: number; y: number }>,
    nodes: DebugNetworkNode[],
    x: number,
    centerY: number,
    spacing: number
  ): void {
    if (nodes.length === 0)
      return;

    const totalHeight = (nodes.length - 1) * spacing;
    const startY = centerY - totalHeight / 2;

    nodes.forEach((node, index) => {
      positions.set(node.id, {
        x,
        y: startY + index * spacing
      });
    });
  }

  private buildNodeClasses(node: DebugNetworkNode): string {
    const classes: string[] = [node.kind];

    if (!node.isActive)
      classes.push('inactive');

    if (node.isTyping)
      classes.push('typing');

    if (node.isSpeaking)
      classes.push('speaking');

    if (node.isStreaming)
      classes.push('streaming');

    if (node.isMuted)
      classes.push('muted');

    if (node.isDeafened)
      classes.push('deafened');

    return classes.join(' ');
  }

  private buildEdgeClasses(edge: DebugNetworkEdge, label: string): string {
    const classes: string[] = [edge.kind];

    if (!edge.isActive)
      classes.push('inactive');

    if (label.trim().length > 0)
      classes.push('has-label');

    return classes.join(' ');
  }

  private buildNodeLabel(node: DebugNetworkNode): string {
    const lines = [node.label];

    if (node.secondaryLabel)
      lines.push(node.secondaryLabel);

    if (node.kind === 'local-client' || node.kind === 'remote-client') {
      lines.push(`ID ${this.shortenIdentifier(node.userId ?? node.identity ?? 'unknown')}`);

      const statusLine = this.buildCompactStatusLine(node);

      if (statusLine)
        lines.push(statusLine);

      lines.push(`A${node.streams.audio} V${node.streams.video} • ↑${node.textMessages.sent} ↓${node.textMessages.received}`);

      if (this.hasHandshakeActivity(node)) {
        lines.push(
          `HS O${node.handshake.offersSent}/${node.handshake.offersReceived} A${node.handshake.answersSent}/${node.handshake.answersReceived}`
        );
        lines.push(`ICE ${node.handshake.iceSent}/${node.handshake.iceReceived} • Drop ${node.connectionDrops}`);
      } else {
        lines.push(`Drop ${node.connectionDrops}`);
      }

      if (this.hasDownloadMetrics(node)) {
        const downloadSummary = [
          `F${this.formatMbps(node.downloads.fileMbps)}`,
          `A${this.formatMbps(node.downloads.audioMbps)}`,
          `V${this.formatMbps(node.downloads.videoMbps)}`
        ].join(' ');

        lines.push(`↓ ${downloadSummary}`);
      }
    }

    return lines.join('\n');
  }

  private buildEdgeLabel(edge: DebugNetworkEdge): string {
    if (edge.kind === 'membership')
      return edge.isActive ? edge.stateLabel : '';

    return edge.label;
  }

  private getEdgeOrder(kind: DebugNetworkEdge['kind']): number {
    switch (kind) {
      case 'signaling':
        return 0;
      case 'peer':
        return 1;
      case 'membership':
        return 2;
    }
  }

  private buildGraphStyles() {
    return [
      {
        selector: 'node',
        style: {
          'background-color': '#2563eb',
          'border-color': '#60a5fa',
          'border-width': 2,
          color: '#f8fafc',
          'font-family': 'Inter, ui-sans-serif, system-ui, sans-serif',
          'font-size': 10,
          'font-weight': 600,
          height: 152,
          label: 'data(label)',
          padding: 12,
          shape: 'round-rectangle',
          'text-background-color': '#0f172acc',
          'text-background-opacity': 1,
          'text-background-padding': 4,
          'text-border-radius': 8,
          'text-halign': 'center',
          'text-max-width': 208,
          'text-outline-color': '#0f172a',
          'text-outline-width': 0,
          'text-valign': 'center',
          'text-wrap': 'wrap',
          width: 224
        }
      },
      {
        selector: 'node.local-client',
        style: {
          'background-color': '#2563eb',
          'border-color': '#93c5fd'
        }
      },
      {
        selector: 'node.remote-client',
        style: {
          'background-color': '#0f766e',
          'border-color': '#34d399'
        }
      },
      {
        selector: 'node.signaling-server',
        style: {
          'background-color': '#9a3412',
          'border-color': '#fdba74',
          height: 82,
          shape: 'round-rectangle',
          width: 190
        }
      },
      {
        selector: 'node.app-server',
        style: {
          'background-color': '#5b21b6',
          'border-color': '#c4b5fd',
          height: 82,
          shape: 'round-rectangle',
          width: 190
        }
      },
      {
        selector: 'node.typing',
        style: {
          'border-color': '#fbbf24',
          'border-width': 4
        }
      },
      {
        selector: 'node.speaking',
        style: {
          'border-color': '#34d399',
          'border-width': 4,
          'overlay-color': '#34d399',
          'overlay-opacity': 0.14,
          'overlay-padding': 6
        }
      },
      {
        selector: 'node.streaming',
        style: {
          'border-color': '#e879f9'
        }
      },
      {
        selector: 'node.muted',
        style: {
          'background-blacken': 0.28
        }
      },
      {
        selector: 'node.deafened',
        style: {
          'border-style': 'dashed'
        }
      },
      {
        selector: 'node.inactive',
        style: {
          opacity: 0.45
        }
      },
      {
        selector: 'edge',
        style: {
          color: '#e2e8f0',
          'curve-style': 'bezier',
          'font-family': 'Inter, ui-sans-serif, system-ui, sans-serif',
          'font-size': 10,
          'line-color': '#64748b',
          label: 'data(label)',
          opacity: 0.92,
          'target-arrow-shape': 'none',
          'text-background-color': '#0f172acc',
          'text-background-opacity': 0,
          'text-background-padding': 0,
          'text-margin-y': -10,
          'text-max-width': 120,
          'text-outline-width': 0,
          'text-wrap': 'wrap',
          width: 2.5
        }
      },
      {
        selector: 'edge.has-label',
        style: {
          'text-background-opacity': 1,
          'text-background-padding': 3
        }
      },
      {
        selector: 'edge.signaling',
        style: {
          'line-color': '#fb923c',
          'line-style': 'dashed'
        }
      },
      {
        selector: 'edge.peer',
        style: {
          'line-color': '#22c55e'
        }
      },
      {
        selector: 'edge.membership',
        style: {
          'line-color': '#c084fc',
          'line-style': 'dotted',
          width: 1.5
        }
      },
      {
        selector: 'edge.inactive',
        style: {
          opacity: 0.3
        }
      }
    ];
  }

  private buildCompactStatusLine(node: DebugNetworkNode): string | null {
    const tokens: string[] = [];

    if (node.isSpeaking)
      tokens.push('Speaking');
    else if (node.isTyping)
      tokens.push('Typing');

    if (node.isMuted)
      tokens.push('Muted');
    else if (node.isVoiceConnected)
      tokens.push('Mic on');

    if (node.isStreaming)
      tokens.push('Screen');

    if (node.pingMs !== null)
      tokens.push(`${node.pingMs} ms`);

    return tokens.length > 0 ? tokens.join(' • ') : null;
  }

  private hasHandshakeActivity(node: DebugNetworkNode): boolean {
    return node.handshake.offersSent > 0
      || node.handshake.offersReceived > 0
      || node.handshake.answersSent > 0
      || node.handshake.answersReceived > 0
      || node.handshake.iceSent > 0
      || node.handshake.iceReceived > 0;
  }

  private shortenIdentifier(value: string): string {
    if (value.length <= 18)
      return value;

    return `${value.slice(0, 8)}...${value.slice(-6)}`;
  }
}
