import { compile } from 'json-schema-to-typescript'
import type { ServerSurface } from './introspect'

/**
 * Convert an arbitrary server/tool name into a valid PascalCase identifier.
 * Falls back to `Generated` for names with no alphanumeric characters and
 * prefixes an underscore when the result would start with a digit.
 */
function pascal(name: string): string {
  const base = name
    .replace(/[^a-zA-Z0-9]+/g, ' ')
    .split(' ')
    .filter(Boolean)
    .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
    .join('')
  const safe = base || 'Generated'
  return /^[A-Za-z_]/.test(safe) ? safe : `_${safe}`
}

/** Escape a value as a TypeScript string literal (handles quotes/newlines). */
function tsString(value: string): string {
  return JSON.stringify(value)
}

/**
 * Compile a JSON Schema into a TypeScript type via `json-schema-to-typescript`.
 * Returns `'unknown'` for absent/non-object schemas. The compiled output is an
 * `export interface <typeName> { ... }` declaration which callers inline via
 * {@link inlineBody}.
 */
async function schemaToType(
  schema: unknown,
  typeName: string,
): Promise<string> {
  if (!schema || typeof schema !== 'object') return 'unknown'
  const compiled = await compile(schema, typeName, {
    bannerComment: '',
    additionalProperties: false,
    declareExternallyReferenced: true,
  })
  return compiled.trim()
}

export interface EmitInput {
  [serverName: string]: { prefix?: string; surface: ServerSurface }
}

export async function emitDescriptors(input: EmitInput): Promise<string> {
  const blocks: Array<string> = [
    '// AUTO-GENERATED by `npx @tanstack/ai-mcp generate`. Do not edit.',
    "import type { ServerDescriptor } from '@tanstack/ai-mcp'",
    '',
  ]
  // Track config-key -> interface-name so we can emit the combined pool map.
  const mapEntries: Array<[string, string]> = []
  // Distinct server keys can pascal-case to the same identifier
  // (`foo-bar` vs `foo_bar`) — fail loudly rather than emit duplicate
  // interface declarations.
  const seenIfaces = new Set<string>()
  for (const [serverName, { prefix, surface }] of Object.entries(input)) {
    const iface = `${pascal(serverName)}Server`
    if (seenIfaces.has(iface)) {
      throw new Error(
        `Interface name collision for server key "${serverName}" -> ${iface}. ` +
          `Rename one of the colliding servers in your codegen config.`,
      )
    }
    seenIfaces.add(iface)
    mapEntries.push([serverName, iface])
    const toolEntries: Array<string> = []
    for (const tool of surface.tools) {
      const key = prefix ? `${prefix}_${tool.name}` : tool.name
      const inputType = await schemaToType(
        tool.inputSchema,
        `${pascal(key)}Input`,
      )
      const outputType = tool.outputSchema
        ? await schemaToType(tool.outputSchema, `${pascal(key)}Output`)
        : 'unknown'
      // Inline the compiled interface bodies as anonymous object types.
      toolEntries.push(
        `    ${tsString(key)}: { input: ${inlineBody(inputType)}; output: ${inlineBody(outputType)} }`,
      )
    }
    blocks.push(
      `export interface ${iface} extends ServerDescriptor {`,
      `  tools: {`,
      toolEntries.join('\n'),
      `  }`,
      `  resources: ${emitResources(surface)}`,
      `  prompts: ${emitPrompts(surface)}`,
      `  capabilities: ${JSON.stringify(surface.capabilities)} & Record<string, unknown>`,
      `}`,
      '',
    )
  }
  // Combined map for createMCPClients<MCPServers>(...). Keys = config keys
  // verbatim (NOT pascal-cased) so they match the runtime config object and
  // pool.clients access.
  blocks.push(
    'export interface MCPServers extends Record<string, ServerDescriptor> {',
    ...mapEntries.map(([key, iface]) => `  ${tsString(key)}: ${iface}`),
    '}',
    '',
  )
  return blocks.join('\n')
}

/**
 * Extract the `{ ... }` body from a compiled `export interface X { ... }`
 * declaration so it can be inlined as an anonymous object type. Collapses
 * newlines onto a single line. Falls back to `unknown` when no brace is found
 * (e.g. the compiled type is itself `unknown`).
 */
function inlineBody(compiled: string): string {
  const brace = compiled.indexOf('{')
  return brace >= 0
    ? compiled.slice(brace).replace(/\n/g, ' ').trim()
    : 'unknown'
}

function emitResources(s: ServerSurface): string {
  if (!s.resources.length) return '{}'
  return `{ ${s.resources
    .map(
      (r) => `${tsString(r.uri)}: { uri: ${tsString(r.uri)}; data: unknown }`,
    )
    .join('; ')} }`
}

function emitPrompts(s: ServerSurface): string {
  if (!s.prompts.length) return '{}'
  return `{ ${s.prompts
    .map(
      (p) =>
        `${tsString(p.name)}: { args: Record<string, string>; messages: unknown }`,
    )
    .join('; ')} }`
}
