import {
  createProviderInstrumentation,
  type ProviderInstrumentationPort,
} from "../providers/instrumentation.js";
import {
  BroadcastValidationError,
  broadcastLimits,
  type ChannelDefinition,
  channelKey,
  type InferChannelEvent,
  type InferChannelParams,
  parseChannelEvent,
  parseChannelParams,
} from "./index.js";

export type BroadcastClientStatus =
  | "connecting"
  | "connected"
  | "reconnecting"
  | "blocked"
  | "closed";

/** Why this physical connection was opened, or why it is reconnecting. */
export interface BroadcastConnectionInfo {
  readonly reason:
    | "initial"
    | "planned-renewal"
    | "subscription-change"
    | "interruption"
    | "unknown";
}

/** Safe connection/protocol failure; application callback errors are reported separately. */
export class BroadcastClientError extends Error {
  constructor(
    message: string,
    readonly retryable: boolean,
    readonly status?: number,
  ) {
    super(message);
    this.name = "BroadcastClientError";
  }
}

export interface BroadcastClientSubscription {
  unsubscribe(): void;
  getStatus(): BroadcastClientStatus;
}

export interface BroadcastClient {
  subscribe<C extends ChannelDefinition>(
    channel: C,
    options: {
      params: InferChannelParams<C>;
      onEvent: (event: InferChannelEvent<C>) => void | Promise<void>;
      /** Refetch authoritative state after initial readiness and every recovered connection. */
      onSync: (info: BroadcastConnectionInfo) => void | Promise<void>;
      onError?: (error: unknown) => void;
      onStatusChange?: (
        status: BroadcastClientStatus,
        info: BroadcastConnectionInfo,
      ) => void;
    },
  ): BroadcastClientSubscription;
  /** Merge these optional headers into the app's typed HTTP client to opt into exclusion. */
  getRequestHeaders(): Record<string, string>;
  getStatus(): BroadcastClientStatus;
  /** Explicit app action after credentials/access change. Renewals never unblock denied subscriptions. */
  resume(): void;
  close(): void;
}

export interface BroadcastClientOptions {
  url: string;
  /** Optional existing instrumentation sink; records causes and counts, never credentials or channel params. */
  instrumentation?: ProviderInstrumentationPort;
  /** Resolved afresh for every request. Never place credentials in the URL. */
  headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
  credentials?: RequestCredentials;
  fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}

interface Observer {
  active: boolean;
  validation: AbortController;
  onEvent: (event: { event: string; data: unknown }) => void | Promise<void>;
  onSync: (info: BroadcastConnectionInfo) => void | Promise<void>;
  onError?: (error: unknown) => void;
  onStatusChange?: (
    status: BroadcastClientStatus,
    info: BroadcastConnectionInfo,
  ) => void;
}
interface Entry {
  id: string;
  key: string;
  channel: ChannelDefinition;
  params: Record<string, string>;
  observers: Set<Observer>;
  blocked: boolean;
  ready: boolean;
  failures: number;
  nextAttemptAt: number;
  connectionInfo?: BroadcastConnectionInfo;
}

function retryDelay(failures: number, minimum = 0): number {
  const ceiling = Math.min(30_000, 1_000 * 2 ** Math.min(failures, 5));
  return Math.max(minimum, Math.round(ceiling * (0.5 + Math.random() * 0.5)));
}

function responseRetryDelay(response: Response): number {
  const value = response.headers.get("retry-after");
  if (!value) return 0;
  const seconds = Number(value);
  const delay = Number.isFinite(seconds)
    ? seconds * 1_000
    : Date.parse(value) - Date.now();
  return Number.isFinite(delay) ? Math.max(0, delay) : 0;
}

/** Stop waiting even if an application schema/header callback ignores cancellation. */
async function abortable<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
  let cancel = () => {};
  try {
    return await Promise.race([
      work,
      new Promise<never>((_, reject) => {
        cancel = () =>
          reject(
            new BroadcastClientError("Broadcast operation interrupted", true),
          );
        if (signal.aborted) cancel();
        else signal.addEventListener("abort", cancel, { once: true });
      }),
    ]);
  } finally {
    signal.removeEventListener("abort", cancel);
  }
}

/** One multiplexed streaming Fetch connection per instance. Create an instance per app auth session. */
export function createBroadcastClient(
  options: BroadcastClientOptions,
): BroadcastClient {
  const fetcher = options.fetch ?? globalThis.fetch;
  const instrumentation = createProviderInstrumentation(
    options.instrumentation,
    {
      providerName: "broadcast-client",
      watcher: "broadcast",
    },
  );
  let connectionInfo: BroadcastConnectionInfo = Object.freeze({
    reason: "initial",
  });
  let retryReason: BroadcastConnectionInfo["reason"] = "unknown";
  const clientId = crypto.randomUUID();
  const entries = new Map<string, Entry>();
  const observers = new Set<Observer>();
  let nextId = 0;
  let closed = false;
  let generation = 0;
  let interruptConnection:
    | ((reason: BroadcastConnectionInfo["reason"]) => void)
    | undefined;
  let retryTimer: ReturnType<typeof setTimeout> | undefined;
  let restartQueued = false;
  let attempted = false;

  function status(entry?: Entry): BroadcastClientStatus {
    if (closed) return "closed";
    if (entry?.blocked) return "blocked";
    if (entry?.ready) return "connected";
    if (!entry && [...entries.values()].some((value) => value.ready))
      return "connected";
    if (
      !entry &&
      entries.size > 0 &&
      [...entries.values()].every((value) => value.blocked)
    )
      return "blocked";
    return attempted ? "reconnecting" : "connecting";
  }

  function report(observer: Observer, error: unknown) {
    if (!observer.active) return;
    try {
      observer.onError?.(error);
    } catch {
      /* Error reporting cannot change transport state. */
    }
  }
  function invoke(observer: Observer, callback: () => void | Promise<void>) {
    if (!observer.active) return;
    try {
      void Promise.resolve(callback()).catch((error) =>
        report(observer, error),
      );
    } catch (error) {
      report(observer, error);
    }
  }
  function notify(entry: Entry, info = connectionInfo) {
    entry.connectionInfo = info;
    for (const observer of entry.observers)
      invoke(observer, () => observer.onStatusChange?.(status(entry), info));
  }
  function fail(
    entry: Entry,
    error: BroadcastClientError,
    minimum = 0,
    info: BroadcastConnectionInfo = Object.freeze({ reason: "interruption" }),
  ) {
    retryReason = info.reason;
    entry.ready = false;
    entry.blocked = !error.retryable;
    if (error.retryable)
      entry.nextAttemptAt = Date.now() + retryDelay(entry.failures++, minimum);
    notify(entry, info);
    if (info.reason !== "planned-renewal")
      for (const observer of entry.observers) report(observer, error);
  }

  function clearRetry() {
    if (retryTimer !== undefined) clearTimeout(retryTimer);
    retryTimer = undefined;
  }
  function scheduleRetry() {
    clearRetry();
    const waiting = [...entries.values()].filter(
      (entry) => !entry.blocked && !entry.ready && entry.nextAttemptAt > 0,
    );
    if (!waiting.length || closed) return;
    const delay = Math.max(
      0,
      Math.min(...waiting.map((entry) => entry.nextAttemptAt)) - Date.now(),
    );
    // Long Retry-After values retain their deadline across timer chunks and subscription changes.
    retryTimer = setTimeout(
      () => {
        if (
          waiting.some(
            (entry) => !entry.blocked && entry.nextAttemptAt <= Date.now(),
          )
        )
          restart(retryReason);
        else scheduleRetry();
      },
      Math.min(delay, 2_147_483_647),
    );
  }

  function restart(reason: BroadcastConnectionInfo["reason"] = "unknown") {
    if (closed) return;
    connectionInfo = Object.freeze({ reason: attempted ? reason : "initial" });
    generation++;
    interruptConnection?.(connectionInfo.reason);
    interruptConnection = undefined;
    clearRetry();
    for (const entry of entries.values()) {
      entry.ready = false;
      notify(entry);
    }
    if (restartQueued) return;
    restartQueued = true;
    queueMicrotask(() => {
      restartQueued = false;
      if (!closed) void connect(generation);
    });
  }

  async function connect(current: number) {
    const included = [...entries.values()].filter(
      (entry) => !entry.blocked && entry.nextAttemptAt <= Date.now(),
    );
    if (!included.length) {
      scheduleRetry();
      return;
    }
    for (const entry of included) entry.nextAttemptAt = 0;
    // Joining channels must not cancel deadlines for subscriptions still waiting to retry.
    scheduleRetry();
    attempted = true;
    const info = connectionInfo;
    let endReason: BroadcastConnectionInfo["reason"] = "unknown";
    instrumentation.custom({
      name: "broadcast.connecting",
      details: { reason: info.reason, subscriptions: included.length },
    });
    const abort = new AbortController();
    interruptConnection = (reason) => {
      endReason = reason;
      abort.abort();
    };
    const live = () =>
      !closed && current === generation && !abort.signal.aborted;
    let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
    let timer: ReturnType<typeof setTimeout> | undefined;
    let lifetime: ReturnType<typeof setTimeout> | undefined;
    let readiness: ReturnType<typeof setTimeout> | undefined;
    const arm = (ms: number) => {
      if (timer !== undefined) clearTimeout(timer);
      timer = setTimeout(() => {
        endReason = "interruption";
        abort.abort();
      }, ms);
    };
    let terminal = false;
    let reading = false;
    try {
      arm(broadcastLimits.readyTimeoutMs);
      const url = new URL(
        options.url,
        typeof location === "undefined" ? "http://localhost" : location.href,
      );
      if (
        url.username ||
        url.password ||
        !["http:", "https:"].includes(url.protocol)
      )
        throw new BroadcastClientError("Invalid broadcast endpoint URL", false);
      url.searchParams.set(
        "subscriptions",
        JSON.stringify({
          version: 1,
          channels: included.map((entry) => ({
            id: entry.id,
            name: entry.channel.name,
            params: entry.params,
          })),
        }),
      );
      if (
        new TextEncoder().encode(url.search).byteLength >
        broadcastLimits.requestBytes
      )
        throw new BroadcastClientError(
          "Broadcast subscriptions exceed the request size limit",
          false,
        );
      const headers = new Headers(
        typeof options.headers === "function"
          ? await abortable(
              Promise.resolve().then(options.headers),
              abort.signal,
            )
          : options.headers,
      );
      if (!live()) return;
      headers.set("Accept", "text/event-stream");
      headers.set("X-Beignet-Broadcast-Client", clientId);
      headers.delete("Last-Event-ID");
      const fetching = fetcher(url, {
        method: "GET",
        headers,
        credentials: options.credentials ?? "same-origin",
        cache: "no-store",
        redirect: "error",
        signal: abort.signal,
      });
      void fetching.then(
        (response) => {
          if (!live()) void response.body?.cancel().catch(() => undefined);
        },
        () => undefined,
      );
      const response = await abortable(fetching, abort.signal);
      if (!live()) return;
      if (!response.ok) {
        endReason = "interruption";
        terminal = true;
        const retryable = response.status === 429 || response.status >= 500;
        for (const entry of included)
          fail(
            entry,
            new BroadcastClientError(
              "Broadcast connection was rejected",
              retryable,
              response.status,
            ),
            responseRetryDelay(response),
          );
        void response.body?.cancel().catch(() => undefined);
        return;
      }
      if (
        response.headers.get("content-type")?.split(";", 1)[0]?.trim() !==
          "text/event-stream" ||
        !response.body
      )
        throw new BroadcastClientError("Invalid broadcast response", false);
      reader = response.body.getReader();
      reading = true;
      arm(broadcastLimits.heartbeatMs * 2 + 5_000);
      const connectedAt = performance.now();
      let receivedReadiness = false;
      readiness = setTimeout(() => {
        endReason = "interruption";
        for (const entry of included)
          if (
            !entry.ready &&
            !entry.blocked &&
            entry.nextAttemptAt <= Date.now()
          )
            fail(
              entry,
              new BroadcastClientError("Broadcast readiness timed out", true),
            );
        restart("interruption");
      }, broadcastLimits.readyTimeoutMs);
      const byId = new Map(included.map((entry) => [entry.id, entry]));
      const decoder = new TextDecoder("utf-8", { fatal: true });
      let buffer = "";
      let data: string[] = [];
      let eventName = "";
      let frameBytes = 0;
      let renewalAnnounced = false;
      async function line(value: string) {
        if (!value) {
          if (data.length) {
            renewalAnnounced = false;
            if (eventName !== "broadcast")
              throw new BroadcastClientError("Unknown broadcast frame", false);
            const frame: unknown = JSON.parse(data.join("\n"));
            if (!frame || typeof frame !== "object")
              throw new BroadcastClientError("Invalid broadcast frame", false);
            const message = frame as Record<string, unknown>;
            if (message.type === "renewal") {
              if (Object.keys(message).length !== 1)
                throw new BroadcastClientError(
                  "Invalid broadcast renewal",
                  false,
                );
              renewalAnnounced = true;
              data = [];
              eventName = "";
              frameBytes = 0;
              return;
            }
            const entry =
              typeof message.id === "string" ? byId.get(message.id) : undefined;
            if (!entry || typeof message.type !== "string")
              throw new BroadcastClientError(
                "Invalid broadcast subscription frame",
                false,
              );
            if (entry.blocked || entry.nextAttemptAt > Date.now()) {
              data = [];
              eventName = "";
              frameBytes = 0;
              return;
            }
            if (message.type === "ready") {
              if (entry.ready)
                throw new BroadcastClientError(
                  "Duplicate broadcast readiness",
                  false,
                );
              const advertisedLifetime = message.maxLifetimeMs;
              if (
                typeof advertisedLifetime !== "number" ||
                !Number.isSafeInteger(advertisedLifetime) ||
                advertisedLifetime < 1 ||
                advertisedLifetime > broadcastLimits.maxLifetimeMs
              )
                throw new BroadcastClientError(
                  "Invalid broadcast connection lifetime",
                  false,
                );
              // Only the first readiness can configure this connection's deadline.
              // Count from response arrival, never from a subscription or heartbeat.
              if (!receivedReadiness) {
                lifetime = setTimeout(
                  () => {
                    endReason = "interruption";
                    abort.abort();
                  },
                  Math.max(
                    0,
                    advertisedLifetime +
                      broadcastLimits.lifetimeGraceMs -
                      (performance.now() - connectedAt),
                  ),
                );
              }
              receivedReadiness = true;
              entry.ready = true;
              entry.failures = 0;
              entry.nextAttemptAt = 0;
              notify(entry, info);
              instrumentation.custom({
                name: "broadcast.ready",
                details: { reason: info.reason },
              });
              for (const observer of entry.observers)
                invoke(observer, () => observer.onSync(info));
            } else if (message.type === "event") {
              if (!entry.ready || typeof message.event !== "string")
                throw new BroadcastClientError(
                  "Event before broadcast readiness",
                  false,
                );
              const event = await abortable(
                parseChannelEvent(entry.channel, {
                  event: message.event,
                  data: message.data,
                }),
                abort.signal,
              );
              if (live())
                for (const observer of entry.observers)
                  invoke(observer, () => observer.onEvent(event));
            } else if (
              message.type === "rejected" ||
              message.type === "unavailable"
            ) {
              if (
                typeof message.status !== "number" ||
                !Number.isInteger(message.status) ||
                message.status < 400 ||
                message.status > 599
              )
                throw new BroadcastClientError(
                  "Invalid broadcast failure",
                  false,
                );
              const retryable = message.type === "unavailable";
              if (
                retryable !== (message.status === 429 || message.status >= 500)
              )
                throw new BroadcastClientError(
                  "Invalid broadcast failure classification",
                  false,
                );
              const retryAfter = message.retryAfterMs;
              if (
                retryAfter !== undefined &&
                (typeof retryAfter !== "number" ||
                  !Number.isFinite(retryAfter) ||
                  retryAfter < 0)
              )
                throw new BroadcastClientError(
                  "Invalid broadcast retry delay",
                  false,
                );
              endReason = "interruption";
              fail(
                entry,
                new BroadcastClientError(
                  "Broadcast subscription is unavailable",
                  retryable,
                  message.status,
                ),
                typeof retryAfter === "number" ? retryAfter : 0,
              );
              scheduleRetry();
            } else
              throw new BroadcastClientError(
                "Unknown broadcast control frame",
                false,
              );
            if (
              included.every(
                (entry) =>
                  entry.ready ||
                  entry.blocked ||
                  entry.nextAttemptAt > Date.now(),
              )
            ) {
              if (readiness !== undefined) clearTimeout(readiness);
              readiness = undefined;
            }
            if (
              included.every(
                (entry) => entry.blocked || entry.nextAttemptAt > Date.now(),
              )
            ) {
              terminal = true;
              abort.abort();
            }
          }
          data = [];
          eventName = "";
          frameBytes = 0;
          return;
        }
        // Renewal is terminal: subsequent traffic makes the eventual EOF unexplained.
        renewalAnnounced = false;
        frameBytes += new TextEncoder().encode(value).byteLength;
        if (
          frameBytes >
          broadcastLimits.payloadBytes + broadcastLimits.requestBytes
        )
          throw new BroadcastClientError(
            "Broadcast frame exceeds the size limit",
            false,
          );
        if (value.startsWith(":")) return;
        const colon = value.indexOf(":");
        const field = colon < 0 ? value : value.slice(0, colon);
        const content =
          colon < 0 ? "" : value.slice(colon + 1).replace(/^ /, "");
        if (field === "data") data.push(content);
        else if (field === "event") eventName = content;
        else if (field === "id" || field === "retry")
          throw new BroadcastClientError(
            "Broadcast replay fields are not supported",
            false,
          );
      }
      while (live()) {
        const chunk = await abortable(reader.read(), abort.signal);
        if (!live()) break;
        if (chunk.done) {
          endReason =
            renewalAnnounced && !buffer && frameBytes === 0
              ? "planned-renewal"
              : "unknown";
          break;
        }
        arm(broadcastLimits.heartbeatMs * 2 + 5_000);
        if (chunk.value.byteLength > broadcastLimits.bufferedBytes)
          throw new BroadcastClientError(
            "Broadcast stream exceeds the buffer limit",
            false,
          );
        try {
          buffer += decoder.decode(chunk.value, { stream: true });
        } catch {
          throw new BroadcastClientError(
            "Invalid broadcast text encoding",
            false,
          );
        }
        if (
          new TextEncoder().encode(buffer).byteLength >
          broadcastLimits.bufferedBytes
        )
          throw new BroadcastClientError(
            "Broadcast stream exceeds the buffer limit",
            false,
          );
        for (;;) {
          const match = /\r\n|\r|\n/.exec(buffer);
          if (!match) break;
          if (match[0] === "\r" && match.index === buffer.length - 1) break;
          const value = buffer.slice(0, match.index);
          buffer = buffer.slice(match.index + match[0].length);
          await line(value);
          if (!live()) break;
        }
      }
    } catch (error) {
      if (current === generation && !closed) endReason = "interruption";
      if (
        current === generation &&
        !closed &&
        error instanceof BroadcastClientError &&
        !error.retryable
      ) {
        terminal = true;
        for (const entry of included) fail(entry, error);
      } else if (
        current === generation &&
        !closed &&
        (error instanceof BroadcastValidationError ||
          (reading && !abort.signal.aborted && error instanceof SyntaxError))
      ) {
        terminal = true;
        for (const entry of included)
          fail(
            entry,
            new BroadcastClientError("Invalid broadcast payload", false),
          );
      }
    } finally {
      if (timer !== undefined) clearTimeout(timer);
      if (lifetime !== undefined) clearTimeout(lifetime);
      if (readiness !== undefined) clearTimeout(readiness);
      abort.abort();
      void reader?.cancel().catch(() => undefined);
      const ended = Object.freeze({ reason: endReason });
      instrumentation.custom({
        name: "broadcast.closed",
        details: { reason: ended.reason },
      });
      if (current === generation && !closed) {
        retryReason = endReason;
        interruptConnection = undefined;
        if (!terminal)
          for (const entry of included) {
            if (!entry.blocked && entry.nextAttemptAt <= Date.now())
              fail(
                entry,
                new BroadcastClientError(
                  endReason === "planned-renewal"
                    ? "Broadcast connection renewed"
                    : "Broadcast connection interrupted",
                  true,
                ),
                0,
                ended,
              );
          }
        scheduleRetry();
      }
    }
  }

  function wake() {
    if (
      typeof document !== "undefined" &&
      document.visibilityState === "hidden"
    )
      return;
    restart();
  }
  if (typeof window !== "undefined") window.addEventListener("online", wake);
  if (typeof document !== "undefined")
    document.addEventListener("visibilitychange", wake);

  return {
    subscribe(channel, subscriptionOptions) {
      if (closed)
        throw new BroadcastClientError("Broadcast client is closed", false);
      let entry: Entry | undefined;
      let invalid = false;
      const observer: Observer = {
        ...subscriptionOptions,
        active: true,
        validation: new AbortController(),
        onEvent: (event) =>
          subscriptionOptions.onEvent(
            event as InferChannelEvent<typeof channel>,
          ),
      };
      observers.add(observer);
      const validationTimeout = setTimeout(
        () => observer.validation.abort(),
        broadcastLimits.readyTimeoutMs,
      );
      void abortable(
        parseChannelParams(channel, subscriptionOptions.params),
        observer.validation.signal,
      )
        .then((params) => {
          if (!observer.active || closed) return;
          const key = channelKey(channel.name, params);
          entry = entries.get(key);
          if (!entry) {
            if (entries.size >= broadcastLimits.subscriptions)
              throw new BroadcastClientError(
                "Broadcast client supports at most 20 distinct subscriptions",
                false,
              );
            entry = {
              id: String(++nextId),
              key,
              channel,
              params,
              observers: new Set(),
              blocked: false,
              ready: false,
              failures: 0,
              nextAttemptAt: 0,
            };
            entries.set(key, entry);
            entry.observers.add(observer);
            restart("subscription-change");
          } else {
            if (entry.channel !== channel)
              throw new BroadcastClientError(
                `Conflicting definitions for channel ${channel.name}`,
                false,
              );
            entry.observers.add(observer);
            invoke(observer, () =>
              observer.onStatusChange?.(
                status(entry),
                entry?.connectionInfo ?? connectionInfo,
              ),
            );
            if (entry.ready)
              invoke(observer, () =>
                observer.onSync(entry?.connectionInfo ?? connectionInfo),
              );
          }
        })
        .catch((error) => {
          invalid = true;
          report(observer, error);
          invoke(observer, () =>
            observer.onStatusChange?.("blocked", connectionInfo),
          );
        })
        .finally(() => clearTimeout(validationTimeout));
      return {
        getStatus: () =>
          !observer.active || closed
            ? "closed"
            : invalid
              ? "blocked"
              : status(entry),
        unsubscribe() {
          if (!observer.active) return;
          observer.active = false;
          observer.validation.abort();
          observers.delete(observer);
          entry?.observers.delete(observer);
          if (
            entry &&
            !entry.observers.size &&
            entries.get(entry.key) === entry
          ) {
            entries.delete(entry.key);
            restart("subscription-change");
          }
        },
      };
    },
    getRequestHeaders: () => ({ "X-Beignet-Broadcast-Client": clientId }),
    getStatus: () => status(),
    resume() {
      for (const entry of entries.values()) {
        entry.blocked = false;
        entry.nextAttemptAt = 0;
        entry.failures = 0;
      }
      restart();
    },
    close() {
      if (closed) return;
      closed = true;
      generation++;
      interruptConnection?.("unknown");
      clearRetry();
      if (typeof window !== "undefined")
        window.removeEventListener("online", wake);
      if (typeof document !== "undefined")
        document.removeEventListener("visibilitychange", wake);
      for (const observer of observers) {
        invoke(observer, () =>
          observer.onStatusChange?.(
            "closed",
            Object.freeze({ reason: "unknown" }),
          ),
        );
        observer.active = false;
        observer.validation.abort();
      }
      observers.clear();
      entries.clear();
    },
  };
}
