/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import type {ParsedArguments} from '../bin/chrome-devtools-mcp-cli-options.js';
import type {AggregatedInfoWithUid} from '../HeapSnapshotManager.js';
import type {McpPage} from '../McpPage.js';
import {zod} from '../third_party/index.js';
import type {
  Dialog,
  ElementHandle,
  Extension,
  Page,
  ScreenRecorder,
  Viewport,
  DevTools,
} from '../third_party/index.js';
import type {InsightName, TraceResult} from '../trace-processing/parse.js';
import type {
  TextSnapshotNode,
  GeolocationOptions,
  ExtensionServiceWorker,
} from '../types.js';
import type {PaginationOptions} from '../utils/types.js';

import type {ToolCategory} from './categories.js';
import type {
  ToolGroup,
  ToolDefinition as ThirdPartyDeveloperToolDefinition,
} from './thirdPartyDeveloper.js';

export interface BaseToolDefinition<
  Schema extends zod.ZodRawShape = zod.ZodRawShape,
> {
  name: string;
  description: string;
  annotations: {
    title?: string;
    category: ToolCategory;
    /**
     * If true, the tool does not modify its environment.
     */
    readOnlyHint: boolean;
    conditions?: string[];
  };
  schema: Schema;
  blockedByDialog: boolean;
}

export interface ToolDefinition<
  Schema extends zod.ZodRawShape = zod.ZodRawShape,
> extends BaseToolDefinition<Schema> {
  schema: Schema;
  handler: (
    request: Request<Schema>,
    response: Response,
    context: Context,
  ) => Promise<void>;
}

export interface Request<Schema extends zod.ZodRawShape> {
  params: zod.objectOutputType<Schema, zod.ZodTypeAny>;
}

export interface ImageContentData {
  data: string;
  mimeType: string;
}

export interface SnapshotParams {
  verbose?: boolean;
  filePath?: string;
}

export interface LighthouseData {
  summary: {
    mode: string;
    device: string;
    url?: string;
    scores: Array<{
      id: string;
      title: string;
      score: number | null;
    }>;
    audits: {
      failed: number;
      passed: number;
    };
    timing: {
      total: number;
    };
  };
  reports: string[];
}

export interface DevToolsData {
  cdpRequestId?: string;
  cdpBackendNodeId?: number;
}

export interface Response {
  appendResponseLine(value: string): void;
  setHeapSnapshotAggregates(
    aggregates: Record<
      string,
      DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo
    >,
    options?: PaginationOptions,
  ): void;
  setHeapSnapshotStats(
    stats: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics,
    staticData: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null,
  ): void;
  setHeapSnapshotNodes(
    nodes: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange,
    options?: PaginationOptions,
  ): void;
  setIncludePages(value: boolean): void;
  setIncludeNetworkRequests(
    value: boolean,
    options?: PaginationOptions & {
      resourceTypes?: string[];
      includePreservedRequests?: boolean;
      networkRequestIdInDevToolsUI?: number;
    },
  ): void;
  setIncludeConsoleData(
    value: boolean,
    options?: PaginationOptions & {
      types?: string[];
      includePreservedMessages?: boolean;
    },
  ): void;
  includeSnapshot(params?: SnapshotParams): void;
  attachImage(value: ImageContentData): void;
  attachNetworkRequest(
    reqId: number,
    options?: {requestFilePath?: string; responseFilePath?: string},
  ): void;
  attachConsoleMessage(msgid: number): void;
  // Allows re-using DevTools data queried by some tools.
  attachDevToolsData(data: DevToolsData): void;
  setTabId(tabId: string): void;
  attachTraceSummary(trace: TraceResult): void;
  attachTraceInsight(
    trace: TraceResult,
    insightSetId: string,
    insightName: InsightName,
  ): void;
  setListExtensions(): void;
  attachLighthouseResult(result: LighthouseData): void;
  setListThirdPartyDeveloperTools(): void;
  setListWebMcpTools(): void;
}

export type SupportedExtensions =
  | '.png'
  | '.jpeg'
  | '.webp'
  | '.json'
  | '.network-response'
  | '.network-request'
  | '.html'
  | '.txt'
  | '.csv'
  | '.json.gz';

/**
 * Only add methods used by tools/*.
 */
export type Context = Readonly<{
  validatePath(filePath?: string): void;
  isRunningPerformanceTrace(): boolean;
  setIsRunningPerformanceTrace(x: boolean): void;
  isCruxEnabled(): boolean;
  recordedTraces(): TraceResult[];
  storeTraceRecording(result: TraceResult): void;
  getPageById(pageId: number): ContextPage;
  newPage(
    background?: boolean,
    isolatedContextName?: string,
  ): Promise<ContextPage>;
  closePage(pageId: number): Promise<void>;
  selectPage(page: ContextPage): void;
  restoreEmulation(page: ContextPage): Promise<void>;
  emulate(
    options: {
      networkConditions?: string;
      cpuThrottlingRate?: number;
      geolocation?: GeolocationOptions;
      userAgent?: string;
      colorScheme?: 'dark' | 'light' | 'auto';
      viewport?: Viewport;
    },
    targetPage?: Page,
  ): Promise<void>;
  saveTemporaryFile(
    data: Uint8Array<ArrayBufferLike>,
    filename: string,
  ): Promise<{filepath: string}>;
  saveFile(
    data: Uint8Array<ArrayBufferLike>,
    clientProvidedFilePath: string,
    extension: SupportedExtensions,
  ): Promise<{filename: string}>;
  waitForTextOnPage(
    text: string[],
    timeout?: number,
    page?: Page,
  ): Promise<Element>;
  /**
   * Returns a reqid for a cdpRequestId.
   */
  resolveCdpRequestId(
    page: ContextPage,
    cdpRequestId: string,
  ): number | undefined;
  getScreenRecorder(): {recorder: ScreenRecorder; filePath: string} | null;
  setScreenRecorder(
    data: {recorder: ScreenRecorder; filePath: string} | null,
  ): void;
  installExtension(path: string): Promise<string>;
  uninstallExtension(id: string): Promise<void>;
  triggerExtensionAction(id: string): Promise<void>;
  listExtensions(): Promise<Map<string, Extension>>;
  getExtension(id: string): Promise<Extension | undefined>;
  getSelectedMcpPage(): McpPage;
  getExtensionServiceWorkers(): ExtensionServiceWorker[];
  getExtensionServiceWorkerId(
    extensionServiceWorker: ExtensionServiceWorker,
  ): string | undefined;
  getHeapSnapshotAggregates(
    filePath: string,
  ): Promise<Record<string, AggregatedInfoWithUid>>;
  getHeapSnapshotStats(
    filePath: string,
  ): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics>;
  getHeapSnapshotStaticData(
    filePath: string,
  ): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null>;
  getHeapSnapshotNodesByUid(
    filePath: string,
    uid: number,
  ): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
}>;

/**
 * Only add methods used by tools/*.
 */
export type ContextPage = Readonly<{
  readonly pptrPage: Page;
  getAXNodeByUid(uid: string): TextSnapshotNode | undefined;
  getElementByUid(uid: string): Promise<ElementHandle<Element>>;

  getDialog(): Dialog | undefined;
  clearDialog(): void;
  throwIfDialogOpen(): void;
  waitForEventsAfterAction(
    action: () => Promise<unknown>,
    options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
  ): Promise<void>;
  getThirdPartyDeveloperTools():
    | ToolGroup<ThirdPartyDeveloperToolDefinition>
    | undefined;
  executeThirdPartyDeveloperTool(
    toolName: string,
    params: Record<string, unknown>,
    response: Response,
  ): Promise<void>;
  getDevToolsData(): Promise<DevToolsData>;
}>;

export function defineTool<Schema extends zod.ZodRawShape>(
  definition: ToolDefinition<Schema>,
): ToolDefinition<Schema>;

export function defineTool<
  Schema extends zod.ZodRawShape,
  Args extends ParsedArguments = ParsedArguments,
>(
  definition: (args?: Args) => ToolDefinition<Schema>,
): (args?: Args) => ToolDefinition<Schema>;

export function defineTool<
  Schema extends zod.ZodRawShape,
  Args extends ParsedArguments = ParsedArguments,
>(
  definition:
    | ToolDefinition<Schema>
    | ((args?: Args) => ToolDefinition<Schema>),
) {
  if (typeof definition === 'function') {
    const factory = definition;
    return (args: Args) => {
      return factory(args);
    };
  }
  return definition;
}

interface PageToolDefinition<
  Schema extends zod.ZodRawShape = zod.ZodRawShape,
> extends BaseToolDefinition<Schema> {
  handler: (
    request: Request<Schema> & {page: ContextPage},
    response: Response,
    context: Context,
  ) => Promise<void>;
}

export type DefinedPageTool<Schema extends zod.ZodRawShape = zod.ZodRawShape> =
  PageToolDefinition<Schema> & {
    pageScoped: true;
    handler: (
      request: Request<Schema> & {page: ContextPage},
      response: Response,
      context: Context,
    ) => Promise<void>;
  };

export function definePageTool<Schema extends zod.ZodRawShape>(
  definition: PageToolDefinition<Schema>,
): DefinedPageTool<Schema>;

export function definePageTool<
  Schema extends zod.ZodRawShape,
  Args extends ParsedArguments = ParsedArguments,
>(
  definition: (args?: Args) => PageToolDefinition<Schema>,
): (args?: Args) => DefinedPageTool<Schema>;

export function definePageTool<
  Schema extends zod.ZodRawShape,
  Args extends ParsedArguments = ParsedArguments,
>(
  definition:
    | PageToolDefinition<Schema>
    | ((args?: Args) => PageToolDefinition<Schema>),
): DefinedPageTool<Schema> | ((args?: Args) => DefinedPageTool<Schema>) {
  if (typeof definition === 'function') {
    return (args?: Args): DefinedPageTool<Schema> => {
      const tool = definition(args);
      return {
        ...tool,
        pageScoped: true,
      };
    };
  }

  return {
    ...definition,
    pageScoped: true,
  } as DefinedPageTool<Schema>;
}

export const CLOSE_PAGE_ERROR =
  'The last open page cannot be closed. It is fine to keep it open.';

export const pageIdSchema = {
  pageId: zod.number().optional().describe('Targets a specific page by ID.'),
};

export const timeoutSchema = {
  timeout: zod
    .number()
    .int()
    .optional()
    .describe(
      `Maximum wait time in milliseconds. If set to 0, the default timeout will be used.`,
    )
    .transform(value => {
      return value && value <= 0 ? undefined : value;
    }),
};

export function viewportTransform(arg: string | undefined):
  | {
      width: number;
      height: number;
      deviceScaleFactor?: number;
      isMobile?: boolean;
      isLandscape?: boolean;
      hasTouch?: boolean;
    }
  | undefined {
  if (!arg) {
    return undefined;
  }
  const [dimensions, ...tags] = arg.split(',');
  const isMobile = tags.includes('mobile');
  const hasTouch = tags.includes('touch');
  const isLandscape = tags.includes('landscape');
  const [width, height, dpr] = dimensions.split('x').map(Number) as [
    number,
    number,
    number | undefined,
  ];
  return {
    width,
    height,
    deviceScaleFactor: dpr,
    isMobile: isMobile,
    isLandscape: isLandscape,
    hasTouch: hasTouch,
  };
}

export function geolocationTransform(arg: string | undefined) {
  if (!arg) {
    return undefined;
  }
  const [latitude, longitude] = arg.split('x').map(Number) as [number, number];
  return {
    latitude,
    longitude,
  };
}
