import * as signalR from "@microsoft/signalr";
import { v4 as uuidv4 } from "uuid";

// Usa variabile ambiente o fallback a window.location per accesso esterno
const getApiUrl = (): string => {
  // Prima controlla la variabile ambiente (deve essere non-vuota)
  const envUrl = import.meta.env.VITE_API_URL;
  if (envUrl && envUrl.trim() !== "") {
    return envUrl;
  }
  // Fallback: usa lo stesso host del frontend ma porta 5000
  const protocol = window.location.protocol;
  const hostname = window.location.hostname;
  return `${protocol}//${hostname}:5000`;
};

const CURSOR_THROTTLE_MS = 50;

// ==================== TYPES ====================

export interface Collaborator {
  connectionId: string;
  userName: string;
  color: string;
  joinedAt: string;
  selectedItemId: string | null;
  cursorX: number | null;
  cursorY: number | null;
  currentViewId: string | null;
  metadata: unknown;
  isActive: boolean;
  lastActivityAt: string;
}

export interface RoomState {
  roomKey: string;
  collaborators: Collaborator[];
  joinedAt: string;
}

export interface DataChangeMessage {
  itemId: string;
  itemType: string;
  changeType: string;
  fieldPath?: string;
  newValue: unknown;
  oldValue?: unknown;
  senderConnectionId?: string;
  timestamp: string;
}

export interface ItemCreatedMessage {
  itemId: string;
  itemType: string;
  item: unknown;
  parentId?: string;
  index?: number;
  senderConnectionId?: string;
  timestamp: string;
}

export interface ItemDeletedMessage {
  itemId: string;
  itemType: string;
  senderConnectionId?: string;
  timestamp: string;
}

export interface BatchOperationMessage {
  operationType: string;
  itemType: string;
  data: unknown;
  senderConnectionId?: string;
  timestamp: string;
}

export interface SelectionChangedMessage {
  connectionId: string;
  itemId: string | null;
  timestamp: string;
}

export interface CursorMovedMessage {
  connectionId: string;
  x: number;
  y: number;
  viewId: string | null;
  timestamp: string;
}

export interface ViewChangedMessage {
  connectionId: string;
  viewId: string;
  timestamp: string;
}

export interface UserTypingMessage {
  connectionId: string;
  itemId: string | null;
  isTyping: boolean;
  timestamp: string;
}

export interface SyncRequestMessage {
  requesterId: string;
  roomKey: string;
  timestamp: string;
}

export interface SyncDataMessage {
  roomKey: string;
  dataJson: string;
  senderConnectionId: string;
  timestamp: string;
}

export interface DataSavedMessage {
  savedBy: string;
  roomKey: string;
  timestamp: string;
}

export interface TemplateSyncMessage {
  templateJson: string;
  senderConnectionId: string;
  version: number;
  timestamp: string;
  compressed?: boolean; // Whether the templateJson is base64-encoded gzip
}

// ==================== COMPRESSION UTILITIES ====================

const COMPRESSION_THRESHOLD = 10000; // Compress if JSON is > 10KB

/**
 * Compress a string using gzip and return base64-encoded result
 */
async function compressString(input: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(input);

  const cs = new CompressionStream("gzip");
  const writer = cs.writable.getWriter();
  writer.write(data);
  writer.close();

  const compressedChunks: Uint8Array[] = [];
  const reader = cs.readable.getReader();

  let result = await reader.read();
  while (!result.done) {
    compressedChunks.push(result.value);
    result = await reader.read();
  }

  // Combine chunks
  const totalLength = compressedChunks.reduce(
    (acc, arr) => acc + arr.length,
    0,
  );
  const compressed = new Uint8Array(totalLength);
  let offset = 0;
  for (const chunk of compressedChunks) {
    compressed.set(chunk, offset);
    offset += chunk.length;
  }

  // Convert to base64
  return btoa(String.fromCharCode(...compressed));
}

/**
 * Decompress a base64-encoded gzip string
 */
async function decompressString(base64Input: string): Promise<string> {
  // Convert base64 to Uint8Array
  const binaryString = atob(base64Input);
  const compressed = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
    compressed[i] = binaryString.charCodeAt(i);
  }

  const ds = new DecompressionStream("gzip");
  const writer = ds.writable.getWriter();
  writer.write(compressed);
  writer.close();

  const decompressedChunks: Uint8Array[] = [];
  const reader = ds.readable.getReader();

  let result = await reader.read();
  while (!result.done) {
    decompressedChunks.push(result.value);
    result = await reader.read();
  }

  // Combine chunks
  const totalLength = decompressedChunks.reduce(
    (acc, arr) => acc + arr.length,
    0,
  );
  const decompressed = new Uint8Array(totalLength);
  let offset = 0;
  for (const chunk of decompressedChunks) {
    decompressed.set(chunk, offset);
    offset += chunk.length;
  }

  const decoder = new TextDecoder();
  return decoder.decode(decompressed);
}

/**
 * Check if CompressionStream is available (modern browsers)
 */
function isCompressionAvailable(): boolean {
  return (
    typeof CompressionStream !== "undefined" &&
    typeof DecompressionStream !== "undefined"
  );
}

export {
  compressString,
  decompressString,
  isCompressionAvailable,
  COMPRESSION_THRESHOLD,
};

export interface UserLeftMessage {
  connectionId: string;
  userName: string;
  timestamp: string;
}

export interface ChangeHistoryEntry {
  id: string;
  type: "data_changed" | "item_created" | "item_deleted" | "batch_operation";
  description: string;
  userName: string;
  userColor: string;
  timestamp: Date;
  itemId?: string;
  itemType?: string;
}

// ==================== COLORS ====================

export const COLLABORATOR_COLORS = [
  "#FF6B6B",
  "#4ECDC4",
  "#45B7D1",
  "#96CEB4",
  "#FFEAA7",
  "#DDA0DD",
  "#98D8C8",
  "#F7DC6F",
  "#BB8FCE",
  "#85C1E9",
  "#F8B500",
  "#58D68D",
];

export function getColorForUser(userName: string): string {
  let hash = 0;
  for (let i = 0; i < userName.length; i++) {
    hash = userName.charCodeAt(i) + ((hash << 5) - hash);
  }
  return COLLABORATOR_COLORS[Math.abs(hash) % COLLABORATOR_COLORS.length];
}

export function generateRandomUserName(): string {
  const adjectives = [
    "Veloce",
    "Creativo",
    "Brillante",
    "Agile",
    "Dinamico",
    "Preciso",
    "Elegante",
    "Audace",
  ];
  const nouns = [
    "Designer",
    "Editor",
    "Artista",
    "Creatore",
    "Architetto",
    "Compositore",
    "Maestro",
    "Inventore",
  ];
  const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
  const noun = nouns[Math.floor(Math.random() * nouns.length)];
  const num = Math.floor(Math.random() * 100);
  return `${adj} ${noun} ${num}`;
}

export function getOrCreateUserName(): string {
  const stored = localStorage.getItem("apollinare_username");
  if (stored) return stored;
  const newName = generateRandomUserName();
  localStorage.setItem("apollinare_username", newName);
  return newName;
}

export function saveUserName(userName: string): void {
  localStorage.setItem("apollinare_username", userName);
}

// ==================== SERVICE ====================

type EventCallback<T> = (data: T) => void;

class CollaborationService {
  private connection: signalR.HubConnection | null = null;
  private currentRoom: string | null = null;
  private userName: string = "";
  private userColor: string = "";
  private isConnecting = false;
  private connectAbortController: AbortController | null = null;

  // Event listeners
  private userJoinedListeners = new Set<EventCallback<Collaborator>>();
  private userLeftListeners = new Set<EventCallback<UserLeftMessage>>();
  private roomStateListeners = new Set<EventCallback<RoomState>>();
  private dataChangedListeners = new Set<EventCallback<DataChangeMessage>>();
  private itemCreatedListeners = new Set<EventCallback<ItemCreatedMessage>>();
  private itemDeletedListeners = new Set<EventCallback<ItemDeletedMessage>>();
  private batchOperationListeners = new Set<
    EventCallback<BatchOperationMessage>
  >();
  private selectionChangedListeners = new Set<
    EventCallback<SelectionChangedMessage>
  >();
  private cursorMovedListeners = new Set<EventCallback<CursorMovedMessage>>();
  private viewChangedListeners = new Set<EventCallback<ViewChangedMessage>>();
  private userTypingListeners = new Set<EventCallback<UserTypingMessage>>();
  private syncRequestedListeners = new Set<EventCallback<SyncRequestMessage>>();
  private syncReceivedListeners = new Set<EventCallback<SyncDataMessage>>();
  private dataSavedListeners = new Set<EventCallback<DataSavedMessage>>();
  private templateSyncListeners = new Set<EventCallback<TemplateSyncMessage>>();
  private connectionStateListeners = new Set<
    EventCallback<{ connected: boolean; connecting: boolean }>
  >();

  // Change history (LIFO)
  private changeHistory: ChangeHistoryEntry[] = [];
  private changeHistoryListeners = new Set<
    EventCallback<ChangeHistoryEntry[]>
  >();
  private maxHistoryEntries = 50;

  // Collaborators cache for name/color lookup
  private collaboratorsCache = new Map<string, Collaborator>();

  // Cursor throttling
  private lastCursorSend = 0;
  private pendingCursorUpdate: {
    x: number;
    y: number;
    viewId: string | null;
  } | null = null;
  private cursorThrottleTimer: ReturnType<typeof setTimeout> | null = null;

  // ==================== GETTERS ====================

  get isConnected(): boolean {
    return this.connection?.state === signalR.HubConnectionState.Connected;
  }

  get currentUserName(): string {
    return this.userName;
  }

  get currentUserColor(): string {
    return this.userColor;
  }

  get connectionId(): string | null {
    return this.connection?.connectionId ?? null;
  }

  get currentRoomKey(): string | null {
    return this.currentRoom;
  }

  // ==================== CONNECTION ====================

  async connect(): Promise<void> {
    // If already connected, return immediately
    if (this.isConnected) return;

    // If already connecting, wait for the connection to complete
    if (this.isConnecting) {
      // Wait for connection state to change
      return new Promise((resolve) => {
        const checkConnection = () => {
          if (this.isConnected) {
            resolve();
          } else if (!this.isConnecting) {
            // Connection attempt finished but not connected - could be cancelled or failed
            // Don't reject, just resolve silently (caller should check isConnected)
            resolve();
          } else {
            setTimeout(checkConnection, 100);
          }
        };
        setTimeout(checkConnection, 100);
      });
    }

    this.isConnecting = true;
    this.connectAbortController = new AbortController();
    this.notifyConnectionState();

    try {
      const apiUrl = getApiUrl(); // Calcola URL al momento della connessione
      console.log(`[Collaboration] Connecting to ${apiUrl}/hubs/collaboration`);
      this.connection = new signalR.HubConnectionBuilder()
        .withUrl(`${apiUrl}/hubs/collaboration`)
        .withAutomaticReconnect([0, 1000, 2000, 5000, 10000, 30000])
        .configureLogging(signalR.LogLevel.Warning)
        .build();

      this.setupEventHandlers();

      // Check if aborted before starting
      if (this.connectAbortController.signal.aborted) {
        console.log("[Collaboration] Connection aborted before start");
        return;
      }

      await this.connection.start();

      // Check if aborted after starting (disconnect was called during connection)
      if (this.connectAbortController.signal.aborted) {
        console.log(
          "[Collaboration] Connection aborted after start, disconnecting",
        );
        await this.connection.stop();
        return;
      }

      console.log("[Collaboration] Connected");
    } catch (error) {
      // Don't log error if it was an intentional abort
      if (this.connectAbortController?.signal.aborted) {
        console.log("[Collaboration] Connection aborted");
        return;
      }
      console.error("[Collaboration] Connection failed:", error);
      throw error;
    } finally {
      this.isConnecting = false;
      this.connectAbortController = null;
      this.notifyConnectionState();
    }
  }

  async disconnect(): Promise<void> {
    // Abort any pending connection attempt
    if (this.connectAbortController) {
      this.connectAbortController.abort();
    }

    if (this.currentRoom) {
      await this.leaveRoom();
    }
    if (this.connection) {
      try {
        await this.connection.stop();
      } catch {
        // Ignore errors when stopping
      }
      this.connection = null;
    }
    this.notifyConnectionState();
  }

  // ==================== ROOM MANAGEMENT ====================

  async joinRoom(
    roomKey: string,
    userName?: string,
    metadata?: unknown,
  ): Promise<void> {
    // Ensure we're connected first
    if (!this.isConnected) {
      await this.connect();
    }

    // Check connection - if not connected (possibly due to abort), just return silently
    if (!this.isConnected || !this.connection) {
      console.log(
        "[Collaboration] Cannot join room: not connected (connection may have been cancelled)",
      );
      return;
    }

    // Leave current room if different
    if (this.currentRoom && this.currentRoom !== roomKey) {
      await this.leaveRoom();
    }

    if (this.currentRoom === roomKey) return;

    this.userName = userName || getOrCreateUserName();
    this.userColor = getColorForUser(this.userName);

    try {
      await this.connection.invoke(
        "JoinRoom",
        roomKey,
        this.userName,
        this.userColor,
        metadata,
      );
      this.currentRoom = roomKey;
      console.log(`[Collaboration] Joined room: ${roomKey}`);
    } catch (error) {
      console.error("[Collaboration] Failed to join room:", error);
      throw error;
    }
  }

  async leaveRoom(): Promise<void> {
    if (!this.currentRoom) return;

    // Only try to invoke if actually connected
    if (this.isConnected && this.connection) {
      try {
        await this.connection.invoke("LeaveRoom", this.currentRoom);
        console.log(`[Collaboration] Left room: ${this.currentRoom}`);
      } catch (error) {
        console.error("[Collaboration] Error leaving room:", error);
      }
    }

    // Always clean up local state
    this.currentRoom = null;
    this.collaboratorsCache.clear();
    this.changeHistory = [];
    this.notifyChangeHistory();
  }

  async switchRoom(newRoomKey: string, metadata?: unknown): Promise<void> {
    if (!this.isConnected) {
      await this.connect();
    }

    // Check connection - if not connected (possibly due to abort), just return silently
    if (!this.isConnected || !this.connection) {
      console.log(
        "[Collaboration] Cannot switch room: not connected (connection may have been cancelled)",
      );
      return;
    }

    try {
      await this.connection.invoke(
        "SwitchRoom",
        newRoomKey,
        this.userName,
        this.userColor,
        metadata,
      );
      this.currentRoom = newRoomKey;
      this.collaboratorsCache.clear();
      console.log(`[Collaboration] Switched to room: ${newRoomKey}`);
    } catch (error) {
      console.error("[Collaboration] Failed to switch room:", error);
      throw error;
    }
  }

  // ==================== DATA SYNC ====================

  sendDataChanged(
    itemId: string,
    itemType: string,
    changeType: string,
    newValue: unknown,
    fieldPath?: string,
  ): void {
    if (!this.canSend()) return;

    this.connection!.invoke("DataChanged", this.currentRoom, {
      itemId,
      itemType,
      changeType,
      fieldPath,
      newValue,
    }).catch((err) =>
      console.error("[Collaboration] Error sending data change:", err),
    );
  }

  sendItemCreated(
    itemId: string,
    itemType: string,
    item: unknown,
    parentId?: string,
    index?: number,
  ): void {
    if (!this.canSend()) return;

    this.connection!.invoke("ItemCreated", this.currentRoom, {
      itemId,
      itemType,
      item,
      parentId,
      index,
    }).catch((err) =>
      console.error("[Collaboration] Error sending item created:", err),
    );
  }

  sendItemDeleted(itemId: string, itemType: string): void {
    if (!this.canSend()) return;

    this.connection!.invoke("ItemDeleted", this.currentRoom, {
      itemId,
      itemType,
    }).catch((err) =>
      console.error("[Collaboration] Error sending item deleted:", err),
    );
  }

  sendBatchOperation(
    operationType: string,
    itemType: string,
    data: unknown,
  ): void {
    if (!this.canSend()) return;

    this.connection!.invoke("BatchOperation", this.currentRoom, {
      operationType,
      itemType,
      data,
    }).catch((err) =>
      console.error("[Collaboration] Error sending batch operation:", err),
    );
  }

  // ==================== PRESENCE ====================

  sendSelectionChanged(itemId: string | null): void {
    if (!this.canSend()) return;

    this.connection!.invoke("SelectionChanged", this.currentRoom, itemId).catch(
      (err) => console.error("[Collaboration] Error sending selection:", err),
    );
  }

  sendCursorMoved(x: number, y: number, viewId?: string | null): void {
    if (!this.canSend()) return;

    const now = Date.now();

    if (now - this.lastCursorSend >= CURSOR_THROTTLE_MS) {
      this.doSendCursor(x, y, viewId ?? null);
      this.lastCursorSend = now;
      this.pendingCursorUpdate = null;
    } else {
      this.pendingCursorUpdate = { x, y, viewId: viewId ?? null };

      if (!this.cursorThrottleTimer) {
        this.cursorThrottleTimer = setTimeout(
          () => {
            if (this.pendingCursorUpdate) {
              this.doSendCursor(
                this.pendingCursorUpdate.x,
                this.pendingCursorUpdate.y,
                this.pendingCursorUpdate.viewId,
              );
              this.lastCursorSend = Date.now();
              this.pendingCursorUpdate = null;
            }
            this.cursorThrottleTimer = null;
          },
          CURSOR_THROTTLE_MS - (now - this.lastCursorSend),
        );
      }
    }
  }

  private doSendCursor(x: number, y: number, viewId: string | null): void {
    this.connection!.invoke(
      "CursorMoved",
      this.currentRoom,
      x,
      y,
      viewId,
    ).catch((err) =>
      console.error("[Collaboration] Error sending cursor:", err),
    );
  }

  sendViewChanged(viewId: string): void {
    if (!this.canSend()) return;

    this.connection!.invoke("ViewChanged", this.currentRoom, viewId).catch(
      (err) => console.error("[Collaboration] Error sending view change:", err),
    );
  }

  sendUserTyping(itemId: string | null, isTyping: boolean): void {
    if (!this.canSend()) return;

    this.connection!.invoke(
      "UserTyping",
      this.currentRoom,
      itemId,
      isTyping,
    ).catch((err) =>
      console.error("[Collaboration] Error sending typing:", err),
    );
  }

  // ==================== SYNC ====================

  requestSync(): void {
    if (!this.canSend()) return;

    this.connection!.invoke("RequestSync", this.currentRoom).catch((err) =>
      console.error("[Collaboration] Error requesting sync:", err),
    );
  }

  sendSync(targetConnectionId: string, dataJson: string): void {
    if (!this.canSend()) return;

    this.connection!.invoke(
      "SendSync",
      this.currentRoom,
      targetConnectionId,
      dataJson,
    ).catch((err) => console.error("[Collaboration] Error sending sync:", err));
  }

  sendDataSaved(): void {
    console.log(
      "[Collaboration] sendDataSaved called, canSend:",
      this.canSend(),
      "isConnected:",
      this.isConnected,
      "currentRoom:",
      this.currentRoom,
    );
    if (!this.canSend()) {
      console.log(
        "[Collaboration] sendDataSaved: canSend is false, not sending",
      );
      return;
    }

    console.log("[Collaboration] sendDataSaved: invoking DataSaved on server");
    this.connection!.invoke("DataSaved", this.currentRoom, this.userName).catch(
      (err) => console.error("[Collaboration] Error sending data saved:", err),
    );
  }

  /**
   * Broadcast the full template to all collaborators in the room
   * This is more efficient than having each client reload from the server
   * Uses compression for large templates (>10KB)
   * @param templateJson The full template JSON string
   * @param version Incrementing version number for conflict detection
   */
  async broadcastTemplateSync(
    templateJson: string,
    version: number,
  ): Promise<void> {
    if (!this.canSend()) {
      console.log("[Collaboration] broadcastTemplateSync: canSend is false");
      return;
    }

    let dataToSend = templateJson;
    let compressed = false;

    // Compress if large enough and compression is available
    if (
      templateJson.length > COMPRESSION_THRESHOLD &&
      isCompressionAvailable()
    ) {
      try {
        const compressedData = await compressString(templateJson);
        // Only use compression if it actually reduces size
        if (compressedData.length < templateJson.length * 0.9) {
          dataToSend = compressedData;
          compressed = true;
          console.log(
            `[Collaboration] Compressed template: ${templateJson.length} -> ${compressedData.length} bytes (${Math.round((1 - compressedData.length / templateJson.length) * 100)}% reduction)`,
          );
        }
      } catch (err) {
        console.warn(
          "[Collaboration] Compression failed, sending uncompressed:",
          err,
        );
      }
    }

    console.log(
      `[Collaboration] Broadcasting template sync, version ${version}, size: ${dataToSend.length} bytes, compressed: ${compressed}`,
    );

    this.connection!.invoke(
      "BroadcastTemplateSync",
      this.currentRoom,
      dataToSend,
      version,
      compressed,
    ).catch((err) =>
      console.error("[Collaboration] Error broadcasting template sync:", err),
    );
  }

  // ==================== SUBSCRIPTIONS ====================

  onUserJoined(callback: EventCallback<Collaborator>): () => void {
    this.userJoinedListeners.add(callback);
    return () => this.userJoinedListeners.delete(callback);
  }

  onUserLeft(callback: EventCallback<UserLeftMessage>): () => void {
    this.userLeftListeners.add(callback);
    return () => this.userLeftListeners.delete(callback);
  }

  onRoomState(callback: EventCallback<RoomState>): () => void {
    this.roomStateListeners.add(callback);
    return () => this.roomStateListeners.delete(callback);
  }

  onDataChanged(callback: EventCallback<DataChangeMessage>): () => void {
    this.dataChangedListeners.add(callback);
    return () => this.dataChangedListeners.delete(callback);
  }

  onItemCreated(callback: EventCallback<ItemCreatedMessage>): () => void {
    this.itemCreatedListeners.add(callback);
    return () => this.itemCreatedListeners.delete(callback);
  }

  onItemDeleted(callback: EventCallback<ItemDeletedMessage>): () => void {
    this.itemDeletedListeners.add(callback);
    return () => this.itemDeletedListeners.delete(callback);
  }

  onBatchOperation(callback: EventCallback<BatchOperationMessage>): () => void {
    this.batchOperationListeners.add(callback);
    return () => this.batchOperationListeners.delete(callback);
  }

  onSelectionChanged(
    callback: EventCallback<SelectionChangedMessage>,
  ): () => void {
    this.selectionChangedListeners.add(callback);
    return () => this.selectionChangedListeners.delete(callback);
  }

  onCursorMoved(callback: EventCallback<CursorMovedMessage>): () => void {
    this.cursorMovedListeners.add(callback);
    return () => this.cursorMovedListeners.delete(callback);
  }

  onViewChanged(callback: EventCallback<ViewChangedMessage>): () => void {
    this.viewChangedListeners.add(callback);
    return () => this.viewChangedListeners.delete(callback);
  }

  onUserTyping(callback: EventCallback<UserTypingMessage>): () => void {
    this.userTypingListeners.add(callback);
    return () => this.userTypingListeners.delete(callback);
  }

  onSyncRequested(callback: EventCallback<SyncRequestMessage>): () => void {
    this.syncRequestedListeners.add(callback);
    return () => this.syncRequestedListeners.delete(callback);
  }

  onSyncReceived(callback: EventCallback<SyncDataMessage>): () => void {
    this.syncReceivedListeners.add(callback);
    return () => this.syncReceivedListeners.delete(callback);
  }

  onDataSaved(callback: EventCallback<DataSavedMessage>): () => void {
    this.dataSavedListeners.add(callback);
    return () => this.dataSavedListeners.delete(callback);
  }

  onTemplateSync(callback: EventCallback<TemplateSyncMessage>): () => void {
    this.templateSyncListeners.add(callback);
    return () => this.templateSyncListeners.delete(callback);
  }

  onConnectionStateChanged(
    callback: EventCallback<{ connected: boolean; connecting: boolean }>,
  ): () => void {
    this.connectionStateListeners.add(callback);
    return () => this.connectionStateListeners.delete(callback);
  }

  onChangeHistoryUpdated(
    callback: EventCallback<ChangeHistoryEntry[]>,
  ): () => void {
    this.changeHistoryListeners.add(callback);
    return () => this.changeHistoryListeners.delete(callback);
  }

  getChangeHistory(): ChangeHistoryEntry[] {
    return [...this.changeHistory];
  }

  getCollaborator(connectionId: string): Collaborator | undefined {
    return this.collaboratorsCache.get(connectionId);
  }

  // ==================== PRIVATE ====================

  private canSend(): boolean {
    return this.isConnected && this.currentRoom !== null;
  }

  private notifyConnectionState(): void {
    const state = {
      connected: this.isConnected,
      connecting: this.isConnecting,
    };
    this.connectionStateListeners.forEach((cb) => {
      try {
        cb(state);
      } catch (e) {
        console.error("[Collaboration] Listener error:", e);
      }
    });
  }

  private notifyChangeHistory(): void {
    this.changeHistoryListeners.forEach((cb) => {
      try {
        cb([...this.changeHistory]);
      } catch (e) {
        console.error("[Collaboration] Listener error:", e);
      }
    });
  }

  private addChangeHistoryEntry(
    entry: Omit<ChangeHistoryEntry, "id" | "timestamp">,
  ): void {
    const fullEntry: ChangeHistoryEntry = {
      ...entry,
      id: uuidv4(),
      timestamp: new Date(),
    };

    this.changeHistory.unshift(fullEntry);

    if (this.changeHistory.length > this.maxHistoryEntries) {
      this.changeHistory = this.changeHistory.slice(0, this.maxHistoryEntries);
    }

    this.notifyChangeHistory();
  }

  private getCollaboratorName(connectionId?: string): string {
    if (!connectionId) return "Sconosciuto";
    return this.collaboratorsCache.get(connectionId)?.userName || "Sconosciuto";
  }

  private getCollaboratorColor(connectionId?: string): string {
    if (!connectionId) return "#888888";
    return this.collaboratorsCache.get(connectionId)?.color || "#888888";
  }

  private setupEventHandlers(): void {
    if (!this.connection) return;

    // Room state (received on join)
    this.connection.on("RoomState", (state: RoomState) => {
      state.collaborators.forEach((c) =>
        this.collaboratorsCache.set(c.connectionId, c),
      );
      this.roomStateListeners.forEach((cb) => {
        try {
          cb(state);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // User joined
    this.connection.on("UserJoined", (collaborator: Collaborator) => {
      this.collaboratorsCache.set(collaborator.connectionId, collaborator);
      this.userJoinedListeners.forEach((cb) => {
        try {
          cb(collaborator);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // User left
    this.connection.on("UserLeft", (message: UserLeftMessage) => {
      this.collaboratorsCache.delete(message.connectionId);
      this.userLeftListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // Data changed
    this.connection.on("DataChanged", (message: DataChangeMessage) => {
      this.dataChangedListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });

      this.addChangeHistoryEntry({
        type: "data_changed",
        description: `Modificato ${message.itemType}`,
        userName: this.getCollaboratorName(message.senderConnectionId),
        userColor: this.getCollaboratorColor(message.senderConnectionId),
        itemId: message.itemId,
        itemType: message.itemType,
      });
    });

    // Item created
    this.connection.on("ItemCreated", (message: ItemCreatedMessage) => {
      this.itemCreatedListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });

      this.addChangeHistoryEntry({
        type: "item_created",
        description: `Creato ${message.itemType}`,
        userName: this.getCollaboratorName(message.senderConnectionId),
        userColor: this.getCollaboratorColor(message.senderConnectionId),
        itemId: message.itemId,
        itemType: message.itemType,
      });
    });

    // Item deleted
    this.connection.on("ItemDeleted", (message: ItemDeletedMessage) => {
      this.itemDeletedListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });

      this.addChangeHistoryEntry({
        type: "item_deleted",
        description: `Eliminato ${message.itemType}`,
        userName: this.getCollaboratorName(message.senderConnectionId),
        userColor: this.getCollaboratorColor(message.senderConnectionId),
        itemId: message.itemId,
        itemType: message.itemType,
      });
    });

    // Batch operation
    this.connection.on("BatchOperation", (message: BatchOperationMessage) => {
      this.batchOperationListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });

      this.addChangeHistoryEntry({
        type: "batch_operation",
        description: `${message.operationType} su ${message.itemType}`,
        userName: this.getCollaboratorName(message.senderConnectionId),
        userColor: this.getCollaboratorColor(message.senderConnectionId),
        itemType: message.itemType,
      });
    });

    // Selection changed
    this.connection.on(
      "SelectionChanged",
      (message: SelectionChangedMessage) => {
        const collab = this.collaboratorsCache.get(message.connectionId);
        if (collab) collab.selectedItemId = message.itemId;

        this.selectionChangedListeners.forEach((cb) => {
          try {
            cb(message);
          } catch (e) {
            console.error("[Collaboration] Listener error:", e);
          }
        });
      },
    );

    // Cursor moved
    this.connection.on("CursorMoved", (message: CursorMovedMessage) => {
      const collab = this.collaboratorsCache.get(message.connectionId);
      if (collab) {
        collab.cursorX = message.x;
        collab.cursorY = message.y;
        collab.currentViewId = message.viewId;
      }

      this.cursorMovedListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // View changed
    this.connection.on("ViewChanged", (message: ViewChangedMessage) => {
      const collab = this.collaboratorsCache.get(message.connectionId);
      if (collab) collab.currentViewId = message.viewId;

      this.viewChangedListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // User typing
    this.connection.on("UserTyping", (message: UserTypingMessage) => {
      this.userTypingListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // Sync requested
    this.connection.on("SyncRequested", (message: SyncRequestMessage) => {
      this.syncRequestedListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // Sync received
    this.connection.on("SyncReceived", (message: SyncDataMessage) => {
      this.syncReceivedListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // Data saved
    this.connection.on("DataSaved", (message: DataSavedMessage) => {
      this.dataSavedListeners.forEach((cb) => {
        try {
          cb(message);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // Template sync (full template broadcast)
    this.connection.on("TemplateSync", async (message: TemplateSyncMessage) => {
      console.log(
        `[Collaboration] Received TemplateSync, version ${message.version}, size: ${message.templateJson.length} bytes, compressed: ${message.compressed}`,
      );

      // Decompress if needed
      let finalMessage = message;
      if (message.compressed) {
        try {
          const decompressedJson = await decompressString(message.templateJson);
          console.log(
            `[Collaboration] Decompressed template: ${message.templateJson.length} -> ${decompressedJson.length} bytes`,
          );
          finalMessage = {
            ...message,
            templateJson: decompressedJson,
            compressed: false,
          };
        } catch (e) {
          console.error("[Collaboration] Decompression failed:", e);
          return; // Don't notify listeners with corrupted data
        }
      }

      this.templateSyncListeners.forEach((cb) => {
        try {
          cb(finalMessage);
        } catch (e) {
          console.error("[Collaboration] Listener error:", e);
        }
      });
    });

    // Reconnection handling
    this.connection.onreconnecting(() => {
      console.log("[Collaboration] Reconnecting...");
      this.notifyConnectionState();
    });

    this.connection.onreconnected(async () => {
      console.log("[Collaboration] Reconnected");
      this.notifyConnectionState();

      // Rejoin room if we were in one - with a small delay to ensure connection is stable
      if (
        this.currentRoom &&
        this.connection?.state === signalR.HubConnectionState.Connected
      ) {
        try {
          await this.connection.invoke(
            "JoinRoom",
            this.currentRoom,
            this.userName,
            this.userColor,
            null,
          );
          console.log(`[Collaboration] Rejoined room: ${this.currentRoom}`);
        } catch (err) {
          console.error("[Collaboration] Error rejoining room:", err);
        }
      }
    });

    this.connection.onclose(() => {
      console.log("[Collaboration] Connection closed");
      this.notifyConnectionState();
    });
  }
}

// Singleton export
export const collaborationService = new CollaborationService();
