import type { Page } from "playwright";

export type SettleOptions = {
  timeoutMs: number;
  appSignalSelector?: string;
  quietWindowMs?: number;
  networkIdleMs?: number;
  domMutationQuietMs?: number;
  animationQuietMs?: number;
  taskQuietMs?: number;
};

export type SettledReport = {
  signal: "app" | "quiet-windows";
  timedOut: boolean;
  phases: Record<string, number>;
};

const DEFAULT_QUIET_WINDOW_MS = 50;
const DEFAULT_NETWORK_IDLE_MS = 500;
const DEFAULT_DOM_QUIET_MS = 50;
const DEFAULT_ANIMATION_QUIET_MS = 50;
const DEFAULT_TASK_QUIET_MS = 50;

type QuietSnapshot = {
  networkIdle: boolean;
  domQuiet: boolean;
  animationQuiet: boolean;
  taskQuiet: boolean;
};

function assertPositiveFiniteNumber(value: number, field: string): void {
  if (!Number.isFinite(value) || value <= 0) {
    throw new Error(`${field} must be a positive finite number`);
  }
}

function validateOptions(opts: SettleOptions): void {
  assertPositiveFiniteNumber(opts.timeoutMs, "timeoutMs");
  if (opts.quietWindowMs !== undefined) {
    assertPositiveFiniteNumber(opts.quietWindowMs, "quietWindowMs");
  }
  if (opts.networkIdleMs !== undefined) {
    assertPositiveFiniteNumber(opts.networkIdleMs, "networkIdleMs");
  }
  if (opts.domMutationQuietMs !== undefined) {
    assertPositiveFiniteNumber(opts.domMutationQuietMs, "domMutationQuietMs");
  }
  if (opts.animationQuietMs !== undefined) {
    assertPositiveFiniteNumber(opts.animationQuietMs, "animationQuietMs");
  }
  if (opts.taskQuietMs !== undefined) {
    assertPositiveFiniteNumber(opts.taskQuietMs, "taskQuietMs");
  }
}

export function buildSettleInitScript(): string {
  return `(() => {
    const globalState = window;
    if (globalState.__invSettleState !== undefined) return;
    const state = {
      lastMutationAt: performance.now(),
      lastAnimationAt: performance.now(),
      lastTaskSettledAt: performance.now(),
      pendingTasks: 0,
    };
    const observer = new MutationObserver(() => {
      state.lastMutationAt = performance.now();
    });
    observer.observe(document.documentElement, {
      subtree: true,
      childList: true,
      attributes: true,
      characterData: true,
    });
    const originalRequestAnimationFrame = window.requestAnimationFrame.bind(window);
    window.requestAnimationFrame = (callback) => originalRequestAnimationFrame((time) => {
      state.lastAnimationAt = performance.now();
      callback(time);
    });
    if (
      typeof window.requestIdleCallback === "function" &&
      typeof window.cancelIdleCallback === "function"
    ) {
      const originalRequestIdleCallback = window.requestIdleCallback.bind(window);
      const originalCancelIdleCallback = window.cancelIdleCallback.bind(window);
      const pendingIdleCallbacks = new Set();
      const settleIdleCallback = (id) => {
        if (!pendingIdleCallbacks.delete(id)) return;
        state.pendingTasks -= 1;
        state.lastTaskSettledAt = performance.now();
      };
      window.requestIdleCallback = (callback, options) => {
        let id = 0;
        id = originalRequestIdleCallback((deadline) => {
          try {
            callback(deadline);
          } finally {
            settleIdleCallback(id);
          }
        }, options);
        pendingIdleCallbacks.add(id);
        state.pendingTasks += 1;
        return id;
      };
      window.cancelIdleCallback = (id) => {
        settleIdleCallback(id);
        originalCancelIdleCallback(id);
      };
    }
    globalState.__invSettleState = state;
  })();`;
}

async function installQuietObservers(page: Page): Promise<void> {
  await page.evaluate(buildSettleInitScript());
}

function trackNetworkIdle(page: Page): { pending: () => number } {
  let pendingRequests = 0;
  const onRequest = (): void => {
    pendingRequests += 1;
  };
  const onFinished = (): void => {
    pendingRequests = Math.max(0, pendingRequests - 1);
  };

  page.on("request", onRequest);
  page.on("requestfinished", onFinished);
  page.on("requestfailed", onFinished);

  return {
    pending: () => pendingRequests,
  };
}

async function readQuietSnapshot(
  page: Page,
  thresholds: {
    domMutationQuietMs: number;
    animationQuietMs: number;
    taskQuietMs: number;
  },
): Promise<QuietSnapshot> {
  const pageState = await page.evaluate((input) => {
    const globalState = window as typeof window & {
      __invSettleState?: {
        lastMutationAt: number;
        lastAnimationAt: number;
        lastTaskSettledAt: number;
        pendingTasks: number;
      };
    };
    const state = globalState.__invSettleState;
    if (state === undefined) {
      return {
        domQuiet: true,
        animationQuiet: true,
        taskQuiet: true,
      };
    }

    const elapsed = performance.now();
    return {
      domQuiet: elapsed - state.lastMutationAt >= input.domMutationQuietMs,
      animationQuiet: elapsed - state.lastAnimationAt >= input.animationQuietMs,
      taskQuiet:
        state.pendingTasks === 0 && elapsed - state.lastTaskSettledAt >= input.taskQuietMs,
    };
  }, thresholds);

  return {
    networkIdle: true,
    domQuiet: pageState.domQuiet,
    animationQuiet: pageState.animationQuiet,
    taskQuiet: pageState.taskQuiet,
  };
}

async function waitForAppSignal(
  page: Page,
  selector: string,
  timeoutMs: number,
): Promise<{ timedOut: boolean; elapsedMs: number }> {
  const started = performance.now();
  try {
    await page.waitForSelector(selector, {
      state: "visible",
      timeout: timeoutMs,
    });
    return { timedOut: false, elapsedMs: performance.now() - started };
  } catch {
    return { timedOut: true, elapsedMs: performance.now() - started };
  }
}

async function waitForQuietWindows(
  page: Page,
  network: { pending: () => number },
  options: {
    timeoutMs: number;
    quietWindowMs: number;
    networkIdleMs: number;
    domMutationQuietMs: number;
    animationQuietMs: number;
    taskQuietMs: number;
  },
): Promise<{ timedOut: boolean; phases: Record<string, number> }> {
  const started = performance.now();
  const phases: Record<string, number> = {};
  let networkIdleAt: number | undefined;
  let quietSince: number | undefined;

  while (performance.now() - started < options.timeoutMs) {
    const elapsed = performance.now() - started;

    if (network.pending() === 0) {
      if (networkIdleAt === undefined) {
        networkIdleAt = elapsed;
        phases["network-idle"] = elapsed;
      }
    } else {
      networkIdleAt = undefined;
      quietSince = undefined;
    }

    if (networkIdleAt === undefined) {
      await page.waitForTimeout(10);
      continue;
    }

    if (elapsed - networkIdleAt < options.networkIdleMs) {
      await page.waitForTimeout(10);
      continue;
    }

    const snapshot = await readQuietSnapshot(page, {
      domMutationQuietMs: options.domMutationQuietMs,
      animationQuietMs: options.animationQuietMs,
      taskQuietMs: options.taskQuietMs,
    });

    if (snapshot.domQuiet && phases["dom-quiet"] === undefined) {
      phases["dom-quiet"] = elapsed;
    }
    if (snapshot.animationQuiet && phases["animation-quiet"] === undefined) {
      phases["animation-quiet"] = elapsed;
    }
    if (snapshot.taskQuiet && phases["task-quiet"] === undefined) {
      phases["task-quiet"] = elapsed;
    }

    const allQuiet =
      snapshot.domQuiet && snapshot.animationQuiet && snapshot.taskQuiet;
    if (!allQuiet) {
      quietSince = undefined;
      await page.waitForTimeout(10);
      continue;
    }

    if (quietSince === undefined) {
      quietSince = elapsed;
    }

    if (elapsed - quietSince >= options.quietWindowMs) {
      phases["quiet-windows"] = elapsed;
      phases.total = elapsed;
      return { timedOut: false, phases };
    }

    await page.waitForTimeout(10);
  }

  phases.total = performance.now() - started;
  return { timedOut: true, phases };
}

export async function awaitSettled(
  page: Page,
  opts: SettleOptions,
): Promise<SettledReport> {
  validateOptions(opts);

  const quietWindowMs = opts.quietWindowMs ?? DEFAULT_QUIET_WINDOW_MS;
  const networkIdleMs = opts.networkIdleMs ?? DEFAULT_NETWORK_IDLE_MS;
  const domMutationQuietMs = opts.domMutationQuietMs ?? DEFAULT_DOM_QUIET_MS;
  const animationQuietMs = opts.animationQuietMs ?? DEFAULT_ANIMATION_QUIET_MS;
  const taskQuietMs = opts.taskQuietMs ?? DEFAULT_TASK_QUIET_MS;

  if (opts.appSignalSelector !== undefined && opts.appSignalSelector.trim().length > 0) {
    const app = await waitForAppSignal(page, opts.appSignalSelector, opts.timeoutMs);
    return {
      signal: "app",
      timedOut: app.timedOut,
      phases: {
        app: app.elapsedMs,
        total: app.elapsedMs,
      },
    };
  }

  await installQuietObservers(page);
  const network = trackNetworkIdle(page);
  const quiet = await waitForQuietWindows(page, network, {
    timeoutMs: opts.timeoutMs,
    quietWindowMs,
    networkIdleMs,
    domMutationQuietMs,
    animationQuietMs,
    taskQuietMs,
  });

  return {
    signal: "quiet-windows",
    timedOut: quiet.timedOut,
    phases: quiet.phases,
  };
}
