/**
 * ViewBoundary - Abstraction over Electron's WebContentsView.
 *
 * Provides an injectable interface for view management, enabling:
 * - Unit testing with behavioral mocks
 * - Boundary testing against real WebContentsView
 * - Handle-based access pattern (no direct Electron types exposed)
 */

import type { ViewHandle, Rectangle, WebPreferences, WindowHandle } from "./types";
import { createViewHandle } from "./types";
import { ShellError } from "../../shared/errors/shell-errors";
import type { Logger } from "../platform/logging";
import type { WindowBoundary } from "./window";

// ============================================================================
// Types
// ============================================================================

/**
 * Options for creating a view.
 */
export interface ViewOptions {
  readonly webPreferences?: WebPreferences;
  readonly backgroundColor?: string;
  /** Debug label for logging (e.g., "ui", workspace name). */
  readonly label?: string;
}

/**
 * Details about a window.open() request.
 */
export interface WindowOpenDetails {
  readonly url: string;
  readonly frameName: string;
  readonly disposition:
    | "default"
    | "foreground-tab"
    | "background-tab"
    | "new-window"
    | "save-to-disk"
    | "other";
}

/**
 * Action to take for window.open() requests.
 */
export type WindowOpenAction = { action: "allow" } | { action: "deny" };

/**
 * Handler for window.open() requests.
 */
export type WindowOpenHandler = (details: WindowOpenDetails) => WindowOpenAction;

/**
 * Function to unsubscribe from an event.
 */
export type Unsubscribe = () => void;

/**
 * Details about a failed load event.
 */
export interface FailLoadDetails {
  readonly errorCode: number;
  readonly errorDescription: string;
  readonly isMainFrame: boolean;
  /**
   * The URL whose load failed. For subframe failures, the iframe's `src`.
   * Used by shared-surface backends (iframe-view-manager) to map a
   * subframe failure back to a workspace by URL.
   */
  readonly validatedURL: string;
}

/**
 * Details about a render-process-gone event.
 */
export interface RenderProcessGoneDetails {
  /** Reason as reported by Electron (e.g. "crashed", "killed", "oom"). */
  readonly reason: string;
  /** Process exit code, if known. */
  readonly exitCode: number;
}

/**
 * Keyboard input descriptor (no Electron types).
 */
export interface KeyboardInput {
  readonly type: "keyDown" | "keyUp";
  readonly key: string;
  readonly isAutoRepeat: boolean;
  readonly control: boolean;
  readonly shift: boolean;
  readonly alt: boolean;
  readonly meta: boolean;
}

// ============================================================================
// Interface
// ============================================================================

/**
 * Abstraction over Electron's WebContentsView.
 *
 * Uses opaque ViewHandle references instead of exposing WebContentsView directly.
 * This allows testing without Electron dependencies and ensures all view
 * access goes through this abstraction.
 */
export interface ViewBoundary {
  // Lifecycle
  /**
   * Create a new view.
   *
   * @param options - View creation options
   * @returns Handle to the created view
   */
  createView(options: ViewOptions): ViewHandle;

  /**
   * Destroy a view.
   *
   * @param handle - Handle to the view
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  destroy(handle: ViewHandle): void;

  /**
   * Destroy all views.
   */
  destroyAll(): void;

  // Navigation
  /**
   * Load a URL in the view.
   *
   * @param handle - Handle to the view
   * @param url - URL to load
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  loadURL(handle: ViewHandle, url: string): Promise<void>;

  /**
   * Get the current URL of the view.
   *
   * @param handle - Handle to the view
   * @returns The current URL
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  getURL(handle: ViewHandle): string;

  /**
   * Reload the view's current page in the renderer process.
   *
   * Used to force a fresh paint when a detached view becomes visible after
   * a long idle period (e.g. across system suspend/resume), where the
   * existing renderer can end up unfocusable and showing nothing.
   *
   * @param handle - Handle to the view
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  reload(handle: ViewHandle): void;

  // Layout
  /**
   * Set the bounds of the view.
   *
   * @param handle - Handle to the view
   * @param bounds - The new bounds
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  setBounds(handle: ViewHandle, bounds: Rectangle): void;

  /**
   * Get the bounds of the view.
   *
   * @param handle - Handle to the view
   * @returns The current bounds
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  getBounds(handle: ViewHandle): Rectangle;

  /**
   * Set the background color of the view.
   *
   * @param handle - Handle to the view
   * @param color - CSS color string
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  setBackgroundColor(handle: ViewHandle, color: string): void;

  // Focus
  /**
   * Focus the view.
   *
   * @param handle - Handle to the view
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  focus(handle: ViewHandle): void;

  // Window attachment
  /**
   * Attach the view to a window's content view.
   *
   * @param handle - Handle to the view
   * @param windowHandle - Handle to the window
   * @param index - Optional z-order index (0 = bottom, omit = top)
   * @throws ShellError with code VIEW_NOT_FOUND if view handle is invalid
   */
  attachToWindow(
    handle: ViewHandle,
    windowHandle: WindowHandle,
    index?: number,
    options?: { force?: boolean }
  ): void;

  /**
   * Detach the view from its window's content view.
   *
   * @param handle - Handle to the view
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  detachFromWindow(handle: ViewHandle): void;

  // Events
  /**
   * Subscribe to did-finish-load events.
   *
   * @param handle - Handle to the view
   * @param callback - Called when page finishes loading
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onDidFinishLoad(handle: ViewHandle, callback: () => void): Unsubscribe;

  /**
   * Subscribe to did-fail-load events.
   *
   * @param handle - Handle to the view
   * @param callback - Called with failure details when page fails to load
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onDidFailLoad(handle: ViewHandle, callback: (details: FailLoadDetails) => void): Unsubscribe;

  /**
   * Subscribe to dom-ready events.
   *
   * @param handle - Handle to the view
   * @param callback - Called when DOM is ready
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onDomReady(handle: ViewHandle, callback: () => void): Unsubscribe;

  /**
   * Subscribe to will-navigate events.
   *
   * @param handle - Handle to the view
   * @param callback - Called with the URL when navigation is about to occur.
   *                   Return true to allow navigation, false to prevent it.
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onWillNavigate(handle: ViewHandle, callback: (url: string) => boolean): Unsubscribe;

  /**
   * Set a handler for window.open() requests.
   *
   * @param handle - Handle to the view
   * @param handler - Handler function, or null to use default behavior
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  setWindowOpenHandler(handle: ViewHandle, handler: WindowOpenHandler | null): void;

  // Execution
  /**
   * Execute JavaScript in the view's renderer process.
   *
   * @param handle - Handle to the view
   * @param code - JavaScript code to execute
   * @returns Promise resolving with the result of the executed code
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  executeJavaScript(handle: ViewHandle, code: string): Promise<unknown>;

  /**
   * Capture a PNG screenshot of the view's current visual content.
   *
   * Best-effort: returns null if the view has no rendered content, is detached,
   * or capture fails for any reason. Does not throw.
   *
   * @param handle - Handle to the view
   * @returns PNG-encoded bytes, or null on failure / no content
   */
  capturePNG(handle: ViewHandle): Promise<Buffer | null>;

  // IPC
  /**
   * Send a message to the view's renderer process.
   *
   * @param handle - Handle to the view
   * @param channel - IPC channel name
   * @param args - Arguments to send
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  send(handle: ViewHandle, channel: string, ...args: unknown[]): void;

  // Events (continued)
  /**
   * Subscribe to before-input-event for keyboard interception.
   *
   * @param handle - Handle to the view
   * @param callback - Called with keyboard input and a preventDefault function
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onBeforeInputEvent(
    handle: ViewHandle,
    callback: (input: KeyboardInput, preventDefault: () => void) => void
  ): Unsubscribe;

  /**
   * Subscribe to view destruction.
   *
   * @param handle - Handle to the view
   * @param callback - Called when the view is destroyed
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onDestroyed(handle: ViewHandle, callback: () => void): Unsubscribe;

  /**
   * Subscribe to render-process-gone events (renderer crash / killed / OOM).
   *
   * @param handle - Handle to the view
   * @param callback - Called with the reason and exit code
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onRenderProcessGone(
    handle: ViewHandle,
    callback: (details: RenderProcessGoneDetails) => void
  ): Unsubscribe;

  /**
   * Subscribe to renderer unresponsive events (event loop hang).
   *
   * @param handle - Handle to the view
   * @param callback - Called when the renderer stops responding
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onUnresponsive(handle: ViewHandle, callback: () => void): Unsubscribe;

  /**
   * Subscribe to renderer responsive events (recovered from a hang).
   *
   * @param handle - Handle to the view
   * @param callback - Called when the renderer becomes responsive again
   * @returns Unsubscribe function
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  onResponsive(handle: ViewHandle, callback: () => void): Unsubscribe;

  /**
   * Check if a view handle refers to a valid, non-destroyed view.
   *
   * @param handle - Handle to the view
   * @returns True if the view exists and is not destroyed
   */
  isAvailable(handle: ViewHandle): boolean;

  // DevTools
  /**
   * Open DevTools for a view.
   *
   * @param handle - Handle to the view
   * @param options - DevTools options
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  openDevTools(handle: ViewHandle, options?: { mode?: string }): void;

  /**
   * Close DevTools for a view.
   *
   * @param handle - Handle to the view
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  closeDevTools(handle: ViewHandle): void;

  /**
   * Check if DevTools are open for a view.
   *
   * @param handle - Handle to the view
   * @returns True if DevTools are open
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  isDevToolsOpened(handle: ViewHandle): boolean;

  /**
   * Install a script in every child frame (non-top) that finishes loading
   * inside this view. The script runs same-origin inside each child frame.
   *
   * Used to inject in-frame helpers that can't be installed from the host
   * document due to cross-origin restrictions (e.g., focus trackers that
   * need to observe and call focus on elements inside the iframe).
   *
   * The subscription is established immediately and applies to every future
   * frame load — call once during view setup.
   *
   * @param handle - Handle to the host view
   * @param script - JavaScript source to execute in each child frame
   * @throws ShellError with code VIEW_NOT_FOUND if handle is invalid
   */
  installChildFrameScript(handle: ViewHandle, script: string): void;

  // Cleanup
  /**
   * Dispose of all resources.
   */
  dispose(): Promise<void>;
}

// ============================================================================
// Default Implementation
// ============================================================================

import { WebContentsView } from "electron";

interface ViewState {
  view: WebContentsView;
  attachedToWindow: WindowHandle | null;
  options: ViewOptions;
  label: string;
}

/**
 * Default implementation of ViewBoundary using Electron's WebContentsView.
 */
export class DefaultViewBoundary implements ViewBoundary {
  private readonly views = new Map<string, ViewState>();
  private nextId = 1;

  constructor(
    private readonly windowLayer: WindowBoundary,
    private readonly logger: Logger
  ) {}

  createView(options: ViewOptions): ViewHandle {
    const id = `view-${this.nextId++}`;

    // Build webPreferences, only including defined properties
    const webPreferences: Electron.WebPreferences = {
      nodeIntegration: options.webPreferences?.nodeIntegration ?? false,
      contextIsolation: options.webPreferences?.contextIsolation ?? true,
      sandbox: options.webPreferences?.sandbox ?? true,
      focusOnNavigation: options.webPreferences?.focusOnNavigation ?? true,
    };
    if (options.webPreferences?.partition !== undefined) {
      webPreferences.partition = options.webPreferences.partition;
    }
    if (options.webPreferences?.preload !== undefined) {
      webPreferences.preload = options.webPreferences.preload;
    }
    if (options.webPreferences?.webviewTag !== undefined) {
      webPreferences.webviewTag = options.webPreferences.webviewTag;
    }
    if (options.webPreferences?.backgroundThrottling !== undefined) {
      webPreferences.backgroundThrottling = options.webPreferences.backgroundThrottling;
    }

    const view = new WebContentsView({ webPreferences });

    if (options.backgroundColor) {
      view.setBackgroundColor(options.backgroundColor);
    }

    const label = options.label ?? id;

    this.views.set(id, {
      view,
      attachedToWindow: null,
      options,
      label,
    });

    view.webContents.on("blur", () => {
      this.logger.debug(`blur ${label}`);
    });

    view.webContents.on("focus", () => {
      this.logger.debug(`focus ${label}`);
    });

    const handle = createViewHandle(id);
    this.logger.debug("View created", {
      id,
      handleId: handle.id,
      viewsCount: this.views.size,
      isDestroyed: view.webContents.isDestroyed(),
    });
    return handle;
  }

  destroy(handle: ViewHandle): void {
    const state = this.getView(handle);

    // Detach from window if attached
    if (state.attachedToWindow) {
      this.detachFromWindow(handle);
    }

    this.views.delete(handle.id);

    const wc = state.view.webContents;
    if (wc && !wc.isDestroyed()) {
      wc.close();
    }

    this.logger.debug("View destroyed", { id: handle.id });
  }

  destroyAll(): void {
    for (const [id] of this.views) {
      this.destroy(createViewHandle(id));
    }
  }

  async loadURL(handle: ViewHandle, url: string): Promise<void> {
    const state = this.getView(handle);
    try {
      await state.view.webContents.loadURL(url);
    } catch (error) {
      // Navigation failures are transient (network changes, sleep/resume, etc.)
      // and already handled by the did-fail-load event which triggers retry with backoff.
      // Swallow the rejection to prevent unhandled promise rejections in fire-and-forget callers.
      const message = error instanceof Error ? error.message : String(error);
      this.logger.debug("Navigation failed (handled via did-fail-load)", {
        id: handle.id,
        url,
        error: message,
      });
    }
  }

  getURL(handle: ViewHandle): string {
    const state = this.getView(handle);
    return state.view.webContents.getURL();
  }

  reload(handle: ViewHandle): void {
    const state = this.getView(handle);
    const wc = state.view.webContents;
    if (!wc || wc.isDestroyed()) return;
    wc.reload();
  }

  async capturePNG(handle: ViewHandle): Promise<Buffer | null> {
    try {
      const state = this.getView(handle);
      const wc = state.view.webContents;
      if (!wc || wc.isDestroyed()) return null;
      const image = await wc.capturePage();
      if (image.isEmpty()) return null;
      return image.toPNG();
    } catch (error) {
      this.logger.debug("capturePNG failed", {
        id: handle.id,
        error: error instanceof Error ? error.message : String(error),
      });
      return null;
    }
  }

  setBounds(handle: ViewHandle, bounds: Rectangle): void {
    const state = this.getView(handle);
    state.view.setBounds(bounds);
  }

  getBounds(handle: ViewHandle): Rectangle {
    const state = this.getView(handle);
    return state.view.getBounds();
  }

  setBackgroundColor(handle: ViewHandle, color: string): void {
    const state = this.getView(handle);
    state.view.setBackgroundColor(color);
  }

  focus(handle: ViewHandle): void {
    const state = this.getView(handle);
    state.view.webContents.focus();
  }

  attachToWindow(
    handle: ViewHandle,
    windowHandle: WindowHandle,
    index?: number,
    options?: { force?: boolean }
  ): void {
    const state = this.getView(handle);

    // Get the content view from the window layer
    const contentView = this.windowLayer.getContentView(windowHandle);
    const children = contentView.children;
    const currentIndex = children.indexOf(state.view);
    const isAttached = currentIndex !== -1;

    this.logger.debug("attachToWindow called", {
      viewId: handle.id,
      windowId: windowHandle.id,
      requestedIndex: index ?? "top",
      currentIndex,
      isAttached,
      childCount: children.length,
    });

    // Check if already at the correct position (no-op to preserve focus)
    if (isAttached && !options?.force) {
      // For "top" position (no index), check if already at end
      if (index === undefined && currentIndex === children.length - 1) {
        this.logger.debug("View already at top, skipping attach", { viewId: handle.id });
        return; // Already at top
      }
      // For explicit index, check if already there
      if (index !== undefined && currentIndex === index) {
        this.logger.debug("View already at correct index, skipping attach", {
          viewId: handle.id,
          index,
        });
        return; // Already at correct index
      }
    }
    if (isAttached) {
      // Need to move (or force re-composite) - remove first
      this.logger.debug("View needs to move, removing first", {
        viewId: handle.id,
        fromIndex: currentIndex,
        toIndex: index ?? "top",
      });
      contentView.removeChildView(state.view);
    }

    // Add at specified index or append to top
    if (index !== undefined) {
      contentView.addChildView(state.view, index);
    } else {
      contentView.addChildView(state.view);
    }

    // Track attachment in both view state and window layer
    state.attachedToWindow = windowHandle;
    this.windowLayer.trackAttachedView(windowHandle, handle);

    this.logger.debug("View attached to window", {
      viewId: handle.id,
      windowId: windowHandle.id,
      index: index ?? "top",
      newChildCount: contentView.children.length,
    });
  }

  detachFromWindow(handle: ViewHandle): void {
    const state = this.getView(handle);

    // If not attached, no-op
    if (!state.attachedToWindow) {
      return;
    }

    const windowHandle = state.attachedToWindow;

    try {
      const contentView = this.windowLayer.getContentView(windowHandle);
      contentView.removeChildView(state.view);
    } catch {
      // Window may have been destroyed - ignore
    }

    // Untrack from window layer
    try {
      this.windowLayer.untrackAttachedView(windowHandle, handle);
    } catch {
      // Window may not exist anymore - ignore
    }

    state.attachedToWindow = null;

    this.logger.debug("View detached from window", {
      viewId: handle.id,
      windowId: windowHandle.id,
    });
  }

  onDidFinishLoad(handle: ViewHandle, callback: () => void): Unsubscribe {
    const state = this.getView(handle);
    state.view.webContents.on("did-finish-load", callback);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("did-finish-load", callback);
      }
    };
  }

  onDidFailLoad(handle: ViewHandle, callback: (details: FailLoadDetails) => void): Unsubscribe {
    const state = this.getView(handle);
    const handler = (
      _event: Electron.Event,
      errorCode: number,
      errorDescription: string,
      validatedURL: string,
      isMainFrame: boolean
    ) => {
      callback({ errorCode, errorDescription, isMainFrame, validatedURL });
    };
    state.view.webContents.on("did-fail-load", handler);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("did-fail-load", handler);
      }
    };
  }

  onDomReady(handle: ViewHandle, callback: () => void): Unsubscribe {
    const state = this.getView(handle);
    state.view.webContents.on("dom-ready", callback);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("dom-ready", callback);
      }
    };
  }

  onWillNavigate(handle: ViewHandle, callback: (url: string) => boolean): Unsubscribe {
    const state = this.getView(handle);
    const handler = (event: Electron.Event, url: string) => {
      const allow = callback(url);
      if (!allow) {
        event.preventDefault();
      }
    };
    state.view.webContents.on("will-navigate", handler);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("will-navigate", handler);
      }
    };
  }

  setWindowOpenHandler(handle: ViewHandle, handler: WindowOpenHandler | null): void {
    const state = this.getView(handle);

    if (handler === null) {
      state.view.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
    } else {
      state.view.webContents.setWindowOpenHandler((details) => {
        return handler({
          url: details.url,
          frameName: details.frameName,
          disposition: details.disposition as WindowOpenDetails["disposition"],
        });
      });
    }
  }

  executeJavaScript(handle: ViewHandle, code: string): Promise<unknown> {
    const state = this.getView(handle);
    // userGesture=true so scripted cross-origin iframe focus
    // (iframe.contentWindow.focus()) isn't silently blocked by Chromium.
    return state.view.webContents.executeJavaScript(code, true);
  }

  send(handle: ViewHandle, channel: string, ...args: unknown[]): void {
    const state = this.getView(handle);
    const wc = state.view.webContents;
    if (wc && !wc.isDestroyed()) {
      wc.send(channel, ...args);
    }
  }

  onBeforeInputEvent(
    handle: ViewHandle,
    callback: (input: KeyboardInput, preventDefault: () => void) => void
  ): Unsubscribe {
    const state = this.getView(handle);
    const handler = (event: Electron.Event, electronInput: Electron.Input) => {
      const input: KeyboardInput = {
        type: electronInput.type as "keyDown" | "keyUp",
        key: electronInput.key,
        isAutoRepeat: electronInput.isAutoRepeat,
        control: electronInput.control,
        shift: electronInput.shift,
        alt: electronInput.alt,
        meta: electronInput.meta,
      };
      callback(input, () => event.preventDefault());
    };
    state.view.webContents.on("before-input-event", handler);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("before-input-event", handler);
      }
    };
  }

  onDestroyed(handle: ViewHandle, callback: () => void): Unsubscribe {
    const state = this.getView(handle);
    state.view.webContents.on("destroyed", callback);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("destroyed", callback);
      }
    };
  }

  onRenderProcessGone(
    handle: ViewHandle,
    callback: (details: RenderProcessGoneDetails) => void
  ): Unsubscribe {
    const state = this.getView(handle);
    const handler = (_event: Electron.Event, details: Electron.RenderProcessGoneDetails) => {
      callback({ reason: details.reason, exitCode: details.exitCode });
    };
    state.view.webContents.on("render-process-gone", handler);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("render-process-gone", handler);
      }
    };
  }

  onUnresponsive(handle: ViewHandle, callback: () => void): Unsubscribe {
    const state = this.getView(handle);
    state.view.webContents.on("unresponsive", callback);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("unresponsive", callback);
      }
    };
  }

  onResponsive(handle: ViewHandle, callback: () => void): Unsubscribe {
    const state = this.getView(handle);
    state.view.webContents.on("responsive", callback);
    return () => {
      const wc = state.view.webContents;
      if (wc && !wc.isDestroyed()) {
        wc.off("responsive", callback);
      }
    };
  }

  isAvailable(handle: ViewHandle): boolean {
    const state = this.views.get(handle.id);
    if (!state) return false;
    const wc = state.view.webContents;
    return !!wc && !wc.isDestroyed();
  }

  openDevTools(handle: ViewHandle, options?: { mode?: string }): void {
    const state = this.getView(handle);
    state.view.webContents.openDevTools(options as Electron.OpenDevToolsOptions);
  }

  closeDevTools(handle: ViewHandle): void {
    const state = this.getView(handle);
    state.view.webContents.closeDevTools();
  }

  isDevToolsOpened(handle: ViewHandle): boolean {
    const state = this.getView(handle);
    return state.view.webContents.isDevToolsOpened();
  }

  async dispose(): Promise<void> {
    this.destroyAll();
  }

  installChildFrameScript(handle: ViewHandle, script: string): void {
    const state = this.getView(handle);
    const wc = state.view.webContents;
    wc.on("did-frame-finish-load", (_event, isMainFrame, frameProcessId, frameRoutingId) => {
      if (isMainFrame) return;
      const frame = wc.mainFrame.framesInSubtree.find(
        (f) => f.processId === frameProcessId && f.routingId === frameRoutingId
      );
      if (!frame) return;
      void frame.executeJavaScript(script);
    });
  }

  private getView(handle: ViewHandle): ViewState {
    const state = this.views.get(handle.id);
    if (!state) {
      throw new ShellError("VIEW_NOT_FOUND", `View ${handle.id} not found`, handle.id);
    }
    const wc = state.view.webContents;
    if (!wc || wc.isDestroyed()) {
      this.views.delete(handle.id);
      throw new ShellError("VIEW_DESTROYED", `View ${handle.id} was destroyed`, handle.id);
    }
    return state;
  }
}
