#!/usr/bin/env bun

// typescript5 = npm:typescript@5.9 — TS 7.0 ships without the programmatic
// compiler API (ts.sys, readConfigFile, ...); it returns in 7.1. Typechecking
// runs on 7; only this script needs the legacy API. Drop the alias at 7.1.
import ts from "typescript5";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { ToolIO } from "../src/tools/io-types";

const TOOL_IO_EXPORT = "ToolIO" satisfies keyof { ToolIO: ToolIO };

const appRoot = join(import.meta.dir, "..");
const configPath = join(appRoot, "tsconfig.json");
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
if (configFile.error) {
  throw new Error(
    ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"),
  );
}

const parsed = ts.parseJsonConfigFileContent(
  configFile.config,
  ts.sys,
  appRoot,
  undefined,
  configPath,
);
const program = ts.createProgram(parsed.fileNames, parsed.options);
const checker = program.getTypeChecker();
const source = program.getSourceFile(join(appRoot, "src/tools/io-types.ts"));
if (!source) throw new Error("Could not load tool IO source");
const moduleSymbol = checker.getSymbolAtLocation(source);
if (!moduleSymbol) throw new Error("Could not resolve tool IO module");
const toolIoSymbol = checker
  .getExportsOfModule(moduleSymbol)
  .find((symbol) => symbol.name === TOOL_IO_EXPORT);
if (!toolIoSymbol) throw new Error("Could not resolve ToolIO");

const toolIo = checker.getDeclaredTypeOfSymbol(toolIoSymbol);
const flags =
  ts.TypeFormatFlags.NoTruncation |
  ts.TypeFormatFlags.InTypeAlias |
  ts.TypeFormatFlags.WriteTypeArgumentsOfSignature |
  ts.TypeFormatFlags.UseStructuralFallback;

const entries: string[] = [];
for (const tool of checker.getPropertiesOfType(toolIo)) {
  const contract = checker.getTypeOfSymbolAtLocation(tool, source);
  const input = checker.getPropertyOfType(contract, "input");
  const output = checker.getPropertyOfType(contract, "output");
  if (!input || !output) continue;
  const inputType = checker.getTypeOfSymbolAtLocation(input, source);
  const outputType = checker.getTypeOfSymbolAtLocation(output, source);
  const inputText = checker
    .typeToString(inputType, source, flags)
    .replace(/\bany\b/g, "unknown");
  const outputText = checker
    .typeToString(outputType, source, flags)
    .replace(/\bany\b/g, "unknown");
  entries.push(
    `  ${JSON.stringify(tool.name)}: { input: ${inputText}; output: ${outputText} };`,
  );
}

const output = `/**
 * Generated by apps/api/scripts/generate-tool-contracts.ts.
 *
 * This structural snapshot lets browser code consume the API's typed tool
 * surface without importing server implementations. Regenerate it whenever a
 * builtin tool schema changes.
 */
export interface StudioToolIO {
${entries.join("\n")}
}

export type StudioToolName = keyof StudioToolIO;
export type StudioToolInput<N extends StudioToolName> = StudioToolIO[N]["input"];
export type StudioToolOutput<N extends StudioToolName> =
  StudioToolIO[N]["output"];
`;

const outputPath = join(appRoot, "../../packages/shared/src/tools/tool-io.ts");
await writeFile(outputPath, output);
console.log(`Generated ${entries.length} tool contracts at ${outputPath}`);

function findAliasTypeArgument(
  type: ts.Type,
  aliasName: string,
  index: number,
): ts.Type | undefined {
  if (
    type.aliasSymbol?.name === aliasName &&
    type.aliasTypeArguments?.[index]
  ) {
    return type.aliasTypeArguments[index];
  }
  if (type.isUnionOrIntersection()) {
    for (const member of type.types) {
      const value = findAliasTypeArgument(member, aliasName, index);
      if (value) return value;
    }
  }
  return undefined;
}

const builtInSource = program.getSourceFile(
  join(appRoot, "src/harnesses/decopilot/built-in-tools/index.ts"),
);
const builtInModule =
  builtInSource && checker.getSymbolAtLocation(builtInSource);
const builtInSymbol = builtInModule
  ? checker
      .getExportsOfModule(builtInModule)
      .find((symbol) => symbol.name === "BuiltInToolSet")
  : undefined;
if (!builtInSource || !builtInSymbol) {
  throw new Error("Could not resolve BuiltInToolSet");
}

const chatEntries: string[] = [];
const builtIns = checker.getDeclaredTypeOfSymbol(builtInSymbol);
for (const tool of checker.getPropertiesOfType(builtIns)) {
  const type = checker.getTypeOfSymbolAtLocation(tool, builtInSource);
  const inputSchema = checker.getPropertyOfType(type, "inputSchema");
  const outputSchema = checker.getPropertyOfType(type, "outputSchema");
  const execute = checker.getPropertyOfType(type, "execute");
  if (!inputSchema || !outputSchema || !execute) {
    throw new Error(`Could not resolve schemas for chat tool ${tool.name}`);
  }
  const inputSchemaType = checker.getTypeOfSymbolAtLocation(
    inputSchema,
    builtInSource,
  );
  const outputSchemaType = checker.getTypeOfSymbolAtLocation(
    outputSchema,
    builtInSource,
  );
  const executeType = checker.getTypeOfSymbolAtLocation(execute, builtInSource);
  const input =
    findAliasTypeArgument(inputSchemaType, "FlexibleSchema", 0) ??
    findAliasTypeArgument(executeType, "ToolExecuteFunction", 0);
  const output =
    findAliasTypeArgument(outputSchemaType, "FlexibleSchema", 0) ??
    findAliasTypeArgument(executeType, "ToolExecuteFunction", 1);
  if (!input || !output) {
    throw new Error(`Could not infer schemas for chat tool ${tool.name}`);
  }
  const clean = (typeText: string) =>
    typeText
      .replace(/\bany\b/g, "unknown")
      .replace(/\bGenerateImageResult\b/g, "StudioGenerateImageResult")
      .replace(/import\("[^"]+"\)\./g, "");
  chatEntries.push(
    `  ${JSON.stringify(tool.name)}: { input: ${clean(checker.typeToString(input, builtInSource, flags))}; output: ${clean(checker.typeToString(output, builtInSource, flags))} };`,
  );
}

const chatImports = chatEntries.some((entry) => /\bFinishReason\b/.test(entry))
  ? 'import type { FinishReason } from "ai";\n\n'
  : "";

const chatOutput = `/**
 * Generated by apps/api/scripts/generate-tool-contracts.ts.
 *
 * Browser-safe AI SDK tool contracts for Studio's built-in chat tools.
 */
${chatImports}
export interface StudioGenerateImageResult {
  success: true;
  images: Array<{ uri: string; mediaType: string }>;
  prompt: string;
  model: string;
  usage: { inputTokens: number; outputTokens: number };
  usedReferenceImages: number;
}

export type StudioChatTools = {
${chatEntries.join("\n")}
};
`;
const chatOutputPath = join(
  appRoot,
  "../../packages/shared/src/tools/chat-tools.ts",
);
await writeFile(chatOutputPath, chatOutput);
console.log(
  `Generated ${chatEntries.length} chat tool contracts at ${chatOutputPath}`,
);
