import type { IMatrixContext } from '@open-matrix/core';
import { RequestReply } from '@open-matrix/core';

export type MatrixAppReadinessLevel = 'shell' | 'operational' | 'cognitive';

export interface MatrixAppReadyActorProbe {
  readonly mount: string;
  readonly operation: string;
  readonly ok: boolean;
  readonly error?: string;
}

export interface MatrixAppReadyActorProbeRequest {
  readonly mount: string;
  readonly operation: string;
  readonly payload?: Record<string, unknown>;
  readonly targetRoot?: string;
  readonly timeoutMs?: number;
  readonly accept?: (result: unknown) => boolean;
  readonly invalidResultMessage?: string;
}

export interface MatrixAppReadyDetail {
  readonly appName: string;
  readonly root: string | null;
  readonly authorityRoot: string | null;
  readonly runtimeTargetRoot: string | null;
  readonly requestedTarget: string | null;
  readonly actualTarget: string | null;
  readonly transportConnected: boolean;
  readonly readinessLevel: MatrixAppReadinessLevel;
  readonly requiredActorsReachable: boolean;
  readonly actorProbes: readonly MatrixAppReadyActorProbe[];
  readonly errors: readonly string[];
}

export interface MatrixAppReadySignalOptions {
  readonly appName: string;
  readonly appElement: HTMLElement;
  readonly root: string | null;
  readonly authorityRoot: string | null;
  readonly runtimeTargetRoot: string | null;
  readonly requestedTarget: string | null;
  readonly actualTarget: string | null;
  readonly transportConnected: boolean;
  readonly readinessLevel: MatrixAppReadinessLevel;
  readonly requiredActorsReachable: boolean;
  readonly actorProbes?: readonly MatrixAppReadyActorProbe[];
  readonly errors: readonly string[];
}

export interface MatrixAppReadyMonitorOptions {
  readonly appName: string;
  readonly appSelector: string;
  readonly hostSelector?: string;
  readonly timeoutMs?: number;
  readonly pollIntervalMs?: number;
  readonly readiness: (
    appElement: HTMLElement,
    hostElement: HTMLElement | null
  ) => MatrixAppReadyDetail | Promise<MatrixAppReadyDetail | null> | null;
}

const DEFAULT_TIMEOUT_MS = 30_000;
const DEFAULT_POLL_INTERVAL_MS = 100;

export function markMatrixAppReady(options: MatrixAppReadySignalOptions): MatrixAppReadyDetail {
  const detail: MatrixAppReadyDetail = {
    appName: options.appName,
    root: options.root,
    authorityRoot: options.authorityRoot,
    runtimeTargetRoot: options.runtimeTargetRoot,
    requestedTarget: options.requestedTarget,
    actualTarget: options.actualTarget,
    transportConnected: options.transportConnected,
    readinessLevel: options.readinessLevel,
    requiredActorsReachable: options.requiredActorsReachable,
    actorProbes: options.actorProbes ?? [],
    errors: options.errors,
  };

  options.appElement.setAttribute('data-app-ready', options.appName);
  options.appElement.setAttribute('data-app-ready-level', options.readinessLevel);
  options.appElement.setAttribute('data-app-required-actors-reachable', String(options.requiredActorsReachable));
  const readyRoot = options.runtimeTargetRoot === null ? '' : options.runtimeTargetRoot;
  const requestedTarget = options.requestedTarget === null ? '' : options.requestedTarget;
  const actualTarget = options.actualTarget === null ? '' : options.actualTarget;
  options.appElement.setAttribute('data-app-ready-root', readyRoot);
  options.appElement.setAttribute('data-app-ready-requested-target', requestedTarget);
  options.appElement.setAttribute('data-app-ready-actual-target', actualTarget);
  document.documentElement.setAttribute('data-app-ready', options.appName);
  document.documentElement.setAttribute('data-app-ready-level', options.readinessLevel);
  document.documentElement.setAttribute('data-app-required-actors-reachable', String(options.requiredActorsReachable));
  document.documentElement.setAttribute('data-app-ready-root', readyRoot);
  document.documentElement.setAttribute('data-app-ready-requested-target', requestedTarget);
  document.documentElement.setAttribute('data-app-ready-actual-target', actualTarget);
  window.dispatchEvent(new CustomEvent<MatrixAppReadyDetail>('matrix-app-ready', { detail }));
  return detail;
}

export function monitorMatrixAppReady(options: MatrixAppReadyMonitorOptions): void {
  if (typeof window === 'undefined' || typeof document === 'undefined') {
    return;
  }
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
  const startedAt = performance.now();
  let completed = false;
  let pending = false;
  let timer: number | null = null;

  const clearTimer = () => {
    if (timer !== null) {
      window.clearTimeout(timer);
      timer = null;
    }
  };

  const attempt = () => {
    if (completed || pending) {
      return;
    }
    const appElement = document.querySelector(options.appSelector);
    let hostElement: HTMLElement | null = null;
    if (options.hostSelector) {
      const selectedHost = document.querySelector(options.hostSelector);
      if (!(selectedHost instanceof HTMLElement)) {
        scheduleNextAttempt(startedAt, timeoutMs, pollIntervalMs, attempt, (nextTimer) => {
          timer = nextTimer;
        }, clearTimer);
        return;
      }
      hostElement = selectedHost;
    }

    if (appElement instanceof HTMLElement) {
      const readiness = options.readiness(appElement, hostElement);
      if (isPromiseLike(readiness)) {
        pending = true;
        readiness
          .then((resolved) => {
            pending = false;
            if (resolved) {
              completeReadiness(options, appElement, resolved, clearTimer);
              completed = true;
              return;
            }
            scheduleNextAttempt(startedAt, timeoutMs, pollIntervalMs, attempt, (nextTimer) => {
              timer = nextTimer;
            }, clearTimer);
          })
          .catch(() => {
            pending = false;
            scheduleNextAttempt(startedAt, timeoutMs, pollIntervalMs, attempt, (nextTimer) => {
              timer = nextTimer;
            }, clearTimer);
          });
        return;
      }
      if (readiness) {
        completed = true;
        completeReadiness(options, appElement, readiness, clearTimer);
        return;
      }
    }
    scheduleNextAttempt(startedAt, timeoutMs, pollIntervalMs, attempt, (nextTimer) => {
      timer = nextTimer;
    }, clearTimer);
  };

  window.addEventListener('beforeunload', clearTimer, { once: true });
  attempt();
}

export async function probeMatrixAppReadyActors(
  context: IMatrixContext,
  probes: readonly MatrixAppReadyActorProbeRequest[],
): Promise<readonly MatrixAppReadyActorProbe[]> {
  const results: MatrixAppReadyActorProbe[] = [];
  for (const probe of probes) {
    try {
      const result = await RequestReply.execute<Record<string, unknown>, unknown>(
        context,
        probe.mount,
        probe.operation,
        probe.payload ?? {},
        {
          timeoutMs: probe.timeoutMs ?? 5000,
          ...(probe.targetRoot ? { targetRoot: probe.targetRoot } : {}),
        },
      );
      if (probe.accept && !probe.accept(result)) {
        results.push({
          mount: probe.mount,
          operation: probe.operation,
          ok: false,
          error: probe.invalidResultMessage ?? 'Actor returned an invalid readiness response',
        });
        continue;
      }
      results.push({
        mount: probe.mount,
        operation: probe.operation,
        ok: true,
      });
    } catch (error) {
      results.push({
        mount: probe.mount,
        operation: probe.operation,
        ok: false,
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }
  return results;
}

export function matrixAppReadyProbeErrors(
  probes: readonly MatrixAppReadyActorProbe[],
): readonly string[] {
  return probes
    .filter((probe) => !probe.ok)
    .map((probe) => `${probe.mount} ${probe.operation}: ${probe.error ?? 'not reachable'}`);
}

export function matrixAppReadyProbesOk(
  probes: readonly MatrixAppReadyActorProbe[],
): boolean {
  return probes.length > 0 && probes.every((probe) => probe.ok);
}

function completeReadiness(
  options: MatrixAppReadyMonitorOptions,
  appElement: HTMLElement,
  readiness: MatrixAppReadyDetail,
  clearTimer: () => void,
): void {
  clearTimer();
  markMatrixAppReady({
    appName: options.appName,
    appElement,
    root: readiness.root,
    authorityRoot: readiness.authorityRoot,
    runtimeTargetRoot: readiness.runtimeTargetRoot,
    requestedTarget: readiness.requestedTarget,
    actualTarget: readiness.actualTarget,
    transportConnected: readiness.transportConnected,
    readinessLevel: readiness.readinessLevel,
    requiredActorsReachable: readiness.requiredActorsReachable,
    actorProbes: readiness.actorProbes,
    errors: readiness.errors,
  });
}

function isPromiseLike<T>(value: T | Promise<T>): value is Promise<T> {
  return Boolean(
    value
    && typeof value === 'object'
    && 'then' in value
    && typeof (value as { then?: unknown }).then === 'function'
  );
}

function scheduleNextAttempt(
  startedAt: number,
  timeoutMs: number,
  pollIntervalMs: number,
  attempt: () => void,
  setTimer: (timer: number) => void,
  clearTimer: () => void,
): void {
  if (performance.now() - startedAt >= timeoutMs) {
    clearTimer();
    return;
  }
  setTimer(window.setTimeout(attempt, pollIntervalMs));
}
