#!/usr/bin/env node

import chalk from "chalk";
import { Command } from "commander";
import { Box, render, Text } from "ink";
import SelectInput from "ink-select-input";
import TextInput from "ink-text-input";
import { spawn, spawnSync } from "node:child_process";
import {
  copyFileSync,
  cpSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readdirSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import ora from "ora";
import React, { useState } from "react";
import { extract } from "tar";
import {
  deriveProjectInfo,
  findUnsafeEntries,
  updateIndexTs,
  updatePackageJson,
} from "./utils.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Helper function to run package manager commands securely using spawn
function runPackageManager(
  packageManager: string,
  args: string[],
  cwd: string
): Promise<{ stderr: string }> {
  return new Promise((resolve, reject) => {
    const child = spawn(packageManager, args, {
      cwd,
      stdio: ["ignore", "pipe", "pipe"],
      shell: false,
    });

    let stderr = "";
    child.stderr?.on("data", (data: Buffer) => {
      stderr += data.toString();
    });

    child.on("close", (code) => {
      if (code === 0) {
        resolve({ stderr });
      } else {
        reject(new Error(`${packageManager} install failed:\n${stderr}`));
      }
    });

    child.on("error", (err) => {
      reject(err);
    });
  });
}

// Detect which package manager was used to run this script
function detectPackageManager(): string | null {
  // Check npm_config_user_agent which contains info about the package manager
  const userAgent = process.env.npm_config_user_agent || "";

  if (userAgent.includes("yarn")) {
    return "yarn";
  } else if (userAgent.includes("pnpm")) {
    return "pnpm";
  } else if (userAgent.includes("npm")) {
    return "npm";
  }

  return null;
}

// Get the dev command for a specific package manager
function getDevCommand(packageManager: string): string {
  switch (packageManager) {
    case "yarn":
      return "yarn dev";
    case "pnpm":
      return "pnpm dev";
    case "npm":
    default:
      return "npm run dev";
  }
}

// Get the install command for a specific package manager
function getInstallCommand(packageManager: string): string {
  switch (packageManager) {
    case "yarn":
      return "yarn";
    case "pnpm":
      return "pnpm install";
    case "npm":
    default:
      return "npm install";
  }
}

// Get optimized install arguments for faster installation
function getInstallArgs(packageManager: string): string[] {
  switch (packageManager) {
    case "yarn":
      // Yarn: prefer offline cache, skip audit (faster, but less secure)
      return ["install"];
    case "pnpm":
      // pnpm: prefer offline cache, skip audit
      return ["install"];
    case "npm":
    default:
      // npm: prefer offline cache, skip audit and funding messages
      return ["install"];
  }
}

// Telemetry data defined in https://github.com/vercel-labs/skills/blob/main/src/telemetry.ts
interface InstallTelemetryData {
  event: "install";
  source: string;
  skills: string;
  agents: string;
  global?: "1";
  skillFiles?: string; // JSON stringified { skillName: relativePath }
  /**
   * Source type for different hosts:
   * - 'github': GitHub repository (default, uses raw.githubusercontent.com)
   * - 'raw': Direct URL to SKILL.md (generic raw URL)
   * - Provider IDs like 'mintlify', 'huggingface', etc.
   */
  sourceType?: string;
}

// Send telemetry event for vercel skills.sh
// Necessary for ranking and discoverability of skills
function sendInstallTelemetryEvent(agents: string, skills: string): void {
  const TELEMETRY_URL = "https://add-skill.vercel.sh/t";
  const SOURCE_REPO = "mcp-use/mcp-use";
  const telemetryData: InstallTelemetryData = {
    event: "install",
    source: SOURCE_REPO,
    skills,
    agents,
    sourceType: "github",
  };
  try {
    const params = new URLSearchParams();

    // Add event data
    for (const [key, value] of Object.entries(telemetryData)) {
      if (value !== undefined && value !== null) {
        params.set(key, String(value));
      }
    }

    // Fire and forget - don't await, silently ignore errors
    fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {});
  } catch {
    // Silently fail - telemetry should never break the CLI
  }
}

// Type-safe enum for IDE presets
type AgentPreset = "cursor" | "claude-code" | "codex";

// Download and extract skills folder from GitHub repository
async function addSkillsToProject(projectPath: string): Promise<void> {
  const REPO_OWNER = "mcp-use";
  const REPO_NAME = "mcp-use";
  const REPO_COMMIT = "main";

  const tarballUrl = `https://codeload.github.com/${REPO_OWNER}/${REPO_NAME}/tar.gz/${REPO_COMMIT}`;

  // Create temp directory for extraction
  const tempDir = mkdtempSync(join(tmpdir(), "mcp-use-skills-"));

  try {
    // Download tarball
    const response = await fetch(tarballUrl);
    if (!response.ok) {
      throw new Error(`Failed to download tarball: ${response.statusText}`);
    }

    // Extract only the skills folder from the tarball
    // Tarball structure: mcp-use-{commit}/skills/...
    await pipeline(
      Readable.fromWeb(response.body as any),
      extract({
        cwd: tempDir,
        filter: (path) => path.includes("/skills/mcp-apps-builder/"),
        strip: 1, // Removes 'mcp-use-{commit}/' prefix
      })
    );

    const skillsPath = join(tempDir, "skills");

    if (!existsSync(skillsPath)) {
      throw new Error("Skills folder not found in tarball");
    }

    // Copy to each requested preset location
    const presets: AgentPreset[] = ["cursor", "claude-code", "codex"];
    const presetFolders: Record<AgentPreset, string> = {
      cursor: ".cursor",
      "claude-code": ".claude",
      codex: ".agent",
    };

    for (const preset of presets) {
      const folderName = presetFolders[preset];
      const outputPath = join(projectPath, folderName, "skills");

      // Use cpSync with recursive flag (Node 16.7+)
      cpSync(skillsPath, outputPath, { recursive: true });
    }

    // Get skill names from extracted directory
    const skillNames = readdirSync(skillsPath, { withFileTypes: true })
      .filter((dirent) => dirent.isDirectory())
      .map((dirent) => dirent.name);

    sendInstallTelemetryEvent(presets.join(","), skillNames.join(","));
  } catch (error) {
    console.log(
      chalk.yellow(
        "⚠️  Failed to download skills from GitHub. Continuing without skills..."
      )
    );
    console.log(
      chalk.yellow(
        `   Error: ${error instanceof Error ? error.message : String(error)}`
      )
    );
    return;
  } finally {
    rmSync(tempDir, { recursive: true, force: true });
  }
}

const program = new Command();

// Render logo as ASCII art
function renderLogo(): void {
  console.log(
    chalk.white.bold(
      " ███╗   ███╗   ██████╗  ██████╗         ██╗   ██╗  ███████╗  ███████╗"
    )
  );
  console.log(
    chalk.white.bold(
      " ████╗ ████║  ██╔════╝  ██╔══██╗        ██║   ██║  ██╔════╝  ██╔════╝"
    )
  );
  console.log(
    chalk.white.bold(
      " ██╔████╔██║  ██║       ██████╔╝  ━━━━  ██║   ██║  ███████╗  █████╗  "
    )
  );
  console.log(
    chalk.white.bold(
      " ██║╚██╔╝██║  ██║       ██╔═══╝   ━━━━  ██║   ██║  ╚════██║  ██╔══╝  "
    )
  );
  console.log(
    chalk.white.bold(
      " ██║ ╚═╝ ██║  ╚██████╗  ██║             ╚██████╔╝  ███████║  ███████╗"
    )
  );
  console.log(
    chalk.white.bold(
      " ╚═╝     ╚═╝   ╚═════╝  ╚═╝              ╚═════╝   ╚══════╝  ╚══════╝"
    )
  );
  console.log("");
  console.log(chalk.gray.bold(" by Manufact"));
}

const packageJson = JSON.parse(
  readFileSync(join(__dirname, "../package.json"), "utf-8")
);

// Read current package versions from workspace
function getCurrentPackageVersions(
  isDevelopment: boolean = false,
  useCanary: boolean = false
) {
  const versions: Record<string, string> = {};

  try {
    if (isDevelopment) {
      // In development mode, use workspace dependencies for all packages
      versions["mcp-use"] = "workspace:*";
      versions["@mcp-use/cli"] = "workspace:*";
      versions["@mcp-use/inspector"] = "workspace:*";
    } else if (useCanary) {
      // In canary mode, use canary versions for published packages
      versions["mcp-use"] = "canary";
      // For unpublished packages, keep them as workspace dependencies
      // These packages are not available on npm registry yet
      versions["@mcp-use/cli"] = "canary";
      versions["@mcp-use/inspector"] = "canary";
    } else {
      // In production mode, use latest for published packages
      versions["mcp-use"] = "latest";
      // For unpublished packages, keep them as workspace dependencies
      // These packages are not available on npm registry yet
      versions["@mcp-use/cli"] = "latest";
      versions["@mcp-use/inspector"] = "latest";
    }
  } catch (error) {
    // Use defaults when not in workspace (normal for published package)
    // Log error details in development mode for debugging
    if (process.env.NODE_ENV === "development") {
      console.warn(
        "⚠️  Could not read workspace package versions, using defaults"
      );
      console.warn(
        `   Error: ${error instanceof Error ? error.message : String(error)}`
      );
      if (error instanceof Error && error.stack) {
        console.warn(`   Stack: ${error.stack}`);
      }
    }
  }

  return versions;
}

// Process template files to replace version placeholders
function processTemplateFile(
  filePath: string,
  versions: Record<string, string>,
  isDevelopment: boolean = false,
  useCanary: boolean = false
) {
  const content = readFileSync(filePath, "utf-8");
  let processedContent = content;

  // Replace version placeholders with current versions
  for (const [packageName, version] of Object.entries(versions)) {
    const placeholder = `{{${packageName}_version}}`;
    processedContent = processedContent.replace(
      new RegExp(placeholder, "g"),
      version
    );
  }

  // Handle workspace dependencies based on mode
  if (isDevelopment) {
    // Keep workspace dependencies for development
    processedContent = processedContent.replace(
      /"mcp-use": "\^[^"]+"/,
      '"mcp-use": "workspace:*"'
    );
    processedContent = processedContent.replace(
      /"@mcp-use\/cli": "\^[^"]+"/,
      '"@mcp-use/cli": "workspace:*"'
    );
    processedContent = processedContent.replace(
      /"@mcp-use\/inspector": "\^[^"]+"/,
      '"@mcp-use/inspector": "workspace:*"'
    );
  } else if (useCanary) {
    // Replace both workspace:* and any existing caret versions with canary
    processedContent = processedContent.replace(
      /"mcp-use": "workspace:\*"/,
      `"mcp-use": "canary"`
    );
    processedContent = processedContent.replace(
      /"mcp-use": "\^[^"]+"/,
      `"mcp-use": "canary"`
    );
    processedContent = processedContent.replace(
      /"@mcp-use\/cli": "workspace:\*"/,
      `"@mcp-use/cli": "canary"`
    );
    processedContent = processedContent.replace(
      /"@mcp-use\/cli": "\^[^"]+"/,
      `"@mcp-use/cli": "canary"`
    );
    processedContent = processedContent.replace(
      /"@mcp-use\/inspector": "workspace:\*"/,
      `"@mcp-use/inspector": "canary"`
    );
    processedContent = processedContent.replace(
      /"@mcp-use\/inspector": "\^[^"]+"/,
      `"@mcp-use/inspector": "canary"`
    );
  } else {
    // Replace workspace dependencies with specific versions for production
    processedContent = processedContent.replace(
      /"mcp-use": "workspace:\*"/,
      `"mcp-use": "${versions["mcp-use"] || "latest"}"`
    );
    processedContent = processedContent.replace(
      /"@mcp-use\/cli": "workspace:\*"/,
      `"@mcp-use/cli": "${versions["@mcp-use/cli"] || "latest"}"`
    );
    processedContent = processedContent.replace(
      /"@mcp-use\/inspector": "workspace:\*"/,
      `"@mcp-use/inspector": "${versions["@mcp-use/inspector"] || "latest"}"`
    );
  }

  return processedContent;
}

// Function to get available templates
function getAvailableTemplates(): string[] {
  const templatesDir = join(__dirname, "templates");
  if (!existsSync(templatesDir)) {
    return [];
  }
  return readdirSync(templatesDir, { withFileTypes: true })
    .filter((dirent) => dirent.isDirectory())
    .map((dirent) => dirent.name)
    .sort();
}

// Function to list templates
function listTemplates(): void {
  console.log("");
  renderLogo();
  console.log("");
  console.log(chalk.bold("Available Templates:"));
  console.log("");

  const templatesDir = join(__dirname, "templates");
  const availableTemplates = getAvailableTemplates();

  if (availableTemplates.length === 0) {
    console.log(chalk.red("❌ No templates found!"));
    return;
  }

  for (const template of availableTemplates) {
    const packageJsonPath = join(templatesDir, template, "package.json");
    let description = "MCP server template";

    if (existsSync(packageJsonPath)) {
      try {
        const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
        description = packageJson.description || description;
      } catch (error) {
        // Use default description
      }
    }

    console.log(
      chalk.cyan(`  ${template.padEnd(15)}`),
      chalk.gray(description)
    );
  }

  console.log("");
  console.log(
    chalk.gray(
      "💡 Use with: npx create-mcp-use-app my-project --template <template>"
    )
  );
  console.log("");
}

program
  .name("create-mcp-use-app")
  .description("Create a new MCP server project")
  .version(packageJson.version)
  .argument("[project-name]", "Name of the MCP server project")
  .option(
    "-t, --template <template>",
    "Template to use (starter, mcp-ui, mcp-apps, blank) or GitHub repo URL (owner/repo or https://github.com/owner/repo)"
  )
  .option("--list-templates", "List all available templates")
  .option("--install", "Install dependencies after creating project")
  .option("--no-install", "Skip installing dependencies")
  .option("--skills", "Install skills for all agents")
  .option("--no-skills", "Skip installing skills")
  .option("--no-git", "Skip initializing a git repository")
  .option("--dev", "Use workspace dependencies for development")
  .option("--canary", "Use canary versions of packages")
  .option("--yarn", "Use yarn as package manager")
  .option("--npm", "Use npm as package manager")
  .option("--pnpm", "Use pnpm as package manager")
  .action(
    async (
      projectName: string | undefined,
      options: {
        template?: string;
        listTemplates?: boolean;
        install?: boolean;
        skills?: boolean;
        git: boolean;
        dev: boolean;
        canary: boolean;
        yarn?: boolean;
        npm?: boolean;
        pnpm?: boolean;
      }
    ) => {
      try {
        // Handle --list-templates flag first
        if (options.listTemplates) {
          listTemplates();
          process.exit(0);
        }

        // Validate that --dev and --canary are mutually exclusive
        if (options.dev && options.canary) {
          console.error(chalk.red("❌ Cannot use --dev and --canary together"));
          console.error(
            chalk.yellow("   Please choose only one dependency mode")
          );
          process.exit(1);
        }

        let selectedTemplate = options.template;

        console.log("");
        renderLogo();
        console.log("");

        // If no project name provided, prompt for it
        if (!projectName) {
          projectName = await promptForProjectName();
          console.log("");
        }

        // Prompt for template if one wasn't provided via --template flag
        if (!options.template && !selectedTemplate) {
          selectedTemplate = await promptForTemplate();
        }

        // Set default template if still not selected
        if (!selectedTemplate) {
          selectedTemplate = "starter";
        }

        const trimmedProjectName = projectName!.trim();
        if (!trimmedProjectName) {
          console.error(chalk.red("❌ Project name cannot be empty"));
          process.exit(1);
        }

        const { useCurrentDir, projectPath, displayName, packageName } =
          deriveProjectInfo(trimmedProjectName, process.cwd());

        if (useCurrentDir) {
          const unsafeEntries = findUnsafeEntries(projectPath);
          if (unsafeEntries.length > 0) {
            console.error(
              chalk.red(
                "❌ Cannot initialize here — these entries would clash with the template:"
              )
            );
            for (const entry of unsafeEntries) {
              console.error(chalk.red(`   • ${entry}`));
            }
            console.error("");
            console.error(
              chalk.yellow(
                "   Remove these files, or pass a project name to scaffold into a new subdirectory instead."
              )
            );
            process.exit(1);
          }
        } else {
          // Security: Validate project name doesn't contain path traversal
          if (
            trimmedProjectName.includes("..") ||
            trimmedProjectName.includes("/") ||
            trimmedProjectName.includes("\\")
          ) {
            console.error(
              chalk.red(
                '❌ Project name cannot contain path separators or ".."'
              )
            );
            console.error(
              chalk.yellow('   Use simple names like "my-mcp-server"')
            );
            process.exit(1);
          }

          // Validate against common protected directory names
          const protectedNames = [
            "node_modules",
            ".git",
            ".env",
            "package.json",
            "src",
            "dist",
          ];
          if (protectedNames.includes(trimmedProjectName.toLowerCase())) {
            console.error(
              chalk.red(`❌ Cannot use protected name "${trimmedProjectName}"`)
            );
            console.error(
              chalk.yellow("   Please choose a different project name")
            );
            process.exit(1);
          }

          // Check if directory already exists
          if (existsSync(projectPath)) {
            console.error(
              chalk.red(`❌ Directory "${trimmedProjectName}" already exists!`)
            );
            console.error(
              chalk.yellow(
                "   Please choose a different name or remove the existing directory"
              )
            );
            process.exit(1);
          }

          // Create project directory
          mkdirSync(projectPath, { recursive: true });
        }

        console.log(chalk.cyan(`🚀 Creating MCP server "${displayName}"...`));

        // Validate template name
        const validatedTemplate = validateTemplateName(selectedTemplate);

        // Get current package versions
        const versions = getCurrentPackageVersions(options.dev, options.canary);

        // Copy template files
        await copyTemplate(
          projectPath,
          validatedTemplate,
          versions,
          options.dev,
          options.canary
        );

        // Templates inline {{PROJECT_NAME}} inside double-quoted string literals,
        // so both files use the npm-safe packageName, not the raw displayName.
        updatePackageJson(projectPath, packageName);
        updateIndexTs(projectPath, packageName);

        // Non-interactive defaults when template is specified via flag
        // Enables usage in CI/tests without blocking prompts
        if (options.template !== undefined) {
          // Default to not installing
          if (options.install === undefined) {
            options.install = false;
          }

          // Default to both agents
          if (options.skills === undefined) {
            options.skills = true;
          }
        }

        // Ask to install skills if not explicitly set
        console.log("");
        const shouldInstallSkills =
          options.skills !== undefined
            ? options.skills
            : await promptForSkillsPresets();
        let skillsInstalled = false;
        if (shouldInstallSkills) {
          console.log("");
          console.log(chalk.cyan("📚 Installing skills..."));
          try {
            await addSkillsToProject(projectPath);
            skillsInstalled = true;
            console.log(chalk.green("✅ Skills installed successfully!"));
          } catch (err) {
            console.log(
              chalk.yellow(
                "⚠️  Skills install failed. Run `npx skills add mcp-use/mcp-use` manually in root directory."
              )
            );
          }
        }

        // Determine which package manager to use
        let usedPackageManager = "npm";

        // Check if a specific package manager was requested via flags
        if (options.yarn) {
          usedPackageManager = "yarn";
        } else if (options.npm) {
          usedPackageManager = "npm";
        } else if (options.pnpm) {
          usedPackageManager = "pnpm";
        } else {
          // Try to detect which package manager was used to run this script
          const detected = detectPackageManager();
          if (detected) {
            usedPackageManager = detected;
          } else {
            // No flag and couldn't detect, try in order: npm → pnpm → yarn
            const defaultOrder = ["npm", "pnpm", "yarn"];
            // We'll determine the working one during installation
            usedPackageManager = defaultOrder[0];
          }
        }

        // Ask to install dependencies if not explicitly set
        console.log("");
        const shouldInstall =
          options.install !== undefined
            ? options.install
            : await promptForInstall(usedPackageManager);

        // Install dependencies if requested or chosen via prompt
        if (shouldInstall) {
          console.log("");
          console.log(chalk.cyan("📦 Installing dependencies..."));
          console.log("");

          const isKnownManager =
            options.yarn ||
            options.npm ||
            options.pnpm ||
            detectPackageManager();
          const managersToTry = isKnownManager
            ? [usedPackageManager]
            : ["npm", "pnpm", "yarn"];

          let installed = false;
          for (const pm of managersToTry) {
            const spinner = ora(`Installing packages with ${pm}...`).start();
            try {
              await runPackageManager(pm, getInstallArgs(pm), projectPath);
              usedPackageManager = pm;
              spinner.succeed(`Packages installed successfully with ${pm}`);
              installed = true;
              break;
            } catch (err) {
              const remaining = managersToTry.slice(
                managersToTry.indexOf(pm) + 1
              );
              if (remaining.length > 0) {
                spinner.warn(`${pm} not available, trying ${remaining[0]}...`);
              } else {
                spinner.fail("Package installation failed");
                if (err instanceof Error) {
                  console.error(chalk.red(err.message));
                }
              }
            }
          }

          if (!installed) {
            console.log(
              '⚠️  Please run "npm install", "yarn install", or "pnpm install" manually'
            );
          }
        }

        // Note: Git initialization is skipped to avoid delays when scanning node_modules.
        // Users can run `git init` themselves when ready.

        console.log("");
        console.log(chalk.green("✅ MCP server created successfully!"));
        if (options.dev) {
          console.log(
            chalk.yellow("🔧 Development mode: Using workspace dependencies")
          );
        } else if (options.canary) {
          console.log(
            chalk.cyan("🚀 Canary mode: Using canary versions of packages")
          );
        }
        console.log("");
        console.log(chalk.bold("📁 Project structure:"));
        console.log(`   ${useCurrentDir ? "." : displayName}/`);
        if (skillsInstalled) {
          console.log("   ├── .agent/skills/");
          console.log("   ├── .claude/skills/");
          console.log("   ├── .cursor/skills/");
        }
        if (validatedTemplate === "blank") {
          console.log("   ├── public/");
          console.log("   ├── index.ts (server entry point)");
          console.log("   ├── package.json");
          console.log("   ├── tsconfig.json");
          console.log("   └── README.md");
        } else if (validatedTemplate === "mcp-apps") {
          console.log("   ├── public/");
          console.log("   ├── resources/");
          console.log("   │   └── product-search-result/");
          console.log("   │       └── widget.tsx");
          console.log("   ├── index.ts (server entry point)");
          console.log("   ├── package.json");
          console.log("   ├── tsconfig.json");
          console.log("   └── README.md");
        } else if (validatedTemplate === "starter") {
          console.log("   ├── public/");
          console.log("   ├── resources/");
          console.log("   │   └── display-weather.tsx");
          console.log("   ├── index.ts (server entry point)");
          console.log("   ├── package.json");
          console.log("   ├── tsconfig.json");
          console.log("   └── README.md");
        } else {
          console.log("   ├── index.ts (server entry point)");
          console.log("   ├── package.json");
          console.log("   ├── tsconfig.json");
          console.log("   └── README.md");
        }
        console.log("");
        console.log(chalk.bold("🚀 To get started:"));
        if (!useCurrentDir) {
          console.log(chalk.cyan(`   cd ${displayName}`));
        }
        if (!shouldInstall) {
          console.log(
            chalk.cyan(`   ${getInstallCommand(usedPackageManager)}`)
          );
        }
        console.log(chalk.cyan(`   ${getDevCommand(usedPackageManager)}`));
        console.log("");
        console.log(chalk.bold("📤 To deploy:"));
        console.log(
          chalk.cyan(
            `   ${usedPackageManager === "yarn" ? "yarn" : usedPackageManager === "pnpm" ? "pnpm" : "npm run"} deploy`
          )
        );
        console.log("");
        if (options.dev) {
          console.log(
            chalk.yellow(
              "💡 Development mode: Your project uses workspace dependencies"
            )
          );
          console.log(
            chalk.yellow(
              "   Make sure you're in the mcp-use workspace root for development"
            )
          );
          console.log("");
        }
        console.log(chalk.cyan("📚 Learn more: https://manufact.com/docs"));
        console.log(chalk.gray("💬 For feedback and bug reporting visit:"));
        console.log(
          chalk.gray(
            "   https://github.com/mcp-use/mcp-use or https://manufact.com"
          )
        );
      } catch (error) {
        console.error("❌ Error creating MCP server:", error);
        process.exit(1);
      }
    }
  );

// Parse GitHub repo URL to extract owner, repo, and branch
interface GitHubRepoInfo {
  owner: string;
  repo: string;
  branch?: string;
}

function parseGitHubRepoUrl(url: string): GitHubRepoInfo | null {
  const trimmed = url.trim();

  // Match patterns:
  // - https://github.com/owner/repo
  // - https://github.com/owner/repo.git
  // - https://github.com/owner/repo#branch
  // - https://github.com/owner/repo/tree/branch
  // - github.com/owner/repo
  // - owner/repo
  // - owner/repo#branch

  // Full URL with https://
  let match = trimmed.match(
    /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/#]+?)(?:\.git)?(?:\/tree\/([^/]+))?(?:#(.+))?$/
  );
  if (match) {
    const result = {
      owner: match[1],
      repo: match[2],
      branch: match[3] || match[4] || undefined,
    };
    return validateGitHubRepoInfo(result) ? result : null;
  }

  // github.com/owner/repo format
  match = trimmed.match(
    /^(?:www\.)?github\.com\/([^/]+)\/([^/#]+?)(?:\.git)?(?:#(.+))?$/
  );
  if (match) {
    const result = {
      owner: match[1],
      repo: match[2],
      branch: match[3] || undefined,
    };
    return validateGitHubRepoInfo(result) ? result : null;
  }

  // owner/repo format
  match = trimmed.match(/^([^/#]+)\/([^/#]+?)(?:#(.+))?$/);
  if (match) {
    const result = {
      owner: match[1],
      repo: match[2],
      branch: match[3] || undefined,
    };
    return validateGitHubRepoInfo(result) ? result : null;
  }

  return null;
}

function validateGitHubRepoInfo(info: GitHubRepoInfo): boolean {
  // GitHub usernames and repo names can contain alphanumeric characters and hyphens
  // Branch names can contain alphanumeric, hyphens, underscores, slashes, and dots
  const validIdentifier = /^[a-zA-Z0-9_-]+$/;
  const validBranch = /^[a-zA-Z0-9_./-]+$/;

  if (!validIdentifier.test(info.owner) || !validIdentifier.test(info.repo)) {
    return false;
  }

  if (info.branch && !validBranch.test(info.branch)) {
    return false;
  }

  return true;
}

function isGitHubRepoUrl(template: string): boolean {
  return parseGitHubRepoUrl(template) !== null;
}

// Clone GitHub repo to a temporary directory
async function cloneGitHubRepo(
  repoInfo: GitHubRepoInfo,
  tempDir: string
): Promise<string> {
  // Check if git is available using spawnSync with array arguments
  const versionCheck = spawnSync("git", ["--version"], {
    stdio: "ignore",
    shell: false,
  });
  if (versionCheck.status !== 0 || versionCheck.error) {
    console.error(
      chalk.red("❌ Git is not installed or not available in PATH")
    );
    console.error(
      chalk.yellow("   Please install Git to use GitHub repository templates")
    );
    process.exit(1);
  }

  const repoUrl = `https://github.com/${repoInfo.owner}/${repoInfo.repo}.git`;
  const branch = repoInfo.branch || "main";

  const spinner = ora(`Cloning repository from GitHub...`).start();

  // Helper function to clone with a specific branch
  const cloneWithBranch = (
    branchName: string
  ): { success: boolean; error?: Error } => {
    const result = spawnSync(
      "git",
      ["clone", "--depth", "1", "--branch", branchName, repoUrl, tempDir],
      {
        stdio: "pipe",
        shell: false,
      }
    );

    if (result.status !== 0) {
      const errorMessage =
        result.stderr?.toString() ||
        result.stdout?.toString() ||
        "Unknown error";
      return {
        success: false,
        error: new Error(errorMessage),
      };
    }

    return { success: true };
  };

  // Try cloning with the specified branch
  const cloneResult = cloneWithBranch(branch);
  if (cloneResult.success) {
    spinner.succeed("Repository cloned successfully");
    return tempDir;
  }

  // If branch doesn't exist and no branch was specified, try main/master
  if (!repoInfo.branch) {
    spinner.text = `Branch "${branch}" not found, trying "main"...`;
    const mainResult = cloneWithBranch("main");
    if (mainResult.success) {
      spinner.succeed("Repository cloned successfully (using main branch)");
      return tempDir;
    }

    spinner.text = `Branch "main" not found, trying "master"...`;
    const masterResult = cloneWithBranch("master");
    if (masterResult.success) {
      spinner.succeed("Repository cloned successfully (using master branch)");
      return tempDir;
    }
  }

  // All attempts failed
  spinner.fail("Failed to clone repository");
  console.error(chalk.red(`❌ Error cloning repository: ${repoUrl}`));
  if (repoInfo.branch) {
    console.error(chalk.yellow(`   Branch "${repoInfo.branch}" may not exist`));
  }

  const errorMessage = cloneResult.error?.message || "Unknown error";
  if (errorMessage.includes("not found")) {
    console.error(chalk.yellow(`   Repository may not exist or is private`));
  } else {
    console.error(chalk.yellow(`   ${errorMessage}`));
  }

  throw cloneResult.error || new Error("Failed to clone repository");
}

// Validate and sanitize template name to prevent path traversal
function validateTemplateName(template: string): string {
  const sanitized = template.trim();

  // If it's a GitHub repo URL, skip validation (it will be handled separately)
  if (isGitHubRepoUrl(sanitized)) {
    return sanitized;
  }

  // Template aliases for backward compatibility
  const aliases: Record<string, string> = {
    "apps-sdk": "mcp-apps", // Silent redirect for backward compatibility
  };

  const resolvedTemplate = aliases[sanitized] || sanitized;

  // Security: Prevent path traversal attacks
  if (
    resolvedTemplate.includes("..") ||
    resolvedTemplate.includes("/") ||
    resolvedTemplate.includes("\\")
  ) {
    console.error(chalk.red("❌ Invalid template name"));
    console.error(
      chalk.yellow("   Template name cannot contain path separators")
    );
    process.exit(1);
  }

  // Only allow alphanumeric characters, hyphens, and underscores
  if (!/^[a-zA-Z0-9_-]+$/.test(resolvedTemplate)) {
    console.error(chalk.red("❌ Invalid template name"));
    console.error(
      chalk.yellow(
        "   Template name can only contain letters, numbers, hyphens, and underscores"
      )
    );
    process.exit(1);
  }

  return resolvedTemplate;
}

async function copyTemplate(
  projectPath: string,
  template: string,
  versions: Record<string, string>,
  isDevelopment: boolean = false,
  useCanary: boolean = false
) {
  // Check if template is a GitHub repo URL
  const repoInfo = parseGitHubRepoUrl(template);
  if (repoInfo) {
    const tempDir = mkdtempSync(join(tmpdir(), "create-mcp-use-app-"));

    try {
      // Clone the repository
      await cloneGitHubRepo(repoInfo, tempDir);

      // Copy the cloned repository contents to the project path
      copyDirectoryWithProcessing(
        tempDir,
        projectPath,
        versions,
        isDevelopment,
        useCanary
      );
    } finally {
      // Clean up temporary directory
      try {
        rmSync(tempDir, { recursive: true, force: true });
      } catch (error) {
        // Ignore cleanup errors
        if (process.env.NODE_ENV === "development") {
          console.warn(
            `Warning: Failed to clean up temp directory: ${tempDir}`
          );
        }
      }
    }
    return;
  }

  // Handle local templates
  const templatePath = join(__dirname, "templates", template);

  if (!existsSync(templatePath)) {
    console.error(chalk.red(`❌ Template "${template}" not found!`));

    // Dynamically list available templates
    const templatesDir = join(__dirname, "templates");
    if (existsSync(templatesDir)) {
      const availableTemplates = readdirSync(templatesDir, {
        withFileTypes: true,
      })
        .filter((dirent) => dirent.isDirectory())
        .map((dirent) => dirent.name)
        .sort();

      console.log(`Available templates: ${availableTemplates.join(", ")}`);
    } else {
      console.log("No templates directory found");
    }

    console.log(
      '💡 Tip: Use "starter" template for a comprehensive MCP server with all features'
    );
    console.log(
      '💡 Tip: Use "mcp-apps" template for a MCP server that displays Widgets on ChatGPT, Claude, and other mcp-apps compatible clients'
    );
    console.log(
      '💡 Tip: Use a GitHub repo URL like "owner/repo" or "https://github.com/owner/repo" to use a custom template'
    );
    process.exit(1);
  }

  copyDirectoryWithProcessing(
    templatePath,
    projectPath,
    versions,
    isDevelopment,
    useCanary
  );
}

function copyDirectoryWithProcessing(
  src: string,
  dest: string,
  versions: Record<string, string>,
  isDevelopment: boolean,
  useCanary: boolean = false
) {
  const entries = readdirSync(src, { withFileTypes: true });

  for (const entry of entries) {
    // Skip .git directories (user's project will have its own git repo initialized)
    if (entry.name === ".git") {
      continue;
    }

    const srcPath = join(src, entry.name);
    // Special handling: rename gitignore to .gitignore
    // This is necessary because npm excludes .gitignore files when publishing packages
    const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
    const destPath = join(dest, destName);

    if (entry.isDirectory()) {
      mkdirSync(destPath, { recursive: true });
      copyDirectoryWithProcessing(
        srcPath,
        destPath,
        versions,
        isDevelopment,
        useCanary
      );
    } else {
      // Process files that might contain version placeholders
      if (entry.name === "package.json" || entry.name.endsWith(".json")) {
        const processedContent = processTemplateFile(
          srcPath,
          versions,
          isDevelopment,
          useCanary
        );
        writeFileSync(destPath, processedContent);
      } else {
        copyFileSync(srcPath, destPath);
      }
    }
  }
}

// Ink component for install dependencies prompt (Y/n)
function InstallPrompt({
  packageManager,
  onSubmit,
}: {
  packageManager: string;
  onSubmit: (install: boolean) => void;
}) {
  const [value, setValue] = useState("");

  const handleSubmit = (val: string) => {
    const trimmed = val.trim().toLowerCase();
    if (trimmed === "" || trimmed === "y" || trimmed === "yes") {
      onSubmit(true);
    } else if (trimmed === "n" || trimmed === "no") {
      onSubmit(false);
    }
  };

  return (
    <Box flexDirection="column">
      <Box marginBottom={1}>
        <Text bold>Install dependencies with {packageManager}? (Y/n)</Text>
      </Box>
      <Box>
        <Text color="cyan">❯ </Text>
        <TextInput value={value} onChange={setValue} onSubmit={handleSubmit} />
      </Box>
    </Box>
  );
}

async function promptForInstall(packageManager: string): Promise<boolean> {
  return new Promise((resolve) => {
    const { unmount } = render(
      <InstallPrompt
        packageManager={packageManager}
        onSubmit={(install) => {
          unmount();
          resolve(install);
        }}
      />
    );
  });
}

// Ink component for skills preset prompt (Y/n)
function SkillsPresetPrompt({
  onSubmit,
}: {
  onSubmit: (install: boolean) => void;
}) {
  const [value, setValue] = useState("");

  const handleSubmit = (val: string) => {
    const trimmed = val.trim().toLowerCase();
    if (trimmed === "" || trimmed === "y" || trimmed === "yes") {
      onSubmit(true); // Install all skills
    } else if (trimmed === "n" || trimmed === "no") {
      onSubmit(false); // No skills
    }
  };

  return (
    <Box flexDirection="column">
      <Box marginBottom={1}>
        <Text bold>
          Install AI coding skills for Cursor, Claude Code, and Codex?
          (Recommended) (Y/n)
        </Text>
      </Box>
      <Box>
        <Text color="cyan">❯ </Text>
        <TextInput value={value} onChange={setValue} onSubmit={handleSubmit} />
      </Box>
    </Box>
  );
}

async function promptForSkillsPresets(): Promise<boolean> {
  return new Promise((resolve) => {
    const { unmount } = render(
      <SkillsPresetPrompt
        onSubmit={(presets) => {
          unmount();
          resolve(presets);
        }}
      />
    );
  });
}

// Ink component for project name input
function ProjectNameInput({ onSubmit }: { onSubmit: (name: string) => void }) {
  const [value, setValue] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = (val: string) => {
    const trimmed = val.trim();

    if (!trimmed) {
      setError("Project name is required");
      return;
    }
    if (trimmed === ".") {
      const unsafeEntries = findUnsafeEntries(process.cwd());
      if (unsafeEntries.length > 0) {
        const list = unsafeEntries.map((entry) => `   • ${entry}`).join("\n");
        setError(
          `Cannot initialize here — these entries would clash with the template:\n${list}\n\nRemove these files, or pick a project name to scaffold into a subdirectory.`
        );
        return;
      }
    } else {
      if (!/^[a-zA-Z0-9-_]+$/.test(trimmed)) {
        setError(
          "Project name can only contain letters, numbers, hyphens, and underscores"
        );
        return;
      }
      if (existsSync(join(process.cwd(), trimmed))) {
        setError(
          `Directory "${trimmed}" already exists! Please choose a different name.`
        );
        return;
      }
    }

    onSubmit(trimmed);
  };

  return (
    <Box flexDirection="column">
      <Box marginBottom={1}>
        <Text bold>What is your project name?</Text>
      </Box>
      <Box>
        <Text color="cyan">❯ </Text>
        <TextInput value={value} onChange={setValue} onSubmit={handleSubmit} />
      </Box>
      {error && (
        <Box marginTop={1}>
          <Text color="red">✖ {error}</Text>
        </Box>
      )}
    </Box>
  );
}

async function promptForProjectName(): Promise<string> {
  return new Promise((resolve) => {
    const { unmount } = render(
      <ProjectNameInput
        onSubmit={(name) => {
          unmount();
          resolve(name);
        }}
      />
    );
  });
}

// Ink component for template selection
function TemplateSelector({
  onSelect,
}: {
  onSelect: (template: string) => void;
}) {
  const templatesDir = join(__dirname, "templates");

  if (!existsSync(templatesDir)) {
    return (
      <Box flexDirection="column">
        <Text color="red">
          ❌ Templates directory not found at: {templatesDir}
        </Text>
        <Text color="yellow"> __dirname: {__dirname}</Text>
      </Box>
    );
  }

  const availableTemplates = readdirSync(templatesDir, { withFileTypes: true })
    .filter((dirent) => dirent.isDirectory())
    .map((dirent) => dirent.name)
    .sort();

  if (availableTemplates.length === 0) {
    return (
      <Box flexDirection="column">
        <Text color="red">❌ No templates found in: {templatesDir}</Text>
      </Box>
    );
  }

  // Read template descriptions dynamically from package.json files
  const items = availableTemplates.map((template) => {
    const packageJsonPath = join(templatesDir, template, "package.json");
    let description = "MCP server template";

    if (existsSync(packageJsonPath)) {
      try {
        const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
        description = packageJson.description || description;
      } catch (error) {
        // Use default description
      }
    }

    return {
      label: `${template} - ${description}`,
      value: template,
    };
  });

  // Set default to mcp-apps if available, otherwise first template
  const defaultIndex = items.findIndex((item) => item.value === "mcp-apps");
  const initialIndex = defaultIndex >= 0 ? defaultIndex : 0;

  return (
    <Box flexDirection="column">
      <Box marginBottom={1}>
        <Text bold>Select a template:</Text>
      </Box>
      <SelectInput
        items={items}
        initialIndex={initialIndex}
        onSelect={(item) => onSelect(item.value)}
        indicatorComponent={({ isSelected }) => (
          <Text color="cyan">{isSelected ? "❯ " : "  "}</Text>
        )}
        itemComponent={({ isSelected, label }) => (
          <Text color={isSelected ? "cyan" : undefined}>{label}</Text>
        )}
      />
    </Box>
  );
}

async function promptForTemplate(): Promise<string> {
  return new Promise((resolve) => {
    const { unmount } = render(
      <TemplateSelector
        onSelect={(template) => {
          unmount();
          resolve(template);
        }}
      />
    );
  });
}

program.parse();
