import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import {
  loadDashboardRuntime,
  type DashboardRuntimeOptions,
  type DashboardRuntimeState,
} from "$lib/server/dashboard";
import {
  resolveDashboardDatasources,
  resolveDashboardTile,
  type DashboardTileReference,
  type DashboardTileResolution,
  type DatasourceResolutionOptions,
} from "$lib/server/datasources";

export interface LoadDashboardResponseOptions
  extends Pick<
    DashboardRuntimeOptions,
    "refreshSeedDocument" | "seedIfEmpty" | "seedDocument"
  >,
    DatasourceResolutionOptions {
  disableLiveDatasources?: boolean;
  hydrateLiveDatasources?: boolean;
  logTileResolution?: (event: DashboardTileResolutionLogEvent) => void;
  now?: () => number;
  tileCache?: DashboardTileCache;
}

interface DashboardTileCacheEntry {
  expiresAt: number;
  response: DashboardTileResolution;
}

export interface DashboardTileCache {
  resolve(
    key: string,
    ttlMs: number,
    now: number,
    load: () => Promise<DashboardTileResolution>,
  ): Promise<DashboardTileCacheResult>;
}

const dashboardTileResponseHeaders = {
  "Cache-Control": "private, max-age=5, stale-while-revalidate=30",
};

const dashboardBatchTileConcurrency = 6;

const defaultDashboardTileCache = createDashboardTileCache();

interface DashboardTileCacheResult {
  cache: "hit" | "miss";
  coalesced: boolean;
  response: DashboardTileResolution;
}

export interface DashboardTileResolutionLogEvent {
  cache: "bypass" | "hit" | "miss";
  coalesced: boolean;
  durationMs: number;
  errorCategory?: string;
  status: DashboardTileResolution["state"] | "error";
  tileKey: string;
}

export interface DashboardTilesBatchResponse {
  state: "ready";
  tiles: DashboardTileResolution[];
}

export function createDashboardTileCache(): DashboardTileCache {
  const entries = new Map<string, DashboardTileCacheEntry>();
  const inFlight = new Map<string, Promise<DashboardTileResolution>>();

  return {
    async resolve(key, ttlMs, now, load) {
      const cached = entries.get(key);
      if (cached && cached.expiresAt > now) {
        return {
          cache: "hit",
          coalesced: false,
          response: cached.response,
        };
      }

      const active = inFlight.get(key);
      if (active) {
        return active
          .then((response) => ({
            cache: "miss" as const,
            coalesced: true,
            response,
          }))
          .catch((error) => {
            throw new DashboardTileCacheResolutionError(error, {
              cache: "miss",
              coalesced: true,
            });
          });
      }

      const request = load()
        .then((response) => {
          if (response.state === "ready") {
            entries.set(key, {
              expiresAt: now + ttlMs,
              response,
            });
          }

          return response;
        })
        .finally(() => {
          inFlight.delete(key);
        });

      inFlight.set(key, request);
      return request.then((response) => ({
        cache: "miss" as const,
        coalesced: false,
        response,
      }));
    },
  };
}

export async function loadDashboardResponse(
  options: LoadDashboardResponseOptions = {},
): Promise<DashboardRuntimeState> {
  const liveHydrationEnabled =
    !options.disableLiveDatasources &&
    process.env.DISABLE_LIVE_DATASOURCES !== "1";
  const dashboard = loadDashboardRuntime(undefined, {
    refreshSeedDocument: options.refreshSeedDocument ?? true,
    seedIfEmpty: options.seedIfEmpty ?? true,
    seedDocument: options.seedDocument,
  });

  if (dashboard.state !== "ready") {
    return dashboard;
  }

  if (
    !options.hydrateLiveDatasources ||
    options.disableLiveDatasources ||
    process.env.DISABLE_LIVE_DATASOURCES === "1"
  ) {
    return {
      ...dashboard,
      liveDatasourceHydration: {
        enabled: liveHydrationEnabled,
      },
    };
  }

  return {
    ...dashboard,
    document: await resolveDashboardDatasources(dashboard.document, options),
    liveDatasourceHydration: {
      enabled: false,
    },
  };
}

export async function loadDashboardTileResponse(
  tile: DashboardTileReference,
  options: LoadDashboardResponseOptions = {},
): Promise<DashboardTileResolution> {
  if (
    options.disableLiveDatasources ||
    process.env.DISABLE_LIVE_DATASOURCES === "1"
  ) {
    const tileKey = dashboardTileCacheKey(tile);
    const startedAt = options.now?.() ?? Date.now();
    const response = {
      state: "disabled",
      tile,
      message: "Live datasource hydration is disabled.",
    } satisfies DashboardTileResolution;
    logDashboardTileResolution(options, {
      cache: "bypass",
      coalesced: false,
      durationMs: elapsedDashboardTileMs(startedAt, options),
      status: response.state,
      tileKey,
    });
    return response;
  }

  const tileKey = dashboardTileCacheKey(tile);
  const startedAt = options.now?.() ?? Date.now();

  const dashboard = loadDashboardRuntime(undefined, {
    refreshSeedDocument: options.refreshSeedDocument ?? true,
    seedIfEmpty: options.seedIfEmpty ?? true,
    seedDocument: options.seedDocument,
  });

  if (dashboard.state !== "ready") {
    const response = {
      state: "not_found",
      tile,
      message: `Dashboard is not ready: ${dashboard.state}`,
    } satisfies DashboardTileResolution;
    logDashboardTileResolution(options, {
      cache: "bypass",
      coalesced: false,
      durationMs: elapsedDashboardTileMs(startedAt, options),
      status: response.state,
      tileKey,
    });
    return response;
  }

  const cache = options.tileCache || defaultDashboardTileCache;
  const now = options.now?.() ?? Date.now();

  try {
    const result = await cache.resolve(
      tileKey,
      dashboardTileTtlMs(dashboard.document, tile),
      now,
      () => resolveDashboardTile(dashboard.document, tile, options),
    );
    logDashboardTileResolution(options, {
      cache: result.cache,
      coalesced: result.coalesced,
      durationMs: elapsedDashboardTileMs(startedAt, options),
      status: result.response.state,
      tileKey,
    });
    return result.response;
  } catch (error) {
    const cacheError = dashboardTileCacheResolutionError(error);
    logDashboardTileResolution(options, {
      cache: cacheError?.cache ?? "miss",
      coalesced: cacheError?.coalesced ?? false,
      durationMs: elapsedDashboardTileMs(startedAt, options),
      errorCategory: dashboardTileErrorCategory(cacheError?.cause ?? error),
      status: "error",
      tileKey,
    });
    throw cacheError?.cause ?? error;
  }
}

export async function loadDashboardTilesResponse(
  tiles: DashboardTileReference[],
  options: LoadDashboardResponseOptions = {},
): Promise<DashboardTilesBatchResponse> {
  return {
    state: "ready",
    tiles: await resolveDashboardTilesBatch(tiles, options),
  };
}

export async function handleDashboardRoute(): Promise<Response> {
  return Response.json(await loadDashboardResponse());
}

export async function handleDashboardTileRoute(
  pathname: string,
  options: LoadDashboardResponseOptions = {},
): Promise<Response> {
  const tile = parseDashboardTilePath(pathname);
  if (!tile) {
    return Response.json(
      { ok: false, message: "Invalid dashboard tile route" },
      { status: 404 },
    );
  }

  const response = await loadDashboardTileResponse(tile, options);
  return Response.json(response, {
    status: response.state === "not_found" ? 404 : 200,
    headers: dashboardTileResponseHeaders,
  });
}

export async function handleDashboardTilesRoute(
  request: Request,
  options: LoadDashboardResponseOptions = {},
): Promise<Response> {
  const tiles = await parseDashboardTilesBatchRequest(request);
  if (!tiles) {
    return Response.json(
      { ok: false, message: "Invalid dashboard tiles request" },
      { status: 400 },
    );
  }

  return Response.json(await loadDashboardTilesResponse(tiles, options), {
    headers: dashboardTileResponseHeaders,
  });
}

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

function logDashboardTileResolution(
  options: LoadDashboardResponseOptions,
  event: DashboardTileResolutionLogEvent,
): void {
  if (options.logTileResolution) {
    options.logTileResolution(event);
    return;
  }

  console.info("dashboard.tile", event);
}

function elapsedDashboardTileMs(
  startedAt: number,
  options: LoadDashboardResponseOptions,
): number {
  const now = options.now?.() ?? Date.now();
  return Math.max(0, now - startedAt);
}

function dashboardTileErrorCategory(error: unknown): string {
  if (
    typeof error === "object" &&
    error !== null &&
    "name" in error &&
    typeof error.name === "string"
  ) {
    return error.name;
  }

  return "unknown";
}

async function resolveDashboardTilesBatch(
  tiles: DashboardTileReference[],
  options: LoadDashboardResponseOptions,
): Promise<DashboardTileResolution[]> {
  const results: DashboardTileResolution[] = new Array(tiles.length);
  let nextIndex = 0;

  async function worker() {
    while (nextIndex < tiles.length) {
      const index = nextIndex;
      nextIndex += 1;
      results[index] = await loadDashboardTileResponse(tiles[index], options);
    }
  }

  await Promise.all(
    Array.from(
      { length: Math.min(dashboardBatchTileConcurrency, tiles.length) },
      () => worker(),
    ),
  );

  return results;
}

async function parseDashboardTilesBatchRequest(
  request: Request,
): Promise<DashboardTileReference[] | null> {
  try {
    const body = await request.json() as unknown;
    if (
      typeof body !== "object" ||
      body === null ||
      !("tiles" in body) ||
      !Array.isArray(body.tiles)
    ) {
      return null;
    }

    const tiles = body.tiles.map(parseDashboardTileReference);
    return tiles.every((tile): tile is DashboardTileReference => tile !== null)
      ? tiles
      : null;
  } catch {
    return null;
  }
}

function parseDashboardTileReference(value: unknown): DashboardTileReference | null {
  if (typeof value !== "object" || value === null || !("kind" in value)) {
    return null;
  }

  if (
    (value.kind === "telemetry" || value.kind === "module") &&
    "id" in value &&
    typeof value.id === "string"
  ) {
    return { kind: value.kind, id: value.id };
  }

  if (
    value.kind === "service" &&
    "groupId" in value &&
    typeof value.groupId === "string" &&
    "id" in value &&
    typeof value.id === "string"
  ) {
    return {
      kind: "service",
      groupId: value.groupId,
      id: value.id,
    };
  }

  if (
    value.kind === "status" &&
    "stripId" in value &&
    typeof value.stripId === "string" &&
    "id" in value &&
    typeof value.id === "string"
  ) {
    return {
      kind: "status",
      stripId: value.stripId,
      id: value.id,
    };
  }

  return null;
}

class DashboardTileCacheResolutionError extends Error {
  readonly cache: "hit" | "miss";
  readonly coalesced: boolean;
  override readonly cause: unknown;

  constructor(
    cause: unknown,
    metadata: Pick<DashboardTileCacheResult, "cache" | "coalesced">,
  ) {
    super("Dashboard tile cache resolution failed");
    this.name = "DashboardTileCacheResolutionError";
    this.cause = cause;
    this.cache = metadata.cache;
    this.coalesced = metadata.coalesced;
  }
}

function dashboardTileCacheResolutionError(
  error: unknown,
): DashboardTileCacheResolutionFailure | null {
  if (
    typeof error === "object" &&
    error !== null &&
    "cache" in error &&
    (error.cache === "hit" || error.cache === "miss") &&
    "coalesced" in error &&
    typeof error.coalesced === "boolean" &&
    "cause" in error
  ) {
    return {
      cache: error.cache,
      cause: error.cause,
      coalesced: error.coalesced,
    };
  }

  return null;
}

interface DashboardTileCacheResolutionFailure {
  cache: "hit" | "miss";
  cause: unknown;
  coalesced: boolean;
}

function dashboardTileTtlMs(
  document: DashboardDocument,
  tile: DashboardTileReference,
): number {
  if (tile.kind === "telemetry") return 15_000;
  if (tile.kind === "service") return 30_000;

  if (tile.kind === "module") {
    const module = document.modules?.find((item) => item.id === tile.id);
    if (
      module?.datasource?.type === "external" &&
      module.datasource.adapter === "weather"
    ) {
      return 10 * 60_000;
    }
    return 30_000;
  }

  if (["system-status", "uptime", "load-avg"].includes(tile.id)) {
    return 30_000;
  }

  return 5 * 60_000;
}

function parseDashboardTilePath(pathname: string): DashboardTileReference | null {
  const parts = pathname.split("/").filter(Boolean);
  const [, dashboard, tileRoot, kind, firstId, secondId] = parts;
  if (dashboard !== "dashboard" || tileRoot !== "tile" || !kind || !firstId) {
    return null;
  }

  const id = decodeURIComponent(firstId);
  if (kind === "telemetry" || kind === "module") {
    return { kind, id };
  }

  if (kind === "service" && secondId) {
    return {
      kind,
      groupId: id,
      id: decodeURIComponent(secondId),
    };
  }

  if (kind === "status" && secondId) {
    return {
      kind,
      stripId: id,
      id: decodeURIComponent(secondId),
    };
  }

  return null;
}
