import {
  FAILURE_CODES,
  FailureError,
  TypedEventEmitter,
  type DeviceInfo,
  type ServiceBlueprint,
  type ServiceEvents,
  type ServiceInstance,
} from "@argent/registry";
import type { CDPClient } from "../utils/debugger/cdp-client";
import {
  createChromiumServer,
  discoverPrimaryPage,
  ensureCdpReachable,
  type ChromiumServer,
  type MediaReady,
  type ScreencastFrame,
  type ScreencastOpts,
  type ScreencastSession,
  type ScreenshotOpts,
} from "../chromium-server";
import { parseChromiumCdpPort, resolveDevice } from "../utils/device-info";

export const CHROMIUM_CDP_NAMESPACE = "ChromiumCdp";

type ChromiumFactoryOptions = Record<string, unknown> & { device: DeviceInfo };

/**
 * Build the `ServiceRef` for a Chromium CDP session keyed by an already-resolved
 * `DeviceInfo`. Tool `services()` callbacks call this rather than hand-building
 * the URN string so the blueprint factory always receives the device through
 * the registry's `options` channel and never has to reclassify.
 */
export function chromiumCdpRef(device: DeviceInfo): {
  urn: string;
  options: ChromiumFactoryOptions;
} {
  return {
    urn: `${CHROMIUM_CDP_NAMESPACE}:${device.id}`,
    options: { device },
  };
}

// ── Legacy compatibility surface ─────────────────────────────────────────────
// The first cut of Chromium support exposed a thin `ChromiumCdpApi` directly
// off the blueprint. Existing tools (gesture-tap, screenshot, describe,
// keyboard, run-sequence, etc.) still consume that shape. The full ChromiumServer
// is now the source of truth, and these legacy types are kept so the blueprint
// can publish *both* the new abstraction (`server`) and the original ergonomic
// methods without forcing a callsite-by-callsite migration.

export interface MouseEventArgs {
  type: "mousePressed" | "mouseReleased" | "mouseMoved";
  /** CSS pixels relative to the page viewport. */
  x: number;
  y: number;
  button?: "none" | "left" | "middle" | "right";
  /** Required for press/release. */
  clickCount?: number;
}

export interface KeyEventArgs {
  type: "keyDown" | "keyUp" | "rawKeyDown" | "char";
  /** Browser-style key value, e.g. "a", "Enter", "ArrowLeft". */
  key?: string;
  code?: string;
  text?: string;
  /** DOM keyCode (deprecated but still consumed by many apps). */
  windowsVirtualKeyCode?: number;
  modifiers?: number;
}

export interface ViewportSize {
  width: number;
  height: number;
  /** Device pixel ratio reported by the renderer. */
  devicePixelRatio: number;
}

export interface ChromiumAxNode {
  nodeId: string;
  role?: string;
  name?: string;
  value?: string;
  description?: string;
  ignored?: boolean;
  backendDOMNodeId?: number;
  childIds?: string[];
  properties?: Array<{ name: string; value: { value?: unknown; type: string } }>;
}

export interface ChromiumCdpApi {
  /** CDP port the Chromium app exposed. */
  port: number;
  /** Underlying CDP client connected to the primary page target. */
  cdp: CDPClient;
  /** WebSocket URL to the page target (for diagnostics). */
  pageWebSocketUrl: string;
  /** Backend node id of the document (used as the root for AX queries). */
  rootDomNodeId: number | null;
  /** The full sim-server-equivalent abstraction layer. New callers should use this. */
  server: ChromiumServer;
  /** Re-read the page viewport so normalized → CSS pixel math stays accurate after window resizes. */
  refreshViewport(): Promise<ViewportSize>;
  /** Cached viewport from the most recent connect / refresh. */
  getViewport(): ViewportSize;
  dispatchMouseEvent(event: MouseEventArgs): Promise<void>;
  dispatchKeyEvent(event: KeyEventArgs): Promise<void>;
  /** Screenshot via CDP, persisted under tmpdir; returns file:// URL + absolute path.
   * Supports the sim-server-style options (rotation, scale, downscaler) when sharp is installed. */
  captureScreenshot(opts?: ScreenshotOpts): Promise<MediaReady>;
  /** Returns the accessibility tree rooted at the document. */
  getAxTree(): Promise<ChromiumAxNode[]>;
  /** Navigate the renderer to a URL. */
  navigate(url: string): Promise<void>;
  /** Evaluate JS in the renderer. Resolves to the serialized value when `returnByValue` is true. */
  evaluate(expression: string, options?: { returnByValue?: boolean }): Promise<unknown>;
  /** Start a screencast (one CDP session shared across all subscribers). */
  startScreencast(opts?: ScreencastOpts): Promise<ScreencastSession>;
  /** Last received screencast frame, or null. */
  getLastFrame(): ScreencastFrame | null;
}

// Re-exports for discovery callers that previously imported these straight from
// the blueprint module.
export { discoverPrimaryPage, ensureCdpReachable };

async function getDocumentNodeId(cdp: CDPClient): Promise<number | null> {
  try {
    const out = (await cdp.send("DOM.getDocument", { depth: 0 })) as {
      root?: { nodeId?: number; backendNodeId?: number };
    };
    return out.root?.nodeId ?? out.root?.backendNodeId ?? null;
  } catch {
    return null;
  }
}

export const chromiumCdpBlueprint: ServiceBlueprint<ChromiumCdpApi, DeviceInfo> = {
  namespace: CHROMIUM_CDP_NAMESPACE,
  getURN(device: DeviceInfo) {
    return `${CHROMIUM_CDP_NAMESPACE}:${device.id}`;
  },
  async factory(_deps, payload, options) {
    // Two routes into this factory:
    //   1) A tool's `services()` callback uses chromiumCdpRef(device) and we
    //      get options.device for free.
    //   2) Another blueprint declares `ChromiumCdp:<id>` as a transitive dep
    //      (registry resolves deps via URN strings only, no options channel
    //      — see Registry._resolve). In that case we synthesize DeviceInfo
    //      from the URN payload, which IS the device id.
    // Both paths must agree on the device id; if a caller passed an explicit
    // options.device whose id doesn't match the URN, that's a wiring bug
    // worth surfacing loudly.
    const opts = options as unknown as ChromiumFactoryOptions | undefined;
    const deviceFromOpts = opts?.device;
    const payloadStr = typeof payload === "string" ? payload : (payload as DeviceInfo)?.id;
    if (deviceFromOpts && payloadStr && deviceFromOpts.id !== payloadStr) {
      // Internal wiring invariant, not a telemetry-bearing failure: on every
      // registry path chromiumCdpRef(device) sets options.device and the URN
      // payload from the same device.id (so the ids agree), and the transitive-dep
      // path passes no options at all (so deviceFromOpts is undefined). A mismatch
      // can therefore only come from a hand-crafted direct factory() call — a
      // programmer error — so it stays a plain Error without a code.
      throw new Error(
        `${CHROMIUM_CDP_NAMESPACE}.factory: options.device.id "${deviceFromOpts.id}" disagrees with URN payload "${payloadStr}".`
      );
    }
    const device = deviceFromOpts ?? (payloadStr ? resolveDevice(payloadStr) : null);
    if (!device) {
      // Also dead on any registry path (resolveDevice never returns null and the
      // URN payload is always present); reachable only by a direct factory() call
      // with neither options.device nor a payload. Plain Error, no code.
      throw new Error(
        `${CHROMIUM_CDP_NAMESPACE}.factory could not determine the device — pass it via chromiumCdpRef(device).options or via the URN payload.`
      );
    }
    const port = parseChromiumCdpPort(device.id);
    if (port == null) {
      throw new FailureError(
        `${CHROMIUM_CDP_NAMESPACE}.factory got a malformed device id "${device.id}". ` +
          `Expected "chromium-cdp-<port>".`,
        {
          error_code: FAILURE_CODES.CHROMIUM_DEVICE_ID_INVALID,
          failure_stage: "chromium_factory_device_id",
          failure_area: "tool_server",
          error_kind: "validation",
        }
      );
    }

    const server = await createChromiumServer({ deviceId: device.id, port });
    const rootDomNodeId = await getDocumentNodeId(server.cdp);

    const events = new TypedEventEmitter<ServiceEvents>();
    server.events.on("terminated", (err) => {
      events.emit("terminated", err ?? new Error(`Chromium CDP on port ${port} disconnected`));
    });

    // Legacy adapter — translates the original `dispatchMouseEvent` and
    // `dispatchKeyEvent` calls into the new server's wire formats. Keeping
    // these one-liners means we don't have to rewrite every tool right now;
    // they can migrate to `api.server.send*` at their own pace.
    const api: ChromiumCdpApi = {
      port,
      cdp: server.cdp,
      pageWebSocketUrl: server.pageWebSocketUrl,
      rootDomNodeId,
      server,
      getViewport: () => server.getViewport(),
      refreshViewport: () => server.refreshViewport(),
      dispatchMouseEvent: async (event: MouseEventArgs) => {
        if (!Number.isFinite(event.x) || !Number.isFinite(event.y)) {
          throw new FailureError(
            `Chromium CDP: dispatchMouseEvent received non-finite coords x=${event.x}, y=${event.y}.`,
            {
              error_code: FAILURE_CODES.CHROMIUM_INPUT_INVALID,
              failure_stage: "chromium_dispatch_mouse_coords",
              failure_area: "tool_server",
              error_kind: "validation",
            }
          );
        }
        const button = event.button ?? (event.type === "mouseMoved" ? "none" : "left");
        const buttons = button === "none" ? 0 : 1;
        const payload: Record<string, unknown> = {
          type: event.type,
          x: event.x,
          y: event.y,
          button,
          buttons,
        };
        if (event.type !== "mouseMoved") {
          payload.clickCount = event.clickCount ?? 1;
        }
        await server.cdp.send("Input.dispatchMouseEvent", payload);
      },
      dispatchKeyEvent: async (event: KeyEventArgs) => {
        const payload: Record<string, unknown> = { type: event.type };
        if (event.key !== undefined) payload.key = event.key;
        if (event.code !== undefined) payload.code = event.code;
        if (event.text !== undefined) payload.text = event.text;
        if (event.windowsVirtualKeyCode !== undefined) {
          payload.windowsVirtualKeyCode = event.windowsVirtualKeyCode;
        }
        if (event.modifiers !== undefined) payload.modifiers = event.modifiers;
        await server.cdp.send("Input.dispatchKeyEvent", payload);
      },
      captureScreenshot: (opts2?: ScreenshotOpts) => server.captureScreenshot(opts2),
      getAxTree: async () => {
        const out = (await server.cdp.send("Accessibility.getFullAXTree", {})) as {
          nodes?: ChromiumAxNode[];
        };
        return out.nodes ?? [];
      },
      navigate: (url: string) => server.navigate(url),
      evaluate: (expression: string, opts2?: { returnByValue?: boolean }) =>
        server.evaluate(expression, opts2),
      startScreencast: (opts2?: ScreencastOpts) => server.startScreencast(opts2),
      getLastFrame: () => server.getLastFrame(),
    };

    const instance: ServiceInstance<ChromiumCdpApi> = {
      api,
      dispose: async () => {
        await server.dispose();
      },
      events,
    };
    return instance;
  },
};
