import { realpathSync } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import type { CliArtifactMetadata } from '../../cli-metadata.js';
import type { ServerDefinition } from '../../config.js';
import { MCPORTER_VERSION } from '../../version.js';
import { buildToolDoc, type ToolOptionDoc } from '../list-detail-helpers.js';
import { markExecutable } from './fs-helpers.js';
import { renderEmbeddedHelpSource } from './template-help.js';
import type { GeneratedOption, ToolMetadata } from './tools.js';
import { buildEmbeddedSchemaMap } from './tools.js';
import { stableJsonStringify } from './stable-json.js';

export interface TemplateInput {
  outputPath?: string;
  runtimeScriptPath?: string;
  runtimeKind: 'node' | 'bun';
  timeoutMs: number;
  definition: ServerDefinition;
  serverName: string;
  tools: ToolMetadata[];
  generator: {
    name: string;
    version: string;
  };
  metadata: CliArtifactMetadata;
}

export async function writeTemplate(input: TemplateInput): Promise<string> {
  const resolvedOutput = input.outputPath
    ? path.resolve(input.outputPath)
    : path.resolve(process.cwd(), `${input.serverName}.ts`);
  const runtimeScriptPath = input.runtimeScriptPath ? path.resolve(input.runtimeScriptPath) : resolvedOutput;
  await fs.mkdir(path.dirname(resolvedOutput), { recursive: true });
  await fs.writeFile(
    resolvedOutput,
    renderTemplate({ ...input, outputPath: resolvedOutput, runtimeScriptPath }),
    'utf8'
  );
  await markExecutable(resolvedOutput);
  return resolvedOutput;
}

export async function readPackageMetadata(): Promise<{ name: string; version: string }> {
  const packageJsonPath = path.resolve(process.cwd(), 'package.json');
  try {
    const buffer = await fs.readFile(packageJsonPath, 'utf8');
    const pkg = JSON.parse(buffer) as { name?: string; version?: string };
    return {
      name: pkg.name ?? 'mcporter',
      version: pkg.version ?? '0.0.0',
    };
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
      throw error;
    }
    return { name: 'mcporter', version: MCPORTER_VERSION };
  }
}

export function renderTemplate({
  runtimeKind,
  timeoutMs,
  definition,
  serverName,
  tools,
  generator,
  metadata,
  outputPath,
  runtimeScriptPath,
}: TemplateInput): string {
  const imports = [
    "import path from 'node:path';",
    "import { fileURLToPath } from 'node:url';",
    "import { Command } from 'commander';",
    "import { createGeneratedKeepAliveRuntime, createRuntime, createServerProxy, handleDaemonCli } from 'mcporter';",
    "import { createCallResult } from 'mcporter';",
  ].join('\n');
  const embedded = stableJsonStringify(
    JSON.parse(JSON.stringify(definition, (_key, value) => (value instanceof URL ? value.toString() : value))),
    2
  );
  const relativeStdioCwd = computeRelativeStdioCwd(definition, runtimeScriptPath ?? outputPath);
  const generatorHeader = `Generated by ${generator.name}@${generator.version} — https://github.com/openclaw/mcporter`;
  const toolDocs = tools.map((tool) => ({
    tool,
    doc: buildToolDoc({
      serverName,
      toolName: tool.tool.name,
      description: tool.tool.description,
      outputSchema: tool.tool.outputSchema,
      options: tool.options,
      requiredOnly: true,
      colorize: false,
      flagExtras: [{ text: '--raw <json>' }],
    }),
  }));
  const renderedTools = toolDocs.map((entry) =>
    Object.assign(renderToolCommand(entry.tool, timeoutMs, serverName, entry.doc), {
      doc: entry.doc,
      tool: entry.tool,
    })
  );
  assertUniqueGeneratedCommandNames(renderedTools);
  const toolHelp = renderedTools.map((entry) => ({
    name: entry.commandName,
    description: entry.tool.tool.description ?? '',
    usage: entry.doc.flagUsage ? `${entry.commandName} ${entry.doc.flagUsage}` : undefined,
    flags: entry.doc.flagUsage ?? '',
  }));
  const generatorHeaderLiteral = JSON.stringify(generatorHeader);
  const toolHelpLiteral = stableJsonStringify(toolHelp, 2);
  const embeddedSchemas = stableJsonStringify(buildEmbeddedSchemaMap(tools), 2);
  const embeddedMetadata = stableJsonStringify(metadata, 2);
  const toolBlocks = renderedTools.map((entry) => entry.block).join('\n\n');
  const signatureMap = Object.fromEntries(renderedTools.map((entry) => [entry.commandName, entry.tsSignature]));
  const signatureMapLiteral = stableJsonStringify(signatureMap, 2);
  const generatedHeaderComment = `// @generated by ${generator.name}@${generator.version}. DO NOT EDIT.`;
  return `#!/usr/bin/env ${runtimeKind === 'bun' ? 'bun' : 'node'}
${generatedHeaderComment}
${imports}

const __mcpScriptDir = path.dirname(fileURLToPath(import.meta.url));
const __mcpRelativeStdioCwd: string | null = ${JSON.stringify(relativeStdioCwd)};
const embeddedServer = ${embedded} as const;
const embeddedSchemas = ${embeddedSchemas} as const;
const embeddedName = ${JSON.stringify(serverName)};
const embeddedDescription = ${JSON.stringify(
    definition.description ?? `Standalone CLI for the ${serverName} MCP server.`
  )};
const generatorInfo = ${generatorHeaderLiteral};
const generatorTools = ${toolHelpLiteral} as const;
const embeddedMetadata = ${embeddedMetadata} as const;
const artifactKind = determineArtifactKind();
const program = new Command();
program.name(embeddedName);
program.description(embeddedDescription);
program.option('-t, --timeout <ms>', 'Call timeout in milliseconds', (value) => parseInt(value, 10), ${timeoutMs});
program.option('-o, --output <format>', 'Output format: text|markdown|json|raw', 'text');
const commandSignatures: Record<string, string> = ${signatureMapLiteral};
program.configureHelp({
\tcommandTerm(cmd) {
\t\tconst term = cmd.name();
\t\treturn commandSignatures[term] ?? cmd.name();
\t},
});
program.showSuggestionAfterError(true);

${toolBlocks}

program
\t.command('__mcporter_inspect', { hidden: true })
\t.description('Internal metadata printer for mcporter inspect-cli.')
\t.action(() => {
\t\tconst payload = buildMetadataPayload();
\t\tconsole.log(JSON.stringify(payload, null, 2));
\t});

configureToolCommandHelps();

const FORCE_COLOR = process.env.FORCE_COLOR?.toLowerCase();
const forceDisableColor = FORCE_COLOR === '0' || FORCE_COLOR === 'false';
const forceEnableColor = FORCE_COLOR === '1' || FORCE_COLOR === 'true' || FORCE_COLOR === '2' || FORCE_COLOR === '3';
const hasNoColor = process.env.NO_COLOR !== undefined;
const stdoutStream = process.stdout as NodeJS.WriteStream | undefined;
const supportsAnsiColor = !hasNoColor && (forceEnableColor || (!forceDisableColor && Boolean(stdoutStream?.isTTY)));

const tint = {
	bold(text: string): string {
		return supportsAnsiColor ? '\u001B[1m' + text + '\u001B[0m' : text;
	},
	dim(text: string): string {
		return supportsAnsiColor ? '\u001B[90m' + text + '\u001B[0m' : text;
	},
	extraDim(text: string): string {
		return supportsAnsiColor ? '\u001B[38;5;244m' + text + '\u001B[0m' : text;
	},
};

${renderEmbeddedHelpSource()}

function printResult(result: unknown, format: string) {
\tconst wrapped = createCallResult(result);
\tconst rawPayload = unwrapRawPayload(wrapped.raw);
\tswitch (format) {
\t\tcase 'json': {
\t\t\tconst json = wrapped.json();
\t\t\tif (json !== null) {
\t\t\t\tconsole.log(JSON.stringify(json, null, 2));
\t\t\t\treturn;
\t\t\t}
\t\t\tconsole.log(JSON.stringify(rawPayload, null, 2));
\t\t\treturn;
\t\t}
\t\tcase 'markdown': {
\t\t\tconst markdown = wrapped.markdown();
\t\t\tif (markdown) {
\t\t\t\tconsole.log(markdown);
\t\t\t\treturn;
\t\t\t}
\t\t\tbreak;
\t\t}
\t\tcase 'raw': {
\t\t\tconsole.log(JSON.stringify(rawPayload, null, 2));
\t\t\treturn;
\t\t}
\t}
\tconst text = wrapped.text();
\tif (text) {
\t\tconsole.log(text);
\t} else {
\t\tconsole.log(JSON.stringify(rawPayload, null, 2));
\t}
}

function unwrapRawPayload(value: unknown): unknown {
\tif (value && typeof value === 'object' && 'raw' in value) {
\t\treturn (value as { raw: unknown }).raw;
\t}
\treturn value;
}

function parseArrayOption(value: string, itemType: 'string' | 'number' | 'boolean' | 'json') {
\tconst trimmed = value.trim();
\tif (trimmed.startsWith('[')) {
\t\tconst parsed = JSON.parse(trimmed);
\t\tif (!Array.isArray(parsed)) {
\t\t\tthrow new Error('Expected a JSON array.');
\t\t}
\t\treturn parsed;
\t}
\tif (itemType === 'json') {
\t\tconst parsed = JSON.parse('[' + value + ']');
\t\tif (!Array.isArray(parsed)) {
\t\t\tthrow new Error('Expected JSON array items.');
\t\t}
\t\treturn parsed;
\t}
\tconst values = value.split(',').map((entry) => entry.trim());
\tif (itemType === 'number') {
\t\treturn values.map((entry) => parseFiniteNumber(entry));
\t}
\tif (itemType === 'boolean') {
\t\treturn values.map((entry) => entry !== 'false');
\t}
\treturn values;
}

function parseFiniteNumber(value: string): number {
\tconst trimmed = value.trim();
\tconst parsed = Number(trimmed);
\tif (trimmed === '' || !Number.isFinite(parsed)) {
\t\tthrow new Error('Expected a finite number.');
\t}
\treturn parsed;
}

function normalizeEmbeddedServer(server: typeof embeddedServer) {
\tconst base = { ...server } as Record<string, unknown>;
\tif ((server.command as any).kind === 'http') {
\t\tconst urlRaw = (server.command as any).url;
\t\tconst urlValue = typeof urlRaw === 'string' ? urlRaw : String(urlRaw);
\t\treturn {
\t\t\t...base,
\t\t\tcommand: {
\t\t\t\t...(server.command as Record<string, unknown>),
\t\t\t\turl: new URL(urlValue),
\t\t\t},
\t\t};
\t}
\tif ((server.command as any).kind === 'stdio') {
\t\tconst stdio = server.command as Record<string, unknown>;
\t\tconst resolvedCwd =
\t\t\t__mcpRelativeStdioCwd !== null
\t\t\t\t? path.resolve(__mcpScriptDir, __mcpRelativeStdioCwd)
\t\t\t\t: typeof stdio.cwd === 'string' && stdio.cwd.length > 0
\t\t\t\t\t? stdio.cwd
\t\t\t\t\t: undefined;
\t\treturn {
\t\t\t...base,
\t\t\tcommand: {
\t\t\t\t...stdio,
\t\t\t\targs: [ ...((server.command as any).args ?? []) ],
\t\t\t\t...(resolvedCwd !== undefined ? { cwd: resolvedCwd } : {}),
\t\t\t},
\t\t};
\t}
\treturn base;
}

function determineArtifactKind(): 'template' | 'bundle' | 'binary' {
\tconst scriptPath = typeof process !== 'undefined' && Array.isArray(process.argv) ? process.argv[1] ?? '' : '';
\tif (scriptPath.endsWith('.ts')) {
\t\treturn 'template';
\t}
\tif (scriptPath.endsWith('.js') || scriptPath.endsWith('.mjs') || scriptPath.endsWith('.cjs')) {
\t\treturn 'bundle';
\t}
\treturn 'binary';
}

function resolveArtifactPath(): string {
\tif (typeof process !== 'undefined' && Array.isArray(process.argv) && process.argv.length > 1) {
\t\tconst script = process.argv[1];
\t\tif (script) {
\t\t\treturn script;
\t\t}
\t}
\treturn embeddedMetadata.artifact.path;
}

function buildMetadataPayload() {
\tconst invocation = { ...embeddedMetadata.invocation };
\tconst path = resolveArtifactPath();
\tif (artifactKind === 'template' && path) {
\t\tinvocation.outputPath = invocation.outputPath ?? path;
\t} else if (artifactKind === 'bundle' && path) {
\t\tinvocation.bundle = invocation.bundle ?? path;
\t} else if (artifactKind === 'binary' && path) {
\t\tinvocation.compile = invocation.compile ?? path;
\t}
\treturn {
\t\t...embeddedMetadata,
\t\tartifact: {
\t\t\tpath,
\t\t\tkind: artifactKind,
\t\t},
\t\tinvocation,
\t};
}

async function ensureRuntime() {
	const server = normalizeEmbeddedServer(embeddedServer);
	const baseRuntime = await createRuntime({
		servers: [server as any],
	});
	return await createGeneratedKeepAliveRuntime(baseRuntime, server as any);
}

async function invokeWithTimeout<T>(call: Promise<T>, timeout: number): Promise<T> {
\tif (!Number.isFinite(timeout) || timeout <= 0) {
\t\treturn await call;
\t}
\tlet timer: ReturnType<typeof setTimeout> | undefined;
\ttry {
\t\treturn await Promise.race([
\t\t\tcall,
\t\t\tnew Promise<never>((_, reject) => {
\t\t\t\ttimer = setTimeout(() => {
\t\t\t\t\treject(new Error('Call timed out after ' + timeout + 'ms.'));
\t\t\t\t}, timeout);
\t\t\t}),
\t\t]);
\t} finally {
\t\tif (timer) {
\t\t\tclearTimeout(timer);
\t\t}
\t}
}

function parseGeneratedDaemonInvocation(rawArgs: string[]): { args: string[]; configPath: string; rootDir?: string } | null {
\tconst args = [...rawArgs];
\tlet configPath: string | undefined;
\tlet rootDir: string | undefined;
\twhile (args.length > 0) {
\t\tconst token = args[0];
\t\tif (token === '--config') {
\t\t\targs.shift();
\t\t\tconfigPath = args.shift();
\t\t\tcontinue;
\t\t}
\t\tif (token?.startsWith('--config=')) {
\t\t\targs.shift();
\t\t\tconfigPath = token.slice('--config='.length);
\t\t\tcontinue;
\t\t}
\t\tif (token === '--root') {
\t\t\targs.shift();
\t\t\trootDir = args.shift();
\t\t\tcontinue;
\t\t}
\t\tif (token?.startsWith('--root=')) {
\t\t\targs.shift();
\t\t\trootDir = token.slice('--root='.length);
\t\t\tcontinue;
\t\t}
\t\tbreak;
\t}
\tif (args[0] !== 'daemon') {
\t\treturn null;
\t}
\tif (!configPath) {
\t\tthrow new Error('Generated daemon invocation is missing --config.');
\t}
\treturn { args: args.slice(1), configPath, rootDir };
}

async function runCli(): Promise<void> {
\tconst args = process.argv.slice(2);
\tconst daemonInvocation = parseGeneratedDaemonInvocation(args);
\tif (daemonInvocation) {
\t\tawait handleDaemonCli([...daemonInvocation.args], {
\t\t\tconfigPath: daemonInvocation.configPath,
\t\t\tconfigExplicit: true,
\t\t\trootDir: daemonInvocation.rootDir,
\t\t});
\t\treturn;
\t}
\tif (args.length === 0) {
\t\tprogram.outputHelp();
\t\treturn;
\t}
\tawait program.parseAsync(process.argv);
}

if (process.env.MCPORTER_DISABLE_AUTORUN !== '1') {
\trunCli().catch((error) => {
\t\tconst message = error instanceof Error ? error.message : String(error);
\t\tconsole.error(message);
\t\tprocess.exit(1);
\t});
}
`;
}

export function renderToolCommand(
  tool: ToolMetadata,
  defaultTimeout: number,
  serverName: string,
  existingDoc?: ReturnType<typeof buildToolDoc>
): { block: string; commandName: string; signature: string; tsSignature: string } {
  const commandName = tool.tool.name.replace(/[^a-zA-Z0-9-]/g, '-');
  const description = tool.tool.description ?? `Invoke the ${tool.tool.name} tool.`;
  const doc =
    existingDoc ??
    buildToolDoc({
      serverName,
      toolName: tool.tool.name,
      description: tool.tool.description,
      outputSchema: tool.tool.outputSchema,
      options: tool.options,
      requiredOnly: true,
      colorize: false,
      flagExtras: [{ text: '--raw <json>' }],
    });
  const buildArgs = tool.options
    .map((option) => {
      // Commander.js camelcases flag names (e.g. --relative-path => relativePath).
      const camelCaseProp = option.cliName
        .split('-')
        .filter(Boolean)
        .map((segment, index) => (index === 0 ? segment : `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`))
        .join('');
      const source = `cmdOpts.${camelCaseProp}`;
      return `if (${source} !== undefined) args.${option.property} = ${source};`;
    })
    .join('\n\t\t');
  const requiredChecks = tool.options
    .filter((option) => option.required)
    .map((option) => {
      const camelCaseProp = option.cliName
        .split('-')
        .filter(Boolean)
        .map((segment, index) => (index === 0 ? segment : `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`))
        .join('');
      return { option, camelCaseProp };
    });
  const requiredValidation =
    requiredChecks.length > 0
      ? `const missingRequired = [${requiredChecks
          .map(
            ({ option, camelCaseProp }) =>
              `{ value: cmdOpts.${camelCaseProp}, flag: ${JSON.stringify(`--${option.cliName}`)} }`
          )
          .join(
            ', '
          )}].filter((entry) => entry.value === undefined || (typeof entry.value === 'string' && entry.value.trim() === '')).map((entry) => entry.flag);
\t\t\tif (missingRequired.length > 0) {
\t\t\t\tthrow new Error('Missing required option' + (missingRequired.length === 1 ? '' : 's') + ': ' + missingRequired.join(', '));
\t\t\t}`
      : '';
  const flagUsage = doc.flagUsage;
  const optionLines = doc.optionDocs.map((entry) => renderOption(entry)).join('\n');
  const summary = flagUsage ? `${commandName} ${flagUsage}` : commandName;
  const signature = summary;
  const usageSnippet = flagUsage ? `.usage(${JSON.stringify(flagUsage)})\n` : '';
  const tsSignature = doc.tsSignature;
  const exampleText = doc.examples[0];
  const exampleSnippet = exampleText
    ? `\n\t.addHelpText('after', () => '\\nExample:\\n  ' + ${JSON.stringify(exampleText)})`
    : '';
  const optionalSnippet = doc.optionalSummary
    ? `\n\t.addHelpText('afterAll', () => '\\n' + ${JSON.stringify(doc.optionalSummary)} + '\\n')`
    : '';
  const aliasSnippet = tool.tool.name !== commandName ? `\n\t.alias(${JSON.stringify(tool.tool.name)})` : '';
  const block = `program
\t.command(${JSON.stringify(commandName)})
\t.summary(${JSON.stringify(summary)})
\t.description(${JSON.stringify(description)})
${usageSnippet ? `\t${usageSnippet}` : ''}\t.option('--raw <json>', 'Provide raw JSON arguments to the tool, bypassing flag parsing.')
${optionLines ? `\n${optionLines}` : ''}
${aliasSnippet ? `\t${aliasSnippet}` : ''}\t.action(async (cmdOpts) => {
\t\tconst globalOptions = program.opts();
\t\tconst runtimeContext = await ensureRuntime();
\t\tconst runtime = runtimeContext.runtime;
\t\tconst serverName = embeddedName;
\t\tconst proxy = createServerProxy(runtime, serverName, {
\t\t\tinitialSchemas: embeddedSchemas,
\t\t});
\t\ttry {
\t\t\tconst args = cmdOpts.raw ? JSON.parse(cmdOpts.raw) : ({} as Record<string, unknown>);
\t\t\tif (!cmdOpts.raw) {
\t\t\t\t${requiredValidation}
\t\t\t\t${buildArgs}
\t\t\t}
\t\t\tconst call = (proxy.${tool.methodName} as any)(args);
\t\t\tconst result = await invokeWithTimeout(call, globalOptions.timeout || ${defaultTimeout});
\t\t\tprintResult(result, globalOptions.output ?? 'text');
\t\t} finally {
\t\t\tawait runtimeContext.close(serverName).catch(() => {});
\t\t}
\t})${exampleSnippet}${optionalSnippet};`;
  return { block, commandName, signature, tsSignature };
}

function renderOption(optionDoc: ToolOptionDoc): string {
  const parser = optionParser(optionDoc.option);
  return `\t.option(${JSON.stringify(optionDoc.flagLabel)}, ${JSON.stringify(optionDoc.description)}${
    parser ? `, ${parser}` : ''
  })`;
}

function computeRelativeStdioCwd(definition: ServerDefinition, outputPath?: string): string | null {
  if (!outputPath || definition.command.kind !== 'stdio') {
    return null;
  }
  const rawCwd = definition.command.cwd;
  if (typeof rawCwd === 'string' && path.isAbsolute(rawCwd)) {
    return null;
  }
  const baseCwd = typeof rawCwd === 'string' && rawCwd.length > 0 ? rawCwd : process.cwd();
  const absoluteCwd = realpathIfExists(path.resolve(process.cwd(), baseCwd));
  const outputDir = realpathIfExists(path.dirname(path.resolve(outputPath)));
  const relative = path.relative(outputDir, absoluteCwd);
  if (path.isAbsolute(relative)) {
    return null;
  }
  return relative === '' ? '.' : relative;
}

function realpathIfExists(value: string): string {
  try {
    return realpathSync.native(value);
  } catch {
    return value;
  }
}

export const templateTestHelpers = { computeRelativeStdioCwd };

function optionParser(option: GeneratedOption): string | undefined {
  switch (option.type) {
    case 'number':
      return '(value) => parseFiniteNumber(value)';
    case 'boolean':
      return "(value) => value !== 'false'";
    case 'object':
      return '(value) => JSON.parse(value)';
    case 'array':
      // Coerce array elements to their proper types based on schema
      switch (option.arrayItemType) {
        case 'number':
          return "(value) => parseArrayOption(value, 'number')";
        case 'boolean':
          return "(value) => parseArrayOption(value, 'boolean')";
        case 'object':
          return "(value) => parseArrayOption(value, 'json')";
        default:
          return "(value) => parseArrayOption(value, 'string')";
      }
    default:
      return undefined;
  }
}

function assertUniqueGeneratedCommandNames(tools: Array<{ commandName: string; tool: ToolMetadata }>): void {
  const commands = new Map<string, string>();
  for (const entry of tools) {
    const previous = commands.get(entry.commandName);
    if (previous) {
      throw new Error(
        `Generated command name collision '${entry.commandName}' for tools '${previous}' and '${entry.tool.tool.name}'.`
      );
    }
    commands.set(entry.commandName, entry.tool.tool.name);
  }
}
