/**
 * Reference-counted SignalR connection wrapper.
 *
 * Uses dynamic import("@microsoft/signalr") so the library never enters
 * the SSR bundle. Connections are lazily started on first subscriber and
 * closed gracefully after the last unsubscriber + a configurable grace period.
 */

export interface HubOptions {
  hubUrl: string;
  reconnectDelaysMs?: number[];
  gracePeriodMs?: number;
}

export type ConnectionStatus =
  | "idle"
  | "connecting"
  | "live"
  | "reconnecting"
  | "offline";

type StatusHandler = (status: ConnectionStatus) => void;
type MessageHandler = (message: unknown) => void;

/** Minimal subset of HubConnection we actually use -- aids testability. */
interface HubConnectionLike {
  start(): Promise<void>;
  stop(): Promise<void>;
  invoke(methodName: string, ...args: unknown[]): Promise<unknown>;
  on(methodName: string, handler: (...args: unknown[]) => void): void;
  off(methodName: string, handler: (...args: unknown[]) => void): void;
  onclose(callback: (error?: Error) => void): void;
  onreconnecting(callback: (error?: Error) => void): void;
  onreconnected(callback: (connectionId?: string) => void): void;
}

export class SignalRConnection {
  private readonly hubUrl: string;
  private readonly reconnectDelaysMs: number[];
  private readonly gracePeriodMs: number;

  private connection: HubConnectionLike | null = null;
  private buildPromise: Promise<void> | null = null;
  private _status: ConnectionStatus = "idle";
  private refCount = 0;
  private graceTimer: ReturnType<typeof setTimeout> | null = null;

  private readonly statusHandlers = new Set<StatusHandler>();
  private readonly channelHandlers = new Map<string, Set<MessageHandler>>();

  /** Overridable for testing -- avoids importing @microsoft/signalr at module level. */
  _buildConnection: ((url: string, delays: number[]) => Promise<HubConnectionLike>) | null = null;

  constructor(options: HubOptions) {
    this.hubUrl = options.hubUrl;
    this.reconnectDelaysMs = options.reconnectDelaysMs ?? [0, 2000, 10000, 30000];
    this.gracePeriodMs = options.gracePeriodMs ?? 5000;
  }

  get status(): ConnectionStatus {
    return this._status;
  }

  /**
   * Subscribe to messages on a channel. Returns an unsubscribe function.
   * The underlying connection starts lazily on the first subscriber.
   */
  subscribe(channel: string, handler: MessageHandler): () => void {
    const existing = this.channelHandlers.get(channel);
    const handlers = existing ?? new Set<MessageHandler>();
    if (!existing) {
      this.channelHandlers.set(channel, handlers);
    }
    handlers.add(handler);

    this.refCount++;
    this.cancelGraceTimer();
    void this.ensureConnected(channel, handler);

    let removed = false;
    return () => {
      if (removed) return;
      removed = true;
      handlers.delete(handler);
      if (handlers.size === 0) {
        this.channelHandlers.delete(channel);
        this.connection?.off(channel, handler as (...args: unknown[]) => void);
      }
      this.refCount--;
      if (this.refCount <= 0) {
        this.refCount = 0;
        this.startGraceTimer();
      }
    };
  }

  /**
   * Invoke a hub method after the lazy connection has started.
   * Returns false when the hub cannot be reached, so callers can degrade
   * quietly while keeping already-fetched data visible.
   */
  async invoke(methodName: string, ...args: unknown[]): Promise<boolean> {
    if (!this.buildPromise) {
      this.buildPromise = this.buildAndStart();
    }
    await this.buildPromise;

    if (this._status !== "live" || !this.connection) {
      return false;
    }

    try {
      await this.connection.invoke(methodName, ...args);
      return true;
    } catch {
      this.setStatus("offline");
      return false;
    }
  }

  /** Listen for connection status changes. Returns an unsubscribe function. */
  onStatusChange(handler: StatusHandler): () => void {
    this.statusHandlers.add(handler);
    return () => {
      this.statusHandlers.delete(handler);
    };
  }

  /** Tear down connection immediately (used by tests / cleanup). */
  async destroy(): Promise<void> {
    this.cancelGraceTimer();
    this.refCount = 0;
    if (this.connection) {
      await this.connection.stop();
      this.connection = null;
      this.buildPromise = null;
    }
    this.setStatus("idle");
  }

  // ---- internals ----

  private async ensureConnected(channel: string, handler: MessageHandler): Promise<void> {
    if (!this.buildPromise) {
      this.buildPromise = this.buildAndStart();
    }
    await this.buildPromise;

    // Register handler on the live connection
    this.connection?.on(channel, handler as (...args: unknown[]) => void);
  }

  private async buildAndStart(): Promise<void> {
    this.setStatus("connecting");

    const build = this._buildConnection ?? defaultBuildConnection;
    this.connection = await build(this.hubUrl, this.reconnectDelaysMs);

    this.connection.onclose(() => {
      this.setStatus("offline");
    });
    this.connection.onreconnecting(() => {
      this.setStatus("reconnecting");
    });
    this.connection.onreconnected(() => {
      this.setStatus("live");
    });

    try {
      await this.connection.start();
      this.setStatus("live");
    } catch {
      // Hub unreachable — go offline silently, don't retry-spam
      this.setStatus("offline");
    }
  }

  private setStatus(s: ConnectionStatus): void {
    if (this._status === s) return;
    this._status = s;
    for (const h of this.statusHandlers) {
      h(s);
    }
  }

  private startGraceTimer(): void {
    this.cancelGraceTimer();
    this.graceTimer = setTimeout(() => {
      this.graceTimer = null;
      if (this.refCount === 0 && this.connection) {
        void this.connection.stop().then(() => {
          this.connection = null;
          this.buildPromise = null;
          this.setStatus("idle");
        });
      }
    }, this.gracePeriodMs);
  }

  private cancelGraceTimer(): void {
    if (this.graceTimer !== null) {
      clearTimeout(this.graceTimer);
      this.graceTimer = null;
    }
  }
}

// ---- Shared singleton map ----

const sharedConnections = new Map<string, SignalRConnection>();

export function getSharedConnection(options: HubOptions): SignalRConnection {
  const existing = sharedConnections.get(options.hubUrl);
  if (existing) return existing;
  const conn = new SignalRConnection(options);
  sharedConnections.set(options.hubUrl, conn);
  return conn;
}

/** Reset the singleton map (test helper). */
export function _resetSharedConnections(): void {
  sharedConnections.clear();
}

// ---- Default builder (dynamic import) ----

export function _shouldUseLongPollingForDevTrackerProxy(url: string): boolean {
  try {
    const parsed = new URL(url, "http://localhost");
    const isLocalHost =
      parsed.hostname === "localhost" ||
      parsed.hostname === "127.0.0.1" ||
      parsed.hostname === "::1";
    return isLocalHost && parsed.pathname.replace(/\/$/, "") === "/tracker/hub";
  } catch {
    return false;
  }
}

async function defaultBuildConnection(
  url: string,
  delays: number[],
): Promise<HubConnectionLike> {
  const { HubConnectionBuilder, HttpTransportType, LogLevel } = await import(
    "@microsoft/signalr"
  );
  const builder = new HubConnectionBuilder();
  const withUrlOptions = _shouldUseLongPollingForDevTrackerProxy(url)
    ? { transport: HttpTransportType.LongPolling }
    : undefined;

  return (withUrlOptions ? builder.withUrl(url, withUrlOptions) : builder.withUrl(url))
    .withAutomaticReconnect(delays)
    // Suppress SignalR's internal console logging. The hub is optional
    // (failures degrade to polling) and we already handle status via
    // setStatus("offline"); the console noise (CORS errors, negotiate
    // failures) adds no actionable information for users.
    .configureLogging(LogLevel.None)
    .build();
}
