import type { BrowserContext, Locator, Page } from "playwright";
import { RpaError } from "@/modules/rpa/errors";

/** selectors 契约（与 SelectorGraph 对齐） */
export interface IRPASelectors {
  resolve(key: string): string;
  specs(envKey: string): string[];
  widget(page: Page): Locator;
}

/** exec/runtime 契约（与 ExecutionLayer 对齐） */
export interface IRPAExecution {
  evaluateOnLocator<T>(
    locator: Locator,
    script: string,
    arg?: unknown,
  ): Promise<T>;
  waitForBrowserFunction(
    script: string,
    arg: unknown,
    timeoutMs: number,
  ): Promise<void>;
}

/** state 最小契约（address-side 等使用） */
export interface IRPAState {
  markAddressSide(side: "pickup" | "delivery"): void;
}

/** Kernel 上下文契约（打破 StateEngine ↔ RPAContext 循环依赖） */
export interface IRPAContext {
  readonly page: Page;
  readonly browserContext?: BrowserContext;
  readonly selectors: IRPASelectors;
  readonly exec: IRPAExecution;
  readonly runtime: IRPAExecution;
  readonly state: IRPAState;
  getWidgetScrollAt(): number;
  setWidgetScrollAt(ms: number): void;
}

/** 数组字段统一兜底，禁止 undefined 流入业务 */
export function ensureArray<T>(value: T[] | null | undefined): T[] {
  return Array.isArray(value) ? value : [];
}

function isPlaywrightPage(value: unknown): value is Page {
  if (typeof value !== "object" || value === null) {
    return false;
  }
  const candidate = value as Page;
  return (
    typeof candidate.locator === "function" &&
    typeof candidate.waitForTimeout === "function" &&
    typeof candidate.url === "function"
  );
}

function hasContextContract(value: object): value is IRPAContext {
  const ctx = value as IRPAContext;
  return (
    "selectors" in ctx &&
    "exec" in ctx &&
    "runtime" in ctx &&
    "state" in ctx &&
    "page" in ctx &&
    typeof ctx.getWidgetScrollAt === "function" &&
    typeof ctx.setWidgetScrollAt === "function"
  );
}

/** duck-typing 识别 RPAContext（避免跨 bundle instanceof 失效） */
export function isRPAContext(value: unknown): value is IRPAContext {
  if (typeof value !== "object" || value === null) {
    return false;
  }
  if (!hasContextContract(value)) {
    return false;
  }
  return isPlaywrightPage((value as IRPAContext).page);
}

/** fail-fast：校验 context contract 完整 */
export function assertRPAContext(
  ctx: unknown,
  source = "RPAContext",
  options?: { allowMissingState?: boolean },
): asserts ctx is IRPAContext {
  if (typeof ctx !== "object" || ctx === null) {
    throw new RpaError(
      "STRUCT_CHANGE",
      `${source} 非法：非对象`,
      { retryable: false },
    );
  }

  const typed = ctx as IRPAContext;
  const hasCore =
    typed.selectors instanceof Object &&
    typed.exec instanceof Object &&
    typed.runtime instanceof Object &&
    isPlaywrightPage(typed.page) &&
    typeof typed.getWidgetScrollAt === "function" &&
    typeof typed.setWidgetScrollAt === "function";

  if (!hasCore) {
    throw new RpaError(
      "STRUCT_CHANGE",
      `${source} contract 断裂：缺少 selectors/runtime/state/page`,
      { retryable: false },
    );
  }

  if (!options?.allowMissingState && !(typed.state instanceof Object)) {
    throw new RpaError(
      "STRUCT_CHANGE",
      `${source}.state 未初始化`,
      { retryable: false },
    );
  }
}

export function assertPlaywrightPage(
  page: unknown,
  source = "page",
): asserts page is Page {
  if (!isPlaywrightPage(page)) {
    throw new RpaError(
      "STRUCT_CHANGE",
      `${source} 不是有效 Playwright Page`,
      { retryable: false },
    );
  }
}

export type RPAContextContractSnapshot = {
  hasPage: boolean;
  hasSelectors: boolean;
  hasExec: boolean;
  hasRuntime: boolean;
  hasState: boolean;
};

export function snapshotContextContract(
  ctx: unknown,
): RPAContextContractSnapshot {
  if (typeof ctx !== "object" || ctx === null) {
    return {
      hasPage: false,
      hasSelectors: false,
      hasExec: false,
      hasRuntime: false,
      hasState: false,
    };
  }
  const typed = ctx as Partial<IRPAContext>;
  return {
    hasPage: isPlaywrightPage(typed.page),
    hasSelectors: typed.selectors instanceof Object,
    hasExec: typed.exec instanceof Object,
    hasRuntime: typed.runtime instanceof Object,
    hasState: typed.state instanceof Object,
  };
}
