#!/usr/bin/env tsx
/**
 * Auto-generates strongly-typed TypeScript wrappers for MCP tools.
 *
 * Queries the bridge HTTP server for tool schemas, then generates one `.ts`
 * file per tool under the output directory, plus a barrel `index.ts`.
 *
 * Usage:
 *   npx tsx codegen.ts <output-dir>
 *
 * The generated wrappers import `callTool` from `../../browser-code/client.js`,
 * so the expected workspace layout is:
 *
 *   browser-code/client.ts     ← low-level bridge caller
 *   servers/chrome-devtools/   ← generated wrappers (output-dir)
 */

import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";

const BRIDGE_URL =
  process.env.BROWSER_CODE_BRIDGE_URL ?? "http://127.0.0.1:9223";

// ── Types ────────────────────────────────────────────────────────────

export interface JsonSchema {
  type?: string;
  description?: string;
  items?: JsonSchema;
  enum?: unknown[];
}

export interface ToolSchema {
  name: string;
  description?: string;
  inputSchema?: {
    type: "object";
    properties?: Record<string, JsonSchema>;
    required?: string[];
  };
}

// ── Helpers ──────────────────────────────────────────────────────────

function toCamelCase(s: string): string {
  return s.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
}

function toPascalCase(s: string): string {
  const camel = toCamelCase(s);
  return camel[0].toUpperCase() + camel.slice(1);
}

function jsonSchemaToTs(schema: JsonSchema): string {
  switch (schema.type) {
    case "string":
      return "string";
    case "number":
    case "integer":
      return "number";
    case "boolean":
      return "boolean";
    case "array":
      return schema.items ? `${jsonSchemaToTs(schema.items)}[]` : "unknown[]";
    case "object":
      return "Record<string, unknown>";
    default:
      return "unknown";
  }
}

// ── Code generation ──────────────────────────────────────────────────

export function generateToolFile(tool: ToolSchema): string {
  const funcName = toCamelCase(tool.name);
  const pascalName = toPascalCase(tool.name);
  const props = tool.inputSchema?.properties ?? {};
  const required = new Set(tool.inputSchema?.required ?? []);
  const propEntries = Object.entries(props);
  const hasOnlyOptionalInputs = propEntries.length > 0 && required.size === 0;

  // Check if tool has a filePath string property — if so, we need to resolve
  // relative paths against the caller's cwd (the bridge runs in a different cwd).
  const hasFilePath = props.filePath?.type === "string";
  const supportsScriptAlias =
    tool.name === "evaluate_script" && props.function?.type === "string";

  let code = "// auto-generated by codegen.ts — do not edit\n";
  code += 'import { callTool } from "../../browser-code/client.js";\n';
  if (hasFilePath) {
    code += 'import { resolve } from "node:path";\n';
  }
  code += "\n";

  // Input interface (only if there are parameters)
  if (propEntries.length > 0) {
    code += `export interface ${pascalName}Input {\n`;
    for (const [name, schema] of propEntries) {
      if (schema.description) {
        code += `  /** ${schema.description} */\n`;
      }
      const opt = required.has(name) ? "" : "?";
      code += `  ${name}${opt}: ${jsonSchemaToTs(schema)};\n`;
    }
    if (supportsScriptAlias) {
      code +=
        "  /** Alias for `function` that matches the wrapper name ergonomically. */\n";
      code += "  script?: string;\n";
    }
    code += "}\n\n";
  }

  // Wrapper function
  if (tool.description) {
    code += `/** ${tool.description} */\n`;
  }
  if (propEntries.length > 0) {
    const signature = hasOnlyOptionalInputs
      ? `input: ${pascalName}Input = {}`
      : `input: ${pascalName}Input`;
    code += `export async function ${funcName}(${signature}): Promise<string> {\n`;
    if (supportsScriptAlias) {
      code += "  if (!input.function && input.script) {\n";
      code += "    input = { ...input, function: input.script };\n";
      code += "  }\n";
    }
    if (hasFilePath) {
      code += `  if (input.filePath && !input.filePath.startsWith("/")) {\n`;
      code += `    input = { ...input, filePath: resolve(input.filePath) };\n`;
      code += `  }\n`;
    }
    code += `  return callTool("${tool.name}", input);\n`;
  } else {
    code += `export async function ${funcName}(): Promise<string> {\n`;
    code += `  return callTool("${tool.name}");\n`;
  }
  code += "}\n";

  return code;
}

function generateBarrel(tools: ToolSchema[]): string {
  const lines = ["// auto-generated by codegen.ts — do not edit"];
  for (const tool of tools) {
    const modName = toCamelCase(tool.name);
    lines.push(`export * from "./${modName}.js";`);
  }
  lines.push(""); // trailing newline
  return lines.join("\n");
}

// ── Main ─────────────────────────────────────────────────────────────

async function main() {
  const outputDir = process.argv[2];
  if (!outputDir) {
    console.error("Usage: codegen.ts <output-dir>");
    process.exit(1);
  }

  // Fetch tool schemas from bridge
  const res = await fetch(`${BRIDGE_URL}/tools`);
  if (!res.ok) throw new Error(`Failed to fetch tools: ${await res.text()}`);
  const tools: ToolSchema[] = await res.json();

  // Create output directory
  mkdirSync(outputDir, { recursive: true });

  // Generate one file per tool
  for (const tool of tools) {
    const fileName = `${toCamelCase(tool.name)}.ts`;
    writeFileSync(join(outputDir, fileName), generateToolFile(tool));
  }

  // Generate barrel index.ts
  writeFileSync(join(outputDir, "index.ts"), generateBarrel(tools));

  console.log(`Generated ${tools.length} typed wrappers in ${outputDir}`);
}

if (
  process.argv[1] &&
  import.meta.url === pathToFileURL(process.argv[1]).href
) {
  main().catch((err) => {
    console.error(`[codegen] Fatal: ${err}`);
    process.exit(1);
  });
}
