export interface DashboardRefreshPauseState {
  visibilityState: DocumentVisibilityState;
  online: boolean;
}

export type DashboardTileReference =
  | { kind: "telemetry"; id: string }
  | { kind: "service"; groupId: string; id: string }
  | { kind: "module"; id: string }
  | { kind: "status"; stripId: string; id: string };

type SnapshotMetricValue = Record<string, unknown>;

export type DashboardTileSnapshotItem = {
  detail?: string;
  id: string;
  label?: string;
  severity?: string;
  value?: SnapshotMetricValue | string;
} & Record<string, unknown>;

export type DashboardTileSnapshotResponse =
  | {
      state: "ready";
      tile: DashboardTileReference;
      item: DashboardTileSnapshotItem;
    }
  | {
      state: "not_found";
      tile: DashboardTileReference;
      message: string;
    }
  | {
      state: "disabled";
      tile: DashboardTileReference;
      message: string;
    };

export function shouldPauseDashboardRefresh(
  state: DashboardRefreshPauseState,
): boolean {
  return state.visibilityState !== "visible" || !state.online;
}

export function createDashboardRequestAborter() {
  let shellController: AbortController | undefined;
  let tileController: AbortController | undefined;

  function abortController(controller: AbortController | undefined) {
    if (controller && !controller.signal.aborted) {
      controller.abort();
    }
  }

  return {
    beginShellRun(): AbortSignal {
      abortController(shellController);
      abortController(tileController);
      shellController = new AbortController();
      tileController = undefined;
      return shellController.signal;
    },
    beginTileRun(): AbortSignal {
      abortController(tileController);
      tileController = new AbortController();
      return tileController.signal;
    },
    abortActiveRequests(): void {
      abortController(shellController);
      abortController(tileController);
      shellController = undefined;
      tileController = undefined;
    },
  };
}

type DashboardRefreshEventTarget = Pick<
  EventTarget,
  "addEventListener" | "removeEventListener"
>;

export interface DashboardRefreshLifecycleOptions {
  documentTarget: DashboardRefreshEventTarget;
  windowTarget: DashboardRefreshEventTarget;
  loadDashboard: () => void | Promise<void>;
  pauseRefreshes: () => void;
  refreshPaused: () => boolean;
}

export function attachDashboardRefreshLifecycle(
  options: DashboardRefreshLifecycleOptions,
): () => void {
  function handleRefreshLifecycleChange() {
    if (options.refreshPaused()) {
      options.pauseRefreshes();
      return;
    }

    void options.loadDashboard();
  }

  function handlePageHide() {
    options.pauseRefreshes();
  }

  options.documentTarget.addEventListener(
    "visibilitychange",
    handleRefreshLifecycleChange,
  );
  options.windowTarget.addEventListener("online", handleRefreshLifecycleChange);
  options.windowTarget.addEventListener("offline", handleRefreshLifecycleChange);
  options.windowTarget.addEventListener("pagehide", handlePageHide);

  return () => {
    options.documentTarget.removeEventListener(
      "visibilitychange",
      handleRefreshLifecycleChange,
    );
    options.windowTarget.removeEventListener("online", handleRefreshLifecycleChange);
    options.windowTarget.removeEventListener("offline", handleRefreshLifecycleChange);
    options.windowTarget.removeEventListener("pagehide", handlePageHide);
  };
}

export interface DashboardHydrationQueueOptions<TItem> {
  concurrency: number;
  hydrate: (item: TItem) => Promise<void> | void;
  items: TItem[];
  signal: AbortSignal;
}

export async function runDashboardHydrationQueue<TItem>(
  options: DashboardHydrationQueueOptions<TItem>,
): Promise<void> {
  const concurrency = Math.max(1, Math.floor(options.concurrency));
  let nextIndex = 0;

  async function worker() {
    while (!options.signal.aborted) {
      const item = options.items[nextIndex];
      nextIndex += 1;
      if (item === undefined) return;

      await options.hydrate(item);
    }
  }

  const workerCount = Math.min(concurrency, options.items.length);
  await Promise.all(
    Array.from({ length: workerCount }, () => worker()),
  );
}

export interface DashboardViewportHydrationQueueOptions<TItem> {
  batchSize?: number;
  collectVisibleModelIds: () => Promise<ReadonlySet<string>>;
  concurrency: number;
  getModelId: (item: TItem) => string;
  hydrate: (item: TItem) => Promise<void> | void;
  hydrateBatch?: (items: TItem[]) => Promise<void> | void;
  items: TItem[];
  onAllItemsSettled?: () => void;
  onVisibleItemsSettled?: () => void;
  signal: AbortSignal;
  waitForIdle: () => Promise<void>;
}

export async function runViewportAwareDashboardHydrationQueue<TItem>(
  options: DashboardViewportHydrationQueueOptions<TItem>,
): Promise<void> {
  const visibleModelIds = await options.collectVisibleModelIds();
  if (options.signal.aborted) return;

  const { visible, deferred } = splitDashboardHydrationItemsByVisibility({
    getModelId: options.getModelId,
    items: options.items,
    visibleModelIds,
  });

  await runDashboardHydrationItems(options, visible);
  if (options.signal.aborted) return;
  options.onVisibleItemsSettled?.();

  if (!deferred.length) {
    options.onAllItemsSettled?.();
    return;
  }

  await options.waitForIdle();
  if (options.signal.aborted) return;

  await runDashboardHydrationItems(options, deferred);
  options.onAllItemsSettled?.();
}

async function runDashboardHydrationItems<TItem>(
  options: DashboardViewportHydrationQueueOptions<TItem>,
  items: TItem[],
): Promise<void> {
  if (!options.hydrateBatch) {
    await runDashboardHydrationQueue({
      concurrency: options.concurrency,
      hydrate: options.hydrate,
      items,
      signal: options.signal,
    });
    return;
  }

  await runDashboardHydrationBatchQueue({
    batchSize: options.batchSize ?? options.concurrency,
    hydrateBatch: options.hydrateBatch,
    items,
    signal: options.signal,
  });
}

export async function runDashboardHydrationBatchQueue<TItem>(options: {
  batchSize: number;
  hydrateBatch: (items: TItem[]) => Promise<void> | void;
  items: TItem[];
  signal: AbortSignal;
}): Promise<void> {
  const batchSize = Math.max(1, Math.floor(options.batchSize));

  for (let index = 0; index < options.items.length; index += batchSize) {
    if (options.signal.aborted) return;
    await options.hydrateBatch(options.items.slice(index, index + batchSize));
  }
}

export function splitDashboardHydrationItemsByVisibility<TItem>(options: {
  getModelId: (item: TItem) => string;
  items: TItem[];
  visibleModelIds: ReadonlySet<string>;
}): {
  deferred: TItem[];
  visible: TItem[];
} {
  const visible: TItem[] = [];
  const deferred: TItem[] = [];

  for (const item of options.items) {
    if (options.visibleModelIds.has(options.getModelId(item))) {
      visible.push(item);
    } else {
      deferred.push(item);
    }
  }

  return { visible, deferred };
}

export interface DashboardModelElement {
  getAttribute(name: string): string | null;
}

export interface DashboardViewportElementSource {
  querySelectorAll(selector: string): ArrayLike<DashboardModelElement>;
}

export interface DashboardIntersectionEntry {
  intersectionRatio?: number;
  isIntersecting: boolean;
  target: DashboardModelElement;
}

export interface DashboardIntersectionObserver {
  disconnect(): void;
  observe(element: DashboardModelElement): void;
}

export type DashboardIntersectionObserverFactory = (
  callback: (entries: DashboardIntersectionEntry[]) => void,
) => DashboardIntersectionObserver;

type DashboardTimerHandle = ReturnType<typeof setTimeout>;

export interface DashboardVisibleModelIdCollectorOptions {
  clearTimeout?: (handle: DashboardTimerHandle) => void;
  createObserver?: DashboardIntersectionObserverFactory;
  documentTarget: DashboardViewportElementSource;
  modelIds: Iterable<string>;
  setTimeout?: (callback: () => void, timeoutMs: number) => DashboardTimerHandle;
  signal: AbortSignal;
  timeoutMs?: number;
}

export async function collectVisibleDashboardModelIds(
  options: DashboardVisibleModelIdCollectorOptions,
): Promise<Set<string>> {
  const targetIds = new Set(options.modelIds);
  if (!targetIds.size || options.signal.aborted) return new Set();

  const elements = Array.from(
    options.documentTarget.querySelectorAll("[data-model-id]"),
  ).filter((element) => {
    const modelId = element.getAttribute("data-model-id");
    return modelId ? targetIds.has(modelId) : false;
  });

  if (!elements.length) return new Set();
  if (!options.createObserver) return targetIds;
  const createObserver = options.createObserver;

  const setTimer =
    options.setTimeout ||
    ((callback: () => void, timeoutMs: number) =>
      globalThis.setTimeout(callback, timeoutMs));
  const clearTimer =
    options.clearTimeout ||
    ((handle: DashboardTimerHandle) => globalThis.clearTimeout(handle));
  const timeoutMs = options.timeoutMs ?? 80;

  return new Promise((resolve) => {
    const visibleModelIds = new Set<string>();
    let settled = false;
    let timeoutHandle: DashboardTimerHandle | undefined;
    let observer: DashboardIntersectionObserver | undefined;

    function cleanup() {
      if (timeoutHandle !== undefined) {
        clearTimer(timeoutHandle);
      }
      observer?.disconnect();
      options.signal.removeEventListener("abort", finish);
    }

    function finish() {
      if (settled) return;
      settled = true;
      cleanup();
      resolve(visibleModelIds);
    }

    observer = createObserver((entries) => {
      for (const entry of entries) {
        const modelId = entry.target.getAttribute("data-model-id");
        if (
          modelId &&
          targetIds.has(modelId) &&
          (entry.isIntersecting || (entry.intersectionRatio ?? 0) > 0)
        ) {
          visibleModelIds.add(modelId);
        }
      }
    });

    for (const element of elements) {
      observer.observe(element);
    }

    options.signal.addEventListener("abort", finish, { once: true });
    timeoutHandle = setTimer(finish, timeoutMs);
  });
}

export interface DashboardHydrationIdleOptions {
  cancelIdleCallback?: (handle: number) => void;
  clearTimeout?: (handle: DashboardTimerHandle) => void;
  requestIdleCallback?: (
    callback: () => void,
    options?: { timeout?: number },
  ) => number;
  setTimeout?: (callback: () => void, timeoutMs: number) => DashboardTimerHandle;
  signal: AbortSignal;
  timeoutMs?: number;
}

export async function waitForDashboardHydrationIdle(
  options: DashboardHydrationIdleOptions,
): Promise<void> {
  if (options.signal.aborted) return;

  const setTimer =
    options.setTimeout ||
    ((callback: () => void, timeoutMs: number) =>
      globalThis.setTimeout(callback, timeoutMs));
  const clearTimer =
    options.clearTimeout ||
    ((handle: DashboardTimerHandle) => globalThis.clearTimeout(handle));

  await new Promise<void>((resolve) => {
    let settled = false;
    let idleHandle: number | undefined;
    let timeoutHandle: DashboardTimerHandle | undefined;

    function cleanup() {
      if (idleHandle !== undefined) {
        options.cancelIdleCallback?.(idleHandle);
      }
      if (timeoutHandle !== undefined) {
        clearTimer(timeoutHandle);
      }
      options.signal.removeEventListener("abort", finish);
    }

    function finish() {
      if (settled) return;
      settled = true;
      cleanup();
      resolve();
    }

    options.signal.addEventListener("abort", finish, { once: true });
    if (options.requestIdleCallback) {
      idleHandle = options.requestIdleCallback(finish, {
        timeout: options.timeoutMs ?? 1_000,
      });
    } else {
      timeoutHandle = setTimer(finish, 0);
    }
  });
}

export interface DashboardRefreshDelayOptions {
  failureMultiplierLimit?: number;
  jitterRatio?: number;
  random?: () => number;
}

export function createDashboardRefreshDelay(
  options: DashboardRefreshDelayOptions = {},
) {
  const random = options.random || Math.random;
  const jitterRatio = options.jitterRatio ?? 0.1;
  const failureMultiplierLimit = options.failureMultiplierLimit ?? 8;
  let consecutiveFailures = 0;

  return {
    nextDelayMs(baseDelayMs: number): number {
      const failureMultiplier = consecutiveFailures
        ? Math.min(2 ** consecutiveFailures, failureMultiplierLimit)
        : 1;
      const jitterFactor = 1 + ((random() * 2) - 1) * jitterRatio;
      return Math.max(0, Math.round(baseDelayMs * failureMultiplier * jitterFactor));
    },
    recordFailure(): void {
      consecutiveFailures += 1;
    },
    recordSuccess(): void {
      consecutiveFailures = 0;
    },
  };
}

export interface DashboardPerformanceMarkOptions {
  mark?: (name: string) => void;
}

export const dashboardPerformanceMarks = {
  allTilesSettled: "dashboard:all-tiles-settled",
  firstTileReady: "dashboard:first-tile-ready",
  shellLoad: "dashboard:shell-load",
  visibleTilesReady: "dashboard:visible-tiles-ready",
} as const;

export function createDashboardPerformanceMarks(
  options: DashboardPerformanceMarkOptions = {},
) {
  const mark = options.mark ||
    globalThis.performance?.mark?.bind(globalThis.performance);
  let firstTileReadyMarked = false;

  function safeMark(name: string) {
    try {
      mark?.(name);
    } catch {
      // Performance marks are diagnostics only.
    }
  }

  return {
    markAllTilesSettled(): void {
      safeMark(dashboardPerformanceMarks.allTilesSettled);
    },
    markFirstTileReady(): void {
      if (firstTileReadyMarked) return;
      firstTileReadyMarked = true;
      safeMark(dashboardPerformanceMarks.firstTileReady);
    },
    markShellLoad(): void {
      firstTileReadyMarked = false;
      safeMark(dashboardPerformanceMarks.shellLoad);
    },
    markVisibleTilesReady(): void {
      safeMark(dashboardPerformanceMarks.visibleTilesReady);
    },
  };
}

const dashboardTileBackoffDelaysMs = [15_000, 30_000, 60_000, 120_000];

export function createDashboardTileBackoff() {
  const failures = new Map<string, { attempts: number; nextAttemptAt: number }>();

  return {
    canAttempt(key: string, now = Date.now()): boolean {
      const failure = failures.get(key);
      return !failure || now >= failure.nextAttemptAt;
    },
    recordFailure(key: string, now = Date.now()): void {
      const previousAttempts = failures.get(key)?.attempts || 0;
      const attempts = previousAttempts + 1;
      const delay =
        dashboardTileBackoffDelaysMs[
          Math.min(attempts - 1, dashboardTileBackoffDelaysMs.length - 1)
        ];
      failures.set(key, {
        attempts,
        nextAttemptAt: now + delay,
      });
    },
    recordSuccess(key: string): void {
      failures.delete(key);
    },
  };
}

interface DashboardTileSnapshotRecord {
  item: DashboardTileSnapshotItem;
  savedAt: number;
  tile: DashboardTileReference;
}

interface DashboardTileSnapshotPayload {
  currentRevisionId: string;
  schemaVersion: string;
  tiles: DashboardTileSnapshotRecord[];
  version: 1;
}

export interface DashboardTileSnapshotStoreContext {
  currentRevisionId: string;
  schemaVersion: string;
}

export interface DashboardTileSnapshotStoreOptions {
  now?: () => number;
}

export interface RestoredDashboardTileSnapshot {
  ageMs: number;
  response: Extract<DashboardTileSnapshotResponse, { state: "ready" }>;
}

const dashboardTileSnapshotStorageKey = "dimensionlab.dashboard.tiles.v1";

export function createDashboardTileSnapshotStore(
  storage: Storage | undefined,
  options: DashboardTileSnapshotStoreOptions = {},
) {
  const now = options.now || Date.now;

  function read(): DashboardTileSnapshotPayload | null {
    if (!storage) return null;

    try {
      const serialized = storage.getItem(dashboardTileSnapshotStorageKey);
      if (!serialized) return null;

      const payload = JSON.parse(serialized) as Partial<DashboardTileSnapshotPayload>;
      if (
        payload.version !== 1 ||
        typeof payload.currentRevisionId !== "string" ||
        typeof payload.schemaVersion !== "string" ||
        !Array.isArray(payload.tiles)
      ) {
        return null;
      }

      return {
        currentRevisionId: payload.currentRevisionId,
        schemaVersion: payload.schemaVersion,
        tiles: payload.tiles.filter(isDashboardTileSnapshotRecord),
        version: 1,
      };
    } catch {
      return null;
    }
  }

  function write(payload: DashboardTileSnapshotPayload): void {
    if (!storage) return;

    try {
      storage.setItem(dashboardTileSnapshotStorageKey, JSON.stringify(payload));
    } catch {
      // Best-effort warm-start cache; quota and privacy failures are non-fatal.
    }
  }

  function matchingPayload(
    context: DashboardTileSnapshotStoreContext,
  ): DashboardTileSnapshotPayload {
    const payload = read();
    if (
      payload &&
      payload.currentRevisionId === context.currentRevisionId &&
      payload.schemaVersion === context.schemaVersion
    ) {
      return payload;
    }

    return {
      currentRevisionId: context.currentRevisionId,
      schemaVersion: context.schemaVersion,
      tiles: [],
      version: 1,
    };
  }

  function saveReadyTile(
    input: DashboardTileSnapshotStoreContext & {
      response: Extract<DashboardTileSnapshotResponse, { state: "ready" }>;
    },
  ): void {
    const payload = matchingPayload(input);
    const key = dashboardTileSnapshotKey(input.response.tile);
    const nextRecord: DashboardTileSnapshotRecord = {
      item: input.response.item,
      savedAt: now(),
      tile: input.response.tile,
    };
    payload.tiles = [
      nextRecord,
      ...payload.tiles.filter((record) =>
        dashboardTileSnapshotKey(record.tile) !== key
      ),
    ];
    write(payload);
  }

  return {
    restore(context: DashboardTileSnapshotStoreContext): RestoredDashboardTileSnapshot[] {
      const payload = read();
      if (
        !payload ||
        payload.currentRevisionId !== context.currentRevisionId ||
        payload.schemaVersion !== context.schemaVersion
      ) {
        return [];
      }

      const restoredAt = now();
      return payload.tiles.map((record) => ({
        ageMs: Math.max(0, restoredAt - record.savedAt),
        response: {
          state: "ready",
          tile: record.tile,
          item: {
            ...record.item,
            detail: staleDashboardTileDetail(record.item.detail, restoredAt - record.savedAt),
            severity: "stale",
          },
        },
      }));
    },
    saveReadyTile,
    saveTile(input: DashboardTileSnapshotStoreContext & {
      response: DashboardTileSnapshotResponse;
    }): void {
      if (input.response.state === "ready") {
        saveReadyTile({
          currentRevisionId: input.currentRevisionId,
          response: input.response,
          schemaVersion: input.schemaVersion,
        });
      }
    },
  };
}

function dashboardTileSnapshotKey(tile: DashboardTileReference): string {
  return JSON.stringify(tile);
}

function isDashboardTileSnapshotRecord(
  value: unknown,
): value is DashboardTileSnapshotRecord {
  if (!isSnapshotRecord(value)) return false;

  return (
    typeof value.savedAt === "number" &&
    Number.isFinite(value.savedAt) &&
    isDashboardTileReference(value.tile) &&
    isDashboardTileSnapshotItem(value.tile, value.item)
  );
}

function isDashboardTileReference(
  value: unknown,
): value is DashboardTileReference {
  if (!isSnapshotRecord(value) || typeof value.kind !== "string") return false;

  switch (value.kind) {
    case "telemetry":
    case "module":
      return typeof value.id === "string";
    case "service":
      return typeof value.groupId === "string" && typeof value.id === "string";
    case "status":
      return typeof value.stripId === "string" && typeof value.id === "string";
    default:
      return false;
  }
}

function isDashboardTileSnapshotItem(
  tile: DashboardTileReference,
  value: unknown,
): value is DashboardTileSnapshotItem {
  if (!isSnapshotRecord(value) || value.id !== tile.id) return false;

  switch (tile.kind) {
    case "telemetry":
      return (
        typeof value.label === "string" &&
        isMetricValue(value.value) &&
        isSeverity(value.severity) &&
        isOptionalString(value.detail) &&
        isOptionalString(value.description) &&
        isOptionalString(value.icon) &&
        isOptionalNumberArray(value.sparkline)
      );
    case "service":
      return (
        typeof value.label === "string" &&
        typeof value.description === "string" &&
        isSeverity(value.severity) &&
        isOptionalString(value.detail) &&
        isOptionalString(value.icon) &&
        isOptionalLink(value.link)
      );
    case "module":
      return (
        (value.kind === "summary" ||
          value.kind === "weather" ||
          value.kind === "custom") &&
        isOptionalString(value.title) &&
        isOptionalString(value.label) &&
        isOptionalString(value.value) &&
        isOptionalString(value.detail) &&
        isOptionalString(value.icon) &&
        (value.severity === undefined || isSeverity(value.severity))
      );
    case "status":
      return (
        typeof value.label === "string" &&
        typeof value.value === "string" &&
        isOptionalLink(value.link) &&
        (value.severity === undefined || isSeverity(value.severity))
      );
  }
}

function isSnapshotRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function isMetricValue(value: unknown): boolean {
  if (!isSnapshotRecord(value) || typeof value.kind !== "string") return false;

  if (value.kind === "text") {
    return (
      typeof value.value === "string" &&
      isOptionalString(value.unit)
    );
  }

  const numericKinds = ["bytes", "latency", "number", "percent", "temperature"];
  if (!numericKinds.includes(value.kind)) return false;
  if (typeof value.value !== "number" || !Number.isFinite(value.value)) return false;
  if (value.kind === "percent" && (value.value < 0 || value.value > 100)) {
    return false;
  }

  const precision = value.precision;
  return (
    isOptionalString(value.unit) &&
    (precision === undefined ||
      (typeof precision === "number" &&
        Number.isInteger(precision) &&
        precision >= 0 &&
        precision <= 4))
  );
}

function isSeverity(value: unknown): boolean {
  return (
    value === "neutral" ||
    value === "ok" ||
    value === "warning" ||
    value === "danger" ||
    value === "stale" ||
    value === "unavailable"
  );
}

function isOptionalString(value: unknown): boolean {
  return value === undefined || typeof value === "string";
}

function isOptionalNumberArray(value: unknown): boolean {
  return (
    value === undefined ||
    (Array.isArray(value) &&
      value.every((item) => typeof item === "number" && Number.isFinite(item)))
  );
}

function isOptionalLink(value: unknown): boolean {
  return (
    value === undefined ||
    (isSnapshotRecord(value) &&
      typeof value.href === "string" &&
      isOptionalString(value.label) &&
      (value.external === undefined || typeof value.external === "boolean"))
  );
}

function staleDashboardTileDetail(
  detail: string | undefined,
  ageMs: number,
): string | undefined {
  if (!detail) return detail;
  const ageSeconds = Math.max(0, Math.floor(ageMs / 1_000));
  return ageSeconds > 0 ? `${detail} - stale ${ageSeconds}s` : detail;
}
