import { useEffect, useState } from "react";
import type {
  DashboardDocument,
  DashboardModule,
  ServiceEntry,
  StatusItem,
  TelemetryCard,
} from "@dimensionlab/dashboard-model";
import type { DashboardRuntimeState } from "$lib/server/dashboard";
import {
  attachDashboardRefreshLifecycle,
  collectVisibleDashboardModelIds,
  createDashboardPerformanceMarks,
  createDashboardRefreshDelay,
  createDashboardTileSnapshotStore,
  createDashboardTileBackoff,
  createDashboardRequestAborter,
  runViewportAwareDashboardHydrationQueue,
  shouldPauseDashboardRefresh,
  waitForDashboardHydrationIdle,
  type DashboardIntersectionObserverFactory,
  type DashboardTileReference,
  type DashboardTileSnapshotStoreContext,
  type RestoredDashboardTileSnapshot,
} from "$lib/client/dashboard-refresh";
import { dashboardDocumentToUiDashboard } from "$lib/ui-adapter/model-renderer";
import {
  DashboardFrame,
  SystemState,
  ThemeToggle,
  persistUiTheme,
  resolveInitialUiTheme,
  type UiTheme,
  type UiSeverity,
  type UiDashboardPreview,
} from "@dimensionlab/ui";

type DashboardTileResponse =
  | {
      state: "ready";
      tile: DashboardTileReference;
      item: DashboardModule | ServiceEntry | StatusItem | TelemetryCard;
    }
  | {
      state: "not_found";
      tile: DashboardTileReference;
      message: string;
    }
  | {
      state: "disabled";
      tile: DashboardTileReference;
      message: string;
    };

type DashboardTilesBatchResponse = {
  state: "ready";
  tiles: DashboardTileResponse[];
};

type DashboardTileHydrationResult = "aborted" | "failed" | "ready";

const dashboardTileHydrationConcurrency = 6;
const dashboardFallbackRefreshIntervalMs = 30_000;
const dashboardViewportObservationTimeoutMs = 80;
type DashboardTileSnapshotStore = ReturnType<typeof createDashboardTileSnapshotStore>;

export function AppStateView({
  dashboard,
  onThemeChange,
  theme,
  hydratingItemIds,
}: {
  dashboard: DashboardRuntimeState;
  hydratingItemIds?: ReadonlySet<string>;
  onThemeChange?: (theme: UiTheme) => void;
  theme?: UiTheme;
}) {
  const themeToggle =
    theme && onThemeChange ? (
      <ThemeToggle theme={theme} onThemeChange={onThemeChange} />
    ) : null;

  if (dashboard.state === "ready") {
    const uiDashboard = markHydratingItems(
      dashboardDocumentToUiDashboard(dashboard.document),
      hydratingItemIds,
    );

    return (
      <DashboardFrame
        dashboard={uiDashboard}
        actions={themeToggle}
      />
    );
  }

  const detail = `${dashboard.subtitle}: ${dashboard.message}`;
  const errors = dashboard.state === "invalid" ? dashboard.errors : [];

  return (
    <main className="state-shell" data-dashboard-state={dashboard.state}>
      {themeToggle ? (
        <div className="state-shell__actions">{themeToggle}</div>
      ) : null}
      <SystemState
        title={dashboard.title}
        detail={detail}
        severity={stateSeverity(dashboard.state)}
        icon={stateIcon(dashboard.state)}
      />
      {errors.length ? (
        <ul aria-label="Validation errors">
          {errors.map((error) => (
            <li key={error}>{error}</li>
          ))}
        </ul>
      ) : null}
    </main>
  );
}

export function resolveDocumentMetadata(dashboard: DashboardRuntimeState): {
  description: string;
  title: string;
} {
  if (dashboard.state === "ready") {
    const uiDashboard = dashboardDocumentToUiDashboard(dashboard.document);
    return {
      title: uiDashboard.title,
      description:
        uiDashboard.subtitle ||
        dashboard.document.metadata.description ||
        uiDashboard.title,
    };
  }

  return {
    title: dashboard.title,
    description: dashboard.subtitle || dashboard.message,
  };
}

export default function App() {
  const [dashboard, setDashboard] =
    useState<DashboardRuntimeState | null>(null);
  const [hydratingItemIds, setHydratingItemIds] = useState<Set<string>>(
    () => new Set(),
  );
  const [theme, setTheme] = useState<UiTheme>(() => {
    if (typeof window === "undefined") return "dark";

    return resolveInitialUiTheme(getThemeStorage());
  });

  useEffect(() => {
    if (!dashboard) return;

    const metadata = resolveDocumentMetadata(dashboard);
    document.title = metadata.title;

    let description = document.querySelector<HTMLMetaElement>(
      'meta[name="description"]',
    );
    if (!description) {
      description = document.createElement("meta");
      description.name = "description";
      document.head.append(description);
    }
    description.content = metadata.description;
  }, [dashboard]);

  useEffect(() => {
    document.documentElement.dataset.uiTheme = theme;
    persistUiTheme(theme, getThemeStorage());
  }, [theme]);

  useEffect(() => {
    let cancelled = false;
    let refreshTimer: number | undefined;
    let lastRefreshIntervalMs = dashboardFallbackRefreshIntervalMs;
    let hydrationRun = 0;
    const requestAborter = createDashboardRequestAborter();
    const performanceMarks = createDashboardPerformanceMarks();
    const refreshDelay = createDashboardRefreshDelay();
    const tileBackoff = createDashboardTileBackoff();
    const tileSnapshotStore = createDashboardTileSnapshotStore(
      getTileSnapshotStorage(),
    );

    function refreshPaused() {
      return shouldPauseDashboardRefresh({
        visibilityState: document.visibilityState,
        online: navigator.onLine,
      });
    }

    function clearRefreshTimer() {
      if (refreshTimer !== undefined) {
        window.clearTimeout(refreshTimer);
        refreshTimer = undefined;
      }
    }

    function scheduleNextRefresh(baseDelayMs: number) {
      clearRefreshTimer();
      if (cancelled || refreshPaused()) return;

      refreshTimer = window.setTimeout(() => {
        refreshTimer = undefined;
        void loadDashboard();
      }, refreshDelay.nextDelayMs(baseDelayMs));
    }

    function pauseRefreshes() {
      clearRefreshTimer();
      requestAborter.abortActiveRequests();
      setHydratingItemIds(new Set());
    }

    async function loadDashboard() {
      if (cancelled || refreshPaused()) return;

      clearRefreshTimer();
      const shellSignal = requestAborter.beginShellRun();

      try {
        const response = await fetch("/api/dashboard", { signal: shellSignal });
        const nextDashboard = (await response.json()) as DashboardRuntimeState;

        if (cancelled || shellSignal.aborted || refreshPaused()) return;
        refreshDelay.recordSuccess();
        performanceMarks.markShellLoad();
        const restored = restoreDashboardTileSnapshots(
          nextDashboard,
          nextDashboard.state === "ready"
            ? tileSnapshotStore.restore({
                currentRevisionId: nextDashboard.currentRevisionId,
                schemaVersion: nextDashboard.schemaVersion,
              })
            : [],
        );
        setDashboard(restored.dashboard);

        const currentRun = ++hydrationRun;
        if (
          restored.dashboard.state === "ready" &&
          restored.dashboard.liveDatasourceHydration?.enabled !== false
        ) {
          const tiles = dashboardHydrationTiles(restored.dashboard.document).filter(
            (tile) => tileBackoff.canAttempt(dashboardTileKey(tile)),
          );
          const tileSignal = requestAborter.beginTileRun();
          setHydratingItemIds(
            new Set(
              tiles
                .map(dashboardTileKey)
                .filter((key) => !restored.restoredItemIds.has(key)),
            ),
          );
          hydrateDashboardTiles(
            tiles,
            currentRun,
            tileSignal,
            {
              currentRevisionId: restored.dashboard.currentRevisionId,
              schemaVersion: restored.dashboard.schemaVersion,
            },
          );
        } else {
          setHydratingItemIds(new Set());
        }

        const refreshIntervalSeconds =
          nextDashboard.state === "ready"
            ? nextDashboard.document.metadata.refreshIntervalSeconds
            : undefined;

        if (refreshIntervalSeconds && !refreshPaused()) {
          lastRefreshIntervalMs = refreshIntervalSeconds * 1000;
          scheduleNextRefresh(lastRefreshIntervalMs);
        }
      } catch (error) {
        if (!isAbortError(error) && !cancelled) {
          refreshDelay.recordFailure();
          console.error("Dashboard refresh failed", error);
          scheduleNextRefresh(lastRefreshIntervalMs);
        }
      }
    }

    function hydrateDashboardTiles(
      tiles: DashboardTileReference[],
      run: number,
      signal: AbortSignal,
      snapshotContext: DashboardTileSnapshotStoreContext,
    ) {
      const modelIds = tiles.map(dashboardTileModelId);

      void runViewportAwareDashboardHydrationQueue({
        batchSize: dashboardTileHydrationConcurrency,
        collectVisibleModelIds: async () => {
          await waitForDashboardRenderFrame(signal);
          return collectVisibleDashboardModelIds({
            clearTimeout: window.clearTimeout.bind(window),
            createObserver: getDashboardIntersectionObserverFactory(),
            documentTarget: document,
            modelIds,
            setTimeout: window.setTimeout.bind(window),
            signal,
            timeoutMs: dashboardViewportObservationTimeoutMs,
          });
        },
        concurrency: dashboardTileHydrationConcurrency,
        getModelId: dashboardTileModelId,
        hydrate: async (tile) => {
          const key = dashboardTileKey(tile);
          const result = await hydrateDashboardTile(
            tile,
            run,
            signal,
            snapshotContext,
          );

          if (result === "ready") {
            tileBackoff.recordSuccess(key);
          } else if (result === "failed") {
            tileBackoff.recordFailure(key);
          }
        },
        hydrateBatch: async (batch) => {
          const results = await hydrateDashboardTileBatch(
            batch,
            run,
            signal,
            snapshotContext,
          );

          for (const { result, tile } of results) {
            const key = dashboardTileKey(tile);
            if (result === "ready") {
              tileBackoff.recordSuccess(key);
            } else if (result === "failed") {
              tileBackoff.recordFailure(key);
            }
          }
        },
        items: tiles,
        onAllItemsSettled: () => performanceMarks.markAllTilesSettled(),
        onVisibleItemsSettled: () => performanceMarks.markVisibleTilesReady(),
        signal,
        waitForIdle: () =>
          waitForDashboardHydrationIdle({
            ...getDashboardIdleCallbacks(),
            clearTimeout: window.clearTimeout.bind(window),
            setTimeout: window.setTimeout.bind(window),
            signal,
          }),
      });
    }

    async function hydrateDashboardTileBatch(
      tiles: DashboardTileReference[],
      run: number,
      signal: AbortSignal,
      snapshotContext: DashboardTileSnapshotStoreContext,
    ): Promise<Array<{ result: DashboardTileHydrationResult; tile: DashboardTileReference }>> {
      try {
        const response = await fetch("/api/dashboard/tiles", {
          body: JSON.stringify({ tiles }),
          headers: { "Content-Type": "application/json" },
          method: "POST",
          signal,
        });
        const batchResponse = (await response.json()) as DashboardTilesBatchResponse;

        if (
          cancelled ||
          signal.aborted ||
          run !== hydrationRun ||
          !response.ok ||
          batchResponse.state !== "ready"
        ) {
          throw new Error("Dashboard tile batch hydration failed");
        }

        const responsesByKey = new Map(
          batchResponse.tiles.map((tileResponse) => [
            dashboardTileMatchKey(tileResponse.tile),
            tileResponse,
          ]),
        );

        return tiles.map((tile) => {
          const key = dashboardTileKey(tile);
          try {
            return {
              tile,
              result: applyDashboardTileHydrationResponse(
                tile,
                responsesByKey.get(dashboardTileMatchKey(tile)),
                run,
                signal,
                snapshotContext,
              ),
            };
          } finally {
            finishDashboardTileHydration(key, run, signal);
          }
        });
      } catch (error) {
        if (signal.aborted || cancelled || run !== hydrationRun) {
          return tiles.map((tile) => ({ result: "aborted", tile }));
        }

        return Promise.all(
          tiles.map(async (tile) => ({
            tile,
            result: await hydrateDashboardTile(tile, run, signal, snapshotContext),
          })),
        );
      }
    }

    async function hydrateDashboardTile(
      tile: DashboardTileReference,
      run: number,
      signal: AbortSignal,
      snapshotContext: DashboardTileSnapshotStoreContext,
    ): Promise<DashboardTileHydrationResult> {
      const key = dashboardTileKey(tile);

      try {
        const response = await fetch(dashboardTileUrl(tile), { signal });
        const tileResponse = (await response.json()) as DashboardTileResponse;

        if (!response.ok) {
          return "failed";
        }

        const result = applyDashboardTileHydrationResponse(
          tile,
          tileResponse,
          run,
          signal,
          snapshotContext,
        );
        if (result !== "ready") {
          return signal.aborted || cancelled || run !== hydrationRun
            ? "aborted"
            : "failed";
        }

        return "ready";
      } catch (error) {
        if (!isAbortError(error) && !cancelled) {
          console.error("Dashboard tile hydration failed", error);
          return "failed";
        }
        return "aborted";
      } finally {
        finishDashboardTileHydration(key, run, signal);
      }
    }

    function applyDashboardTileHydrationResponse(
      tile: DashboardTileReference,
      tileResponse: DashboardTileResponse | undefined,
      run: number,
      signal: AbortSignal,
      snapshotContext: DashboardTileSnapshotStoreContext,
    ): DashboardTileHydrationResult {
      if (
        cancelled ||
        signal.aborted ||
        run !== hydrationRun ||
        !tileResponse ||
        dashboardTileMatchKey(tileResponse.tile) !== dashboardTileMatchKey(tile) ||
        tileResponse.state !== "ready"
      ) {
        return signal.aborted || cancelled || run !== hydrationRun
          ? "aborted"
          : "failed";
      }

      const readyTileResponse = tileResponse;
      setDashboard((current) =>
        current?.state === "ready"
          ? {
              ...current,
              document: applyDashboardTile(current.document, readyTileResponse),
            }
          : current,
      );
      tileSnapshotStore.saveReadyTile({
        ...snapshotContext,
        response: readyTileResponse,
      });
      performanceMarks.markFirstTileReady();
      return "ready";
    }

    function finishDashboardTileHydration(
      key: string,
      run: number,
      signal: AbortSignal,
    ) {
      if (!cancelled && !signal.aborted && run === hydrationRun) {
        setHydratingItemIds((current) => {
          const next = new Set(current);
          next.delete(key);
          return next;
        });
      }
    }

    const detachRefreshLifecycle = attachDashboardRefreshLifecycle({
      documentTarget: document,
      windowTarget: window,
      loadDashboard,
      pauseRefreshes,
      refreshPaused: () => cancelled || refreshPaused(),
    });

    void loadDashboard();

    return () => {
      cancelled = true;
      clearRefreshTimer();
      requestAborter.abortActiveRequests();
      detachRefreshLifecycle();
    };
  }, []);

  return dashboard ? (
    <AppStateView
      dashboard={dashboard}
      hydratingItemIds={hydratingItemIds}
      theme={theme}
      onThemeChange={setTheme}
    />
  ) : null;
}

function stateSeverity(state: DashboardRuntimeState["state"]): UiSeverity {
  if (state === "invalid") return "danger";
  if (state === "loading") return "loading";
  return "stale";
}

function stateIcon(state: DashboardRuntimeState["state"]): string {
  if (state === "invalid") return "mdi:file-alert-outline";
  if (state === "loading") return "mdi:progress-clock";
  return "mdi:tray";
}

function getThemeStorage(): Storage | undefined {
  try {
    return window.localStorage;
  } catch {
    return undefined;
  }
}

function getTileSnapshotStorage(): Storage | undefined {
  try {
    return window.localStorage;
  } catch {
    return undefined;
  }
}

type DashboardIdleWindow = Window & {
  cancelIdleCallback?: (handle: number) => void;
  requestIdleCallback?: (
    callback: () => void,
    options?: { timeout?: number },
  ) => number;
};

function getDashboardIdleCallbacks() {
  const idleWindow = window as DashboardIdleWindow;
  return {
    cancelIdleCallback: idleWindow.cancelIdleCallback?.bind(idleWindow),
    requestIdleCallback: idleWindow.requestIdleCallback?.bind(idleWindow),
  };
}

function getDashboardIntersectionObserverFactory():
  | DashboardIntersectionObserverFactory
  | undefined {
  if (typeof window.IntersectionObserver === "undefined") return undefined;

  return (callback) =>
    new window.IntersectionObserver((entries) => {
      callback(entries);
    });
}

async function waitForDashboardRenderFrame(signal: AbortSignal): Promise<void> {
  if (signal.aborted) return;

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

    function finish() {
      if (settled) return;
      settled = true;
      if (frame !== undefined) window.cancelAnimationFrame(frame);
      signal.removeEventListener("abort", finish);
      resolve();
    }

    signal.addEventListener("abort", finish, { once: true });
    frame = window.requestAnimationFrame(finish);
  });
}

function isAbortError(error: unknown): boolean {
  return (
    typeof error === "object" &&
    error !== null &&
    "name" in error &&
    error.name === "AbortError"
  );
}

export function restoreDashboardTileSnapshots(
  dashboard: DashboardRuntimeState,
  snapshots: RestoredDashboardTileSnapshot[],
): {
  dashboard: DashboardRuntimeState;
  restoredItemIds: Set<string>;
} {
  if (dashboard.state !== "ready" || snapshots.length === 0) {
    return {
      dashboard,
      restoredItemIds: new Set(),
    };
  }

  return snapshots.reduce(
    (current, snapshot) => ({
      dashboard: {
        ...current.dashboard,
        document: applyDashboardTile(
          current.dashboard.document,
          snapshot.response as Extract<DashboardTileResponse, { state: "ready" }>,
        ),
      },
      restoredItemIds: new Set([
        ...current.restoredItemIds,
        dashboardTileKey(snapshot.response.tile),
      ]),
    }),
    {
      dashboard,
      restoredItemIds: new Set<string>(),
    },
  );
}

function markHydratingItems(
  dashboard: UiDashboardPreview,
  hydratingItemIds?: ReadonlySet<string>,
): UiDashboardPreview {
  if (!hydratingItemIds?.size) return dashboard;

  return {
    ...dashboard,
    telemetry: dashboard.telemetry.map((card) =>
      hydratingItemIds.has(`telemetry:${card.id}`)
        ? {
            ...card,
            severity: "loading",
            detail: "loading live telemetry",
          }
        : card,
    ),
    serviceGroups: dashboard.serviceGroups.map((group) => ({
      ...group,
      services: group.services.map((service) =>
        hydratingItemIds.has(`service:${group.id}:${service.id}`)
          ? {
              ...service,
              severity: "loading",
              detail: "loading",
            }
          : service,
      ),
    })),
    modules: dashboard.modules.map((module) =>
      hydratingItemIds.has(`module:${module.id}`)
        ? {
            ...module,
            severity: "loading",
            detail: "loading live data",
          }
        : module,
    ),
    statusItems: dashboard.statusItems.map((item) =>
      hydratingItemIds.has(`status:${item.id}`)
        ? {
            ...item,
            severity: "loading",
            value: "loading",
          }
        : item,
    ),
  };
}

export function dashboardHydrationTiles(
  document: DashboardDocument,
): DashboardTileReference[] {
  const telemetry = document.telemetry
    .filter((card) => card.datasource?.type === "external")
    .map((card): DashboardTileReference => ({ kind: "telemetry", id: card.id }));
  const services = document.serviceGroups.flatMap((group) =>
    group.services
      .filter((service) => service.datasource?.type === "external")
      .map((service): DashboardTileReference => ({
        kind: "service",
        groupId: group.id,
        id: service.id,
      })),
  );
  const modules = (document.modules || [])
    .filter((module) =>
      module.datasource?.type === "external" ||
      module.id === "runtime-health-summary"
    )
    .map((module): DashboardTileReference => ({ kind: "module", id: module.id }));
  const status = document.statusStrips.flatMap((strip) =>
    strip.items
      .map((item): DashboardTileReference => ({
        kind: "status",
        stripId: strip.id,
        id: item.id,
      })),
  );

  return [...status, ...telemetry, ...modules, ...services];
}

function dashboardTileKey(tile: DashboardTileReference): string {
  if (tile.kind === "status") return `${tile.kind}:${tile.stripId}:${tile.id}`;
  if (tile.kind === "service") return `${tile.kind}:${tile.groupId}:${tile.id}`;
  return `${tile.kind}:${tile.id}`;
}

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

function dashboardTileModelId(tile: DashboardTileReference): string {
  if (tile.kind === "status") return `${tile.stripId}:${tile.id}`;
  return tile.id;
}

function dashboardTileUrl(tile: DashboardTileReference): string {
  const parts = tile.kind === "status"
    ? ["api", "dashboard", "tile", tile.kind, tile.stripId, tile.id]
    : tile.kind === "service"
      ? ["api", "dashboard", "tile", tile.kind, tile.groupId, tile.id]
      : ["api", "dashboard", "tile", tile.kind, tile.id];
  return `/${parts.map(encodeURIComponent).join("/")}`;
}

function applyDashboardTile(
  document: DashboardDocument,
  response: Extract<DashboardTileResponse, { state: "ready" }>,
): DashboardDocument {
  if (response.tile.kind === "telemetry") {
    return {
      ...document,
      telemetry: document.telemetry.map((card) =>
        card.id === response.tile.id ? response.item as TelemetryCard : card,
      ),
    };
  }

  if (response.tile.kind === "service") {
    const tile = response.tile;
    return {
      ...document,
      serviceGroups: document.serviceGroups.map((group) => ({
        ...group,
        services: group.id === tile.groupId
          ? group.services.map((service) =>
              service.id === tile.id ? response.item as ServiceEntry : service,
            )
          : group.services,
      })),
    };
  }

  if (response.tile.kind === "module") {
    return {
      ...document,
      modules: (document.modules || []).map((module) =>
        module.id === response.tile.id ? response.item as DashboardModule : module,
      ),
    };
  }

  const tile = response.tile;
  return {
    ...document,
    statusStrips: document.statusStrips.map((strip) =>
      strip.id === tile.stripId
        ? {
            ...strip,
            items: strip.items.map((item) =>
              item.id === tile.id ? response.item as StatusItem : item,
            ),
          }
        : strip,
    ),
  };
}
