import { createRequire } from 'module';
import * as path from 'path';

import {
  copyFile,
  cp,
  ensureDir,
  fileExists,
  isDirEmpty,
  readFile,
  readJSON,
  runCmd,
  stat,
  writeFile,
  writeJSON,
} from '@frontmcp/utils';

import { c } from '../../core/colors';
import { runInit } from '../../core/tsconfig';
import { getSelfVersion } from '../../core/version';
import { clack } from '../../shared/prompts';
import { buildSkillsSection } from '../skills/install';

// Inline skill manifest types to avoid build dependency on @frontmcp/skills source
interface SkillCatalogEntry {
  name: string;
  category: string;
  description: string;
  path: string;
  targets: string[];
  hasResources: boolean;
  tags: string[];
  bundle?: string[];
  install: { destinations: string[]; mergeStrategy: string; dependencies?: string[] };
}
interface SkillManifest {
  version: number;
  skills: SkillCatalogEntry[];
}

// =============================================================================
// Types
// =============================================================================

export type DeploymentTarget = 'node' | 'vercel' | 'lambda' | 'cloudflare';
export type RedisSetup = 'docker' | 'existing' | 'none';
export type PackageManager = 'npm' | 'yarn' | 'pnpm';
export type SkillsBundle = 'recommended' | 'minimal' | 'full' | 'none';

export interface CreateOptions {
  projectName: string;
  deploymentTarget: DeploymentTarget;
  redisSetup: RedisSetup;
  enableGitHubActions: boolean;
  packageManager: PackageManager;
  nxScaffolded?: boolean;
  skillsBundle?: SkillsBundle;
}

export interface CreateFlags {
  yes?: boolean;
  target?: DeploymentTarget;
  redis?: RedisSetup;
  cicd?: boolean;
  pm?: PackageManager;
  nx?: boolean;
  skills?: SkillsBundle;
}

interface PmConfig {
  lockfileCopy: string;
  installAll: string;
  pruneDevDeps: string;
  run: string;
  userInstall: string;
  ghCache: string;
  ghInstallCmd: string;
  engines: { node: string; npm?: string };
}

const PM_CONFIG: Record<PackageManager, PmConfig> = {
  npm: {
    lockfileCopy: 'COPY package*.json package-lock.json* ./',
    installAll: 'RUN npm ci',
    pruneDevDeps: 'RUN npm prune --omit=dev',
    run: 'npm run',
    userInstall: 'npm install',
    ghCache: 'npm',
    ghInstallCmd: 'npm ci',
    engines: { node: '>=24', npm: '>=10' },
  },
  yarn: {
    lockfileCopy: 'COPY package.json yarn.lock* ./',
    installAll: 'RUN yarn install --frozen-lockfile',
    pruneDevDeps: 'RUN yarn install --frozen-lockfile --production',
    run: 'yarn',
    userInstall: 'yarn install',
    ghCache: 'yarn',
    ghInstallCmd: 'yarn install --frozen-lockfile',
    engines: { node: '>=24' },
  },
  pnpm: {
    lockfileCopy: 'COPY package.json pnpm-lock.yaml* ./',
    installAll: 'RUN pnpm install --frozen-lockfile',
    pruneDevDeps: 'RUN pnpm prune --prod',
    run: 'pnpm run',
    userInstall: 'pnpm install',
    ghCache: 'pnpm',
    ghInstallCmd: 'pnpm install --frozen-lockfile',
    engines: { node: '>=24' },
  },
};

interface PackageJson {
  name?: string;
  version?: string;
  private?: boolean;
  type?: string;
  main?: string;
  scripts?: Record<string, string>;
  engines?: { node?: string; npm?: string };
  dependencies?: Record<string, string>;
  devDependencies?: Record<string, string>;
  [key: string]: unknown;
}

function isInteractive(): boolean {
  return process.stdin.isTTY === true;
}

function sanitizeForFolder(name: string): string {
  const seg = name.startsWith('@') && name.includes('/') ? name.split('/')[1] : name;
  return (
    seg
      .replace(/[^a-zA-Z0-9._-]/g, '-')
      .replace(/-+/g, '-')
      .replace(/^-|-$/g, '')
      .toLowerCase() || 'frontmcp-app'
  );
}

function sanitizeForNpm(name: string): string {
  if (name.startsWith('@') && name.includes('/')) {
    const [scope, pkg] = name.split('/');
    const s = scope.replace(/[^a-z0-9-]/gi, '').toLowerCase();
    const p = pkg.replace(/[^a-z0-9._-]/gi, '-').toLowerCase();
    return `@${s}/${p || 'frontmcp-app'}`;
  }
  return name.replace(/[^a-z0-9._-]/gi, '-').toLowerCase() || 'frontmcp-app';
}

function pkgNameFromCwd(cwd: string) {
  return (
    path
      .basename(cwd)
      .replace(/[^a-zA-Z0-9._-]/g, '-')
      .toLowerCase() || 'frontmcp-app'
  );
}

async function scaffoldFileIfMissing(baseDir: string, p: string, content: string) {
  if (await fileExists(p)) {
    console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
    return;
  }
  await ensureDir(path.dirname(p));
  await writeFile(p, content.replace(/^\n/, ''));
  console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
}

/**
 * Render a starter `frontmcp.config.ts` for the chosen deployment target
 * (issue #400). The emitted file is consumed by `dev`, `test`, `inspector`,
 * `pm start/socket`, and `skills install/export` — see the matching
 * `deployment/frontmcp-config` docs page for the full surface.
 */
function renderFrontmcpConfigTemplate(projectName: string, deploymentTarget: DeploymentTarget): string {
  const safeName = sanitizeForFolder(projectName);
  // Every value of `DeploymentTarget` ('node' | 'vercel' | 'lambda' |
  // 'cloudflare') ships HTTP, so the scaffold only ever emits the HTTP
  // client + transport blocks. The stdio variant lived here briefly but
  // was unreachable — reintroduce it (and add a 'desktop'/'cli' target to
  // `DeploymentTarget`) if scaffold should support stdio in the future.
  const port = 3000;
  const clientBlock = `  clients: {
    'claude-code': {
      name: '${safeName}',
      transport: 'http',
      url: 'http://127.0.0.1:${port}/mcp',
    },
  },`;
  const transportBlock = `  transport: { default: 'http', http: { port: ${port}, path: '/mcp' } },`;
  return `import { defineConfig } from 'frontmcp';

// Single source of truth for every \`frontmcp\` CLI command (dev / test /
// inspector / pm start / socket / skills install / export). Override any
// field with an explicit CLI flag or the matching \`FRONTMCP_<NAME>\` env
// var — precedence: CLI > env > config > built-in default.
export default defineConfig({
  name: '${safeName}',
  entry: './src/main.ts',
  deployments: [{ target: '${deploymentTarget}' }],
${transportBlock}
  env: {
    shared: {},
    dev: { NODE_ENV: 'development' },
  },
${clientBlock}
});
`;
}

const TEMPLATE_MAIN_TS = `
import 'reflect-metadata';
import { FrontMcp } from '@frontmcp/sdk';
import { CalcApp } from './calc.app';

@FrontMcp({
  info: { name: 'Demo 🚀', version: '0.1.0' },
  apps: [CalcApp],
})
export default class Server {}
`;

const TEMPLATE_CALC_APP_TS = `
import { App } from '@frontmcp/sdk';
import AddTool from './tools/add.tool';

@App({
  id: 'calc',
  name: 'Calculator',
  tools: [AddTool],
})
export class CalcApp {}
`;

const TEMPLATE_ADD_TOOL_TS = `
import { Tool, ToolContext, z } from '@frontmcp/sdk';

@Tool({
  name: 'add',
  description: 'Add two numbers',
  inputSchema: {a: z.number(), b: z.number()},
  outputSchema: {result: z.number()}
})
export default class AddTool extends ToolContext {
  async execute(input: { a: number, b: number }) {
    return {
      result: input.a + input.b,
    };
  }
}
`;

const TEMPLATE_E2E_TEST_TS = `
import { expect, test } from '@frontmcp/testing';

/**
 * E2E tests for the MCP server.
 *
 * Run with: frontmcp test
 */
test.describe('Server E2E', () => {
  test.use({
    server: './src/main.ts',
    port: 3100,
  });

  test('should connect and initialize', async ({ mcp }) => {
    expect(mcp.isConnected()).toBe(true);
    expect(mcp.serverInfo.name).toBeDefined();
  });

  test('should list tools', async ({ mcp }) => {
    const tools = await mcp.tools.list();
    expect(tools.length).toBeGreaterThanOrEqual(0);
  });

  test('should call add tool', async ({ mcp }) => {
    const result = await mcp.tools.call('add', { a: 2, b: 3 });
    expect(result).toBeSuccessful();
  });

  test('should list resources', async ({ mcp }) => {
    const resources = await mcp.resources.list();
    expect(resources).toBeDefined();
  });
});
`;

const TEMPLATE_GITIGNORE = `
# Dependencies
node_modules/

# Build output
dist/
*.tsbuildinfo

# IDE
.idea/
.vscode/
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Environment variables
.env
.env.local
.env.*.local

# FrontMCP development keys (contains private keys - never commit!)
.frontmcp/

# Coverage
coverage/

# Test output
test-output/
`;

const TEMPLATE_DOCKERIGNORE = `
node_modules
dist
.git
coverage
test-output
*.tsbuildinfo
.idea
.vscode
.DS_Store
Thumbs.db
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env
.env.local
.env.*.local
.frontmcp
e2e
*.md
LICENSE
`;

// jest.e2e.config.ts and tsconfig.e2e.json removed — `frontmcp test` auto-generates the correct config

function generateClaudeMd(
  projectName: string,
  pm: PackageManager,
  skillEntries: { name: string; description: string }[],
): string {
  const cfg = PM_CONFIG[pm];
  const version = getSelfVersion();
  const skillsBlock = buildSkillsSection(version, skillEntries);
  return `# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

${skillsBlock}
## Project Overview

TypeScript MCP (Model Context Protocol) server built with the **FrontMCP** framework. Uses decorator-based architecture with \`@FrontMcp\`, \`@App\`, and \`@Tool\` decorators from \`@frontmcp/sdk\`. Requires Node >= 22.

## Commands

- \`${cfg.run} dev\` — start dev server with hot reload (uses \`frontmcp dev\`)
- \`${cfg.run} build\` — production build (uses \`frontmcp build\`)
- \`${cfg.run} test\` — run E2E tests (uses \`frontmcp test\`)
- \`${cfg.run} inspect\` — launch MCP Inspector (interactive debugging)
- \`${cfg.run} doctor\` — check project configuration

**Important:** Always use the package.json scripts above. Never run \`npx tsx src/main.ts\` directly — use \`${cfg.run} dev\` instead.

## Architecture

**Entry point:** \`src/main.ts\` — defines the \`Server\` class with \`@FrontMcp\` decorator, registering apps.

**App/Tool hierarchy:** Server → Apps → Tools. Each app groups related tools:
- \`src/calc.app.ts\` — \`CalcApp\` registered with \`@App\`, contains tools
- \`src/tools/add.tool.ts\` — \`AddTool\` extends \`ToolContext\`, uses Zod schemas for input/output validation

**Key patterns:**
- \`reflect-metadata\` must be imported at the entry point
- Tools extend \`ToolContext\` and implement \`async execute(input)\`
- Input/output schemas defined with Zod in the \`@Tool\` decorator
- E2E tests use \`@frontmcp/testing\` with \`test.use({ server, port })\` fixture pattern

## Testing

Run tests with \`${cfg.run} test\` (which runs \`frontmcp test\` under the hood).

- Test files go in \`e2e/\` directory with \`.e2e.spec.ts\` extension
- Split tests by app/feature: \`e2e/calc.e2e.spec.ts\`, \`e2e/my-feature.e2e.spec.ts\`
- Do NOT create a standalone \`jest.e2e.config.ts\` — \`frontmcp test\` auto-generates the correct Jest/SWC config

## Docker

Docker Compose config is in \`ci/docker-compose.yml\` (includes Redis). Redis-only: \`docker compose -f ci/docker-compose.yml up redis -d\`.

## Environment

See \`.env.example\` for required variables (PORT, NODE_ENV, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD).
`;
}

function generateAgentsMd(projectName: string, pm: PackageManager): string {
  const cfg = PM_CONFIG[pm];
  return `# ${projectName} — Agent Guidelines

## Project Type

FrontMCP standalone app — TypeScript-first framework for building MCP (Model Context Protocol) servers.

## Architecture

- **Language**: TypeScript with strict mode enabled
- **Build system**: FrontMCP CLI (\`frontmcp dev\`, \`frontmcp build\`, \`frontmcp test\`)
- **Testing**: Jest via \`frontmcp test\` with 95%+ coverage requirement
- **Package manager**: ${pm}

## Commands

- \`${cfg.run} dev\` — start dev server with hot reload
- \`${cfg.run} build\` — production build
- \`${cfg.run} test\` — run E2E tests
- \`${cfg.run} inspect\` — launch MCP Inspector
- \`${cfg.run} doctor\` — check project configuration

Do NOT run \`npx tsx src/main.ts\` directly — always use \`${cfg.run} dev\`.

## Key Decorators & Context Classes

| Decorator | Context Class | Purpose |
|-----------|---------------|---------|
| \`@Tool\` | \`ToolContext\` | MCP tool with Zod input/output |
| \`@Resource\` / \`@ResourceTemplate\` | \`ResourceContext\` | MCP resource |
| \`@Prompt\` | \`PromptContext\` | MCP prompt returning \`GetPromptResult\` |
| \`@Skill\` | \`SkillContext\` | Instruction-only AI skill |
| \`@Provider\` | — | DI provider with Symbol token |
| \`@App\` | — | Application grouping tools/resources/prompts |
| \`@FrontMcp\` | — | Server entry point |

## Constraints

- No \`any\` types — use \`unknown\` for generic defaults
- No raw \`node:crypto\` or \`fs\` — use \`@frontmcp/utils\`
- No non-null assertions (\`!\`) — throw proper errors
- Zod v4 for all schema validation
- \`@frontmcp/testing\` for test harnesses
- Test files use \`.spec.ts\` extension (not \`.test.ts\`); E2E uses \`.e2e.spec.ts\`
- Split E2E tests by app/feature into separate files

## Structure

- \`src/main.ts\` — Server entry point with \`@FrontMcp\` decorator
- \`src/*.app.ts\` — App definitions with \`@App\` decorator
- \`src/tools/*.tool.ts\` — Tool implementations extending \`ToolContext\`
- \`e2e/*.e2e.spec.ts\` — E2E tests using \`@frontmcp/testing\`
`;
}

const TEMPLATE_ENV_EXAMPLE = `
# Application
PORT=3000
NODE_ENV=development

# Redis (recommended for development, required for production)
REDIS_HOST=localhost
REDIS_PORT=6379
# SECURITY: Set a strong password in production
REDIS_PASSWORD=
REDIS_DB=0

# Optional: Redis TLS (enable for production)
REDIS_TLS=false
`;

// =============================================================================
// Deployment Target Templates
// =============================================================================

// Docker templates (moved to ci/ folder)
function generateDockerfile(pm: PackageManager): string {
  const cfg = PM_CONFIG[pm];
  const corepack = pm !== 'npm' ? '\nRUN corepack enable\n' : '';
  return `
# Build stage
FROM node:24-slim AS builder

WORKDIR /app
${corepack}
# Install all dependencies (including devDependencies for build)
${cfg.lockfileCopy}
${cfg.installAll}

# Copy source and build
COPY . .
RUN ${cfg.run} build

# Prune devDependencies so only production deps remain
${cfg.pruneDevDeps}

# Production stage
FROM node:24-slim AS runner

WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

EXPOSE 3000

CMD ["node", "dist/main.js"]
`;
}

function generateDockerComposeWithRedis(): string {
  return `
services:
  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 3s
      timeout: 5s
      retries: 3

  app:
    build:
      context: ..
      dockerfile: ci/Dockerfile
    ports:
      - '\${PORT:-3000}:3000'
    environment:
      - NODE_ENV=\${NODE_ENV:-development}
      - PORT=\${PORT:-3000}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      redis:
        condition: service_healthy

volumes:
  redis-data:

# Selective rebuild:
#   docker compose -f ci/docker-compose.yml up --build app   # rebuild only the app
#   docker compose -f ci/docker-compose.yml up --build       # rebuild everything
`;
}

function generateDockerComposeNoRedis(): string {
  return `
services:
  app:
    build:
      context: ..
      dockerfile: ci/Dockerfile
    ports:
      - '\${PORT:-3000}:3000'
    environment:
      - NODE_ENV=\${NODE_ENV:-development}
      - PORT=\${PORT:-3000}

# Selective rebuild:
#   docker compose -f ci/docker-compose.yml up --build app   # rebuild only the app
`;
}

const TEMPLATE_ENV_DOCKER_CI = `
# Docker-specific environment
# Use with: docker compose -f ci/docker-compose.yml --env-file ci/.env.docker up

# Application
PORT=3000
NODE_ENV=development

# Redis - use 'redis' (service name) as host inside Docker network
REDIS_HOST=redis
REDIS_PORT=6379
# SECURITY: Set a strong password in production
REDIS_PASSWORD=
REDIS_DB=0
REDIS_TLS=false
`;

// Vercel template
const TEMPLATE_VERCEL_JSON = (projectName: string) =>
  JSON.stringify(
    {
      $schema: 'https://openapi.vercel.sh/vercel.json',
      name: projectName,
      version: 2,
      builds: [
        {
          src: 'dist/main.js',
          use: '@vercel/node',
        },
      ],
      routes: [
        {
          src: '/(.*)',
          dest: '/dist/main.js',
        },
      ],
      env: {
        NODE_ENV: 'production',
      },
    },
    null,
    2,
  );

// AWS Lambda SAM template
const TEMPLATE_SAM_YAML = (projectName: string) => `
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ${projectName} - FrontMCP Lambda Function

Globals:
  Function:
    Timeout: 30
    Runtime: nodejs24.x
    MemorySize: 256

Resources:
  FrontMCPFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ../dist/
      Handler: main.handler
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY

Outputs:
  ApiEndpoint:
    Description: API Gateway endpoint URL
    Value: !Sub "https://\${ServerlessHttpApi}.execute-api.\${AWS::Region}.amazonaws.com"
`;

// Cloudflare Workers template
const TEMPLATE_WRANGLER_TOML = (projectName: string) => `
name = "${projectName}"
main = "dist/main.js"
compatibility_date = "2024-01-01"

[vars]
NODE_ENV = "production"

# Uncomment to enable KV namespace for caching
# [[kv_namespaces]]
# binding = "CACHE"
# id = "your-kv-namespace-id"
`;

// =============================================================================
// GitHub Actions Templates
// =============================================================================

function generatePmSetupSteps(pm: PackageManager): string {
  const cfg = PM_CONFIG[pm];
  if (pm === 'pnpm') {
    return `
      - name: Setup pnpm
        uses: pnpm/action-setup@v4

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: '${cfg.ghCache}'

      - name: Install dependencies
        run: ${cfg.ghInstallCmd}`;
  }
  return `
      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '24'
          cache: '${cfg.ghCache}'

      - name: Install dependencies
        run: ${cfg.ghInstallCmd}`;
}

function generateGhCi(pm: PackageManager): string {
  const cfg = PM_CONFIG[pm];
  return `
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
${generatePmSetupSteps(pm)}

      - name: Type check
        run: npx tsc --noEmit

      - name: Run tests
        run: ${cfg.run} test
`;
}

function generateGhE2e(pm: PackageManager): string {
  const cfg = PM_CONFIG[pm];
  return `
name: E2E Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
${generatePmSetupSteps(pm)}

      - name: Build
        run: ${cfg.run} build

      - name: Run E2E tests
        run: ${cfg.run} test
`;
}

const TEMPLATE_GH_DEPLOY_DOCKER = `
name: Build and Push Docker Image

on:
  push:
    branches: [main]
    tags: ['v*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: \${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v6

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: \${{ env.REGISTRY }}
          username: \${{ github.actor }}
          password: \${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: \${{ env.REGISTRY }}/\${{ env.IMAGE_NAME }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./ci/Dockerfile
          push: true
          tags: \${{ steps.meta.outputs.tags }}
          labels: \${{ steps.meta.outputs.labels }}
`;

function generateGhDeployVercel(pm: PackageManager): string {
  const cfg = PM_CONFIG[pm];
  return `
name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
${generatePmSetupSteps(pm)}

      - name: Build
        run: ${cfg.run} build

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: \${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: \${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: \${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'
`;
}

function generateGhDeployLambda(pm: PackageManager): string {
  const cfg = PM_CONFIG[pm];
  return `
name: Deploy to AWS Lambda

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
${generatePmSetupSteps(pm)}

      - name: Build
        run: ${cfg.run} build

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: \${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: \${{ secrets.AWS_REGION }}

      - name: Setup SAM
        uses: aws-actions/setup-sam@v2

      - name: Deploy with SAM
        run: |
          cd ci
          sam build
          sam deploy --no-confirm-changeset --no-fail-on-empty-changeset
`;
}

function generateGhDeployCloudflare(pm: PackageManager): string {
  const cfg = PM_CONFIG[pm];
  return `
name: Deploy to Cloudflare Workers

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
${generatePmSetupSteps(pm)}

      - name: Build
        run: ${cfg.run} build

      - name: Deploy to Cloudflare
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: \${{ secrets.CLOUDFLARE_API_TOKEN }}
`;
}

// =============================================================================
// Dynamic README Templates
// =============================================================================

function generateReadme(options: CreateOptions): string {
  const { projectName, deploymentTarget, redisSetup, enableGitHubActions, packageManager } = options;
  const cfg = PM_CONFIG[packageManager];

  let readme = `# ${projectName}

A TypeScript MCP server built with [FrontMCP](https://github.com/agentfront/frontmcp).
`;

  // Add CI badge if GitHub Actions enabled
  if (enableGitHubActions) {
    readme += `
![CI](https://github.com/YOUR_USERNAME/${projectName}/actions/workflows/ci.yml/badge.svg)
`;
  }

  readme += `
## Quick Start

\`\`\`bash
# Install dependencies
${cfg.userInstall}

# Start development server
${cfg.run} dev

# Run MCP Inspector
${cfg.run} inspect
\`\`\`
`;

  // Deployment-specific sections
  if (deploymentTarget === 'node') {
    readme += `
## Docker Development

\`\`\`bash
# Start all services${redisSetup === 'docker' ? ' (includes Redis)' : ''}
${cfg.run} docker:up

# Stop all services
${cfg.run} docker:down

# Rebuild Docker image
${cfg.run} docker:build
\`\`\`
`;

    if (redisSetup === 'docker') {
      readme += `
### Redis

Redis is included in the Docker Compose setup. For local development without Docker:

\`\`\`bash
# Start only Redis
docker compose -f ci/docker-compose.yml up redis -d
\`\`\`
`;
    }

    readme += `
## Production Deployment

Build and push the Docker image:

\`\`\`bash
docker build -f ci/Dockerfile -t ${projectName}:latest .
docker push your-registry/${projectName}:latest
\`\`\`
`;
  }

  if (deploymentTarget === 'vercel') {
    readme += `
## Deploy to Vercel

\`\`\`bash
# Build for production
${cfg.run} build

# Deploy using Vercel CLI
npx vercel --prod
\`\`\`

Or connect your repository to Vercel for automatic deployments.
`;
  }

  if (deploymentTarget === 'lambda') {
    readme += `
## Deploy to AWS Lambda

\`\`\`bash
# Build the project
${cfg.run} build

# Deploy using AWS SAM
${cfg.run} deploy
\`\`\`

### Prerequisites

- AWS CLI configured with appropriate credentials
- AWS SAM CLI installed (\`brew install aws-sam-cli\` or \`pip install aws-sam-cli\`)
`;
  }

  if (deploymentTarget === 'cloudflare') {
    readme += `
## Deploy to Cloudflare Workers

\`\`\`bash
# Build the project
${cfg.run} build

# Deploy using Wrangler
${cfg.run} deploy
\`\`\`

### Prerequisites

- Wrangler CLI installed (\`npm install -g wrangler\`)
- Cloudflare account configured (\`wrangler login\`)
`;
  }

  // Environment variables section
  readme += `
## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| \`PORT\` | 3000 | Application port |
| \`NODE_ENV\` | development | Environment mode |
`;

  if (deploymentTarget === 'node' && redisSetup !== 'none') {
    readme += `| \`REDIS_HOST\` | localhost | Redis host (use \`redis\` in Docker) |
| \`REDIS_PORT\` | 6379 | Redis port |
| \`REDIS_PASSWORD\` | - | Redis password (set in production) |
`;
  }

  // GitHub Actions section
  if (enableGitHubActions) {
    readme += `
## CI/CD

This project includes GitHub Actions workflows:

- **ci.yml**: Runs on every push/PR - type checking and tests
- **e2e.yml**: Runs E2E tests
- **deploy.yml**: Deploys to ${
      deploymentTarget === 'node'
        ? 'GitHub Container Registry'
        : deploymentTarget === 'vercel'
          ? 'Vercel'
          : deploymentTarget === 'lambda'
            ? 'AWS Lambda'
            : 'Cloudflare Workers'
    }

### Required Secrets
`;

    if (deploymentTarget === 'vercel') {
      readme += `
- \`VERCEL_TOKEN\`: Vercel API token
- \`VERCEL_ORG_ID\`: Vercel organization ID
- \`VERCEL_PROJECT_ID\`: Vercel project ID
`;
    } else if (deploymentTarget === 'lambda') {
      readme += `
- \`AWS_ACCESS_KEY_ID\`: AWS access key
- \`AWS_SECRET_ACCESS_KEY\`: AWS secret key
- \`AWS_REGION\`: AWS region (e.g., us-east-1)
`;
    } else if (deploymentTarget === 'cloudflare') {
      readme += `
- \`CLOUDFLARE_API_TOKEN\`: Cloudflare API token with Workers permissions
`;
    } else {
      readme += `
No additional secrets required - uses \`GITHUB_TOKEN\` for GHCR.
`;
    }
  }

  readme += `
## Scripts

| Script | Description |
|--------|-------------|
| \`${cfg.run} dev\` | Start development server with hot reload |
| \`${cfg.run} build\` | Build for production |
| \`${cfg.run} inspect\` | Launch MCP Inspector |
| \`${cfg.run} doctor\` | Check project configuration |
| \`${cfg.run} test\` | Run E2E tests |
`;

  if (deploymentTarget === 'node') {
    readme += `| \`${cfg.run} docker:up\` | Start Docker services |
| \`${cfg.run} docker:down\` | Stop Docker services |
| \`${cfg.run} docker:build\` | Rebuild Docker image |
`;
  }

  if (deploymentTarget === 'lambda' || deploymentTarget === 'cloudflare') {
    readme += `| \`${cfg.run} deploy\` | Deploy to ${deploymentTarget === 'lambda' ? 'AWS Lambda' : 'Cloudflare Workers'} |
`;
  }

  readme += `
## Project Structure

\`\`\`
`;

  // Dynamic project structure based on options
  readme += `├── .env.example       # Environment variables template
├── .gitignore         # Git ignore rules
`;

  if (deploymentTarget === 'node') {
    readme += `├── .dockerignore      # Docker build context exclusions
├── ci/
│   ├── Dockerfile         # Container build config
│   ├── docker-compose.yml # Docker services config
│   └── .env.docker        # Docker-specific env vars
`;
  }

  if (deploymentTarget === 'vercel') {
    readme += `├── vercel.json        # Vercel deployment config
`;
  }

  if (deploymentTarget === 'lambda') {
    readme += `├── ci/
│   └── template.yaml      # AWS SAM template
`;
  }

  if (deploymentTarget === 'cloudflare') {
    readme += `├── wrangler.toml      # Cloudflare Workers config
`;
  }

  if (enableGitHubActions) {
    readme += `├── .github/workflows/
│   ├── ci.yml             # CI workflow
│   ├── e2e.yml            # E2E test workflow
│   └── deploy.yml         # Deployment workflow
`;
  }

  readme += `├── e2e/               # E2E tests
├── package.json       # Dependencies and scripts
├── src/
│   ├── main.ts        # Server entry point
│   ├── calc.app.ts    # Example app
│   └── tools/
│       └── add.tool.ts # Example tool
└── tsconfig.json      # TypeScript config
\`\`\`

## Scaling to a Monorepo

To migrate to an Nx monorepo with multiple apps and shared libraries:

\`\`\`bash
npx frontmcp create my-workspace --nx
\`\`\`

This scaffolds an Nx workspace with generators for tools, resources, prompts, and more.
See the [FrontMCP Nx Plugin docs](https://docs.agentfront.dev) for details.

## Learn More

- [FrontMCP Documentation](https://docs.agentfront.dev)
- [MCP Specification](https://modelcontextprotocol.io)
`;

  return readme;
}

// =============================================================================
// Scaffolding Functions
// =============================================================================

function getDefaults(projectArg?: string): CreateOptions {
  return {
    projectName: projectArg || 'frontmcp-app',
    deploymentTarget: 'node',
    redisSetup: 'docker',
    enableGitHubActions: true,
    packageManager: 'npm',
  };
}

function getInstallCommand(pm: PackageManager): { cmd: string; args: string[] } {
  switch (pm) {
    case 'npm':
      return { cmd: 'npm', args: ['install'] };
    case 'yarn':
      return { cmd: 'yarn', args: ['install'] };
    case 'pnpm':
      return { cmd: 'pnpm', args: ['install'] };
  }
}

async function scaffoldNxWorkspace(projectName: string, flags?: CreateFlags): Promise<void> {
  const pm = flags?.pm ?? 'npm';
  const folder = sanitizeForFolder(projectName);
  const projectDir = path.resolve(process.cwd(), folder);

  // Validate target directory
  try {
    const s = await stat(projectDir);
    if (!s.isDirectory()) {
      console.error(
        c('red', `Refusing to scaffold into non-directory path: ${path.relative(process.cwd(), projectDir)}`),
      );
      console.log(c('gray', 'Pick a different project name or remove/rename the existing file.'));
      process.exit(1);
    }
    if (!(await isDirEmpty(projectDir))) {
      console.error(
        c('red', `Refusing to scaffold into non-empty directory: ${path.relative(process.cwd(), projectDir)}`),
      );
      console.log(c('gray', 'Pick a different name or start with an empty folder.'));
      process.exit(1);
    }
  } catch (e: unknown) {
    if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
      await ensureDir(projectDir);
    } else {
      throw e;
    }
  }

  try {
    // Step 1: Write bootstrap package.json with nx tooling
    const selfVersion = getSelfVersion();
    await writeJSON(path.join(projectDir, 'package.json'), {
      name: folder,
      version: '0.0.1',
      private: true,
      devDependencies: {
        nx: '22.3.3',
        '@nx/devkit': '22.3.3',
        '@frontmcp/nx': `~${selfVersion}`,
      },
    });

    // Step 2: Install nx tooling
    console.log(c('cyan', `\nInstalling Nx tooling in ./${folder}...\n`));
    const install = getInstallCommand(pm);
    await runCmd(install.cmd, install.args, { cwd: projectDir });

    // Step 3: Load nx and @frontmcp/nx from the installed location
    const localRequire = createRequire(path.join(projectDir, 'package.json'));
    const { FsTree, flushChanges } = localRequire('nx/src/generators/tree.js');
    const { workspaceGenerator } = localRequire('@frontmcp/nx');

    // Step 4: Run the workspace generator
    console.log(c('cyan', `\nScaffolding Nx monorepo: ${projectName}...\n`));
    const tree = new FsTree(process.cwd(), false);
    const callback = await workspaceGenerator(tree, {
      name: folder,
      packageManager: pm,
      skipInstall: true,
      skipGit: false,
      createSampleApp: true,
    });

    flushChanges(tree.root, tree.listChanges());

    if (callback) {
      await callback();
    }

    // Step 5: Install full project dependencies
    console.log(c('cyan', '\nInstalling project dependencies...\n'));
    await runCmd(install.cmd, install.args, { cwd: projectDir });

    console.log(c('green', `\n✅ Nx monorepo created at ./${folder}\n`));
    console.log(c('dim', 'Next steps:'));
    console.log(c('dim', `  cd ${folder}`));
    console.log(c('dim', '  nx g @frontmcp/nx:app my-app      # Add an app'));
    console.log(c('dim', '  nx g @frontmcp/nx:lib my-lib       # Add a library'));
    console.log(c('dim', '  nx g @frontmcp/nx:tool my-tool     # Add a tool to an app'));
    console.log(c('dim', '  nx dev demo                        # Start dev server'));
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : String(err);
    if (
      message.includes('Cannot find module') ||
      message.includes('Cannot find package') ||
      message.includes('MODULE_NOT_FOUND')
    ) {
      console.error(c('red', '\nFailed to install Nx tooling.'));
      console.log(c('dim', 'Check your network connection and try again.'));
      console.log(c('dim', 'You can also install manually:'));
      console.log(c('bold', `  cd ${folder} && ${pm} install`));
    } else {
      console.error(c('red', `\nFailed to scaffold Nx workspace: ${message}`));
    }
    process.exit(1);
  }
}

async function collectOptions(projectArg?: string, flags?: CreateFlags): Promise<CreateOptions> {
  const p = await clack();

  // Project name
  let projectName = projectArg;
  if (!projectName) {
    const result = await p.text({
      message: 'Project name',
      validate: (val) => {
        if (!val.trim()) return 'Project name is required';
        return undefined;
      },
    });
    if (p.isCancel(result)) {
      p.cancel('Cancelled.');
      process.exit(0);
    }
    projectName = result;
  }

  if (!flags?.yes) {
    const projectType = await p.select({
      message: 'Project type',
      options: [
        { label: 'Standalone project (recommended)', value: 'standalone' as const },
        { label: 'Nx monorepo (advanced)', value: 'nx' as const },
      ],
      initialValue: 'standalone' as const,
    });
    if (p.isCancel(projectType)) {
      p.cancel('Cancelled.');
      process.exit(0);
    }
    if (projectType === 'nx') {
      await scaffoldNxWorkspace(projectName, flags);
      return {
        projectName,
        deploymentTarget: 'node',
        redisSetup: 'none',
        enableGitHubActions: false,
        packageManager: flags?.pm ?? 'npm',
        nxScaffolded: true,
      };
    }
  }

  // Deployment target
  let deploymentTarget = flags?.target;
  if (!deploymentTarget) {
    const result = await p.select({
      message: 'Select deployment target',
      options: [
        { label: 'Node.js (Docker) - Recommended for production', value: 'node' as DeploymentTarget },
        { label: 'Vercel (Serverless)', value: 'vercel' as DeploymentTarget },
        { label: 'AWS Lambda', value: 'lambda' as DeploymentTarget },
        { label: 'Cloudflare Workers', value: 'cloudflare' as DeploymentTarget },
      ],
      initialValue: 'node' as DeploymentTarget,
    });
    if (p.isCancel(result)) {
      p.cancel('Cancelled.');
      process.exit(0);
    }
    deploymentTarget = result;
  }

  // Redis setup (only for Node.js/Docker)
  let redisSetup: RedisSetup = 'none';
  if (deploymentTarget === 'node') {
    if (flags?.redis) {
      redisSetup = flags.redis;
    } else {
      const result = await p.select({
        message: 'Redis setup',
        options: [
          { label: 'Docker Compose (recommended for development)', value: 'docker' as RedisSetup },
          { label: 'Existing Redis (I have my own Redis)', value: 'existing' as RedisSetup },
          { label: 'None (skip Redis)', value: 'none' as RedisSetup },
        ],
        initialValue: 'docker' as RedisSetup,
      });
      if (p.isCancel(result)) {
        p.cancel('Cancelled.');
        process.exit(0);
      }
      redisSetup = result;
    }
  }

  // Package manager
  let packageManager = flags?.pm;
  if (!packageManager) {
    const result = await p.select({
      message: 'Package manager',
      options: [
        { label: 'npm (default)', value: 'npm' as PackageManager },
        { label: 'yarn', value: 'yarn' as PackageManager },
        { label: 'pnpm', value: 'pnpm' as PackageManager },
      ],
      initialValue: 'npm' as PackageManager,
    });
    if (p.isCancel(result)) {
      p.cancel('Cancelled.');
      process.exit(0);
    }
    packageManager = result;
  }

  // GitHub Actions
  let enableGitHubActions = flags?.cicd;
  if (enableGitHubActions === undefined) {
    const result = await p.confirm({
      message: 'Set up GitHub Actions CI/CD?',
      initialValue: true,
    });
    if (p.isCancel(result)) {
      p.cancel('Cancelled.');
      process.exit(0);
    }
    enableGitHubActions = result;
  }

  return {
    projectName,
    deploymentTarget,
    redisSetup,
    enableGitHubActions,
    packageManager,
  };
}

async function loadSkillEntriesForClaudeMd(
  options: Pick<CreateOptions, 'deploymentTarget' | 'skillsBundle'>,
): Promise<{ name: string; description: string }[]> {
  const bundle = options.skillsBundle ?? 'recommended';
  if (bundle === 'none') return [];

  try {
    const catalogDir = path.resolve(__dirname, '..', '..', '..', '..', 'skills', 'catalog');
    const manifestPath = path.join(catalogDir, 'skills-manifest.json');

    let manifestContent: string;
    if (await fileExists(manifestPath)) {
      manifestContent = await readFile(manifestPath);
    } else {
      const require_ = createRequire(__filename);
      const pkgManifest = require_.resolve('@frontmcp/skills/catalog/skills-manifest.json');
      manifestContent = await readFile(pkgManifest);
    }
    const manifest = JSON.parse(manifestContent) as SkillManifest;
    const target = options.deploymentTarget;
    return manifest.skills
      .filter((s) => {
        const targetMatch = s.targets.includes('all') || s.targets.includes(target);
        const bundleMatch = s.bundle?.includes(bundle);
        return targetMatch && bundleMatch;
      })
      .map((s) => ({ name: s.name, description: s.description }));
  } catch {
    return [];
  }
}

async function scaffoldSkills(targetDir: string, options: CreateOptions): Promise<void> {
  const bundle = options.skillsBundle ?? 'recommended';
  if (bundle === 'none') return;

  let manifest: SkillManifest;
  try {
    const catalogDir = path.resolve(__dirname, '..', '..', '..', '..', 'skills', 'catalog');
    const manifestPath = path.join(catalogDir, 'skills-manifest.json');

    // Try bundled catalog first, then fallback to @frontmcp/skills package
    let manifestContent: string;
    if (await fileExists(manifestPath)) {
      manifestContent = await readFile(manifestPath);
    } else {
      try {
        const require_ = createRequire(__filename);
        const pkgManifest = require_.resolve('@frontmcp/skills/catalog/skills-manifest.json');
        manifestContent = await readFile(pkgManifest);
      } catch {
        // Skills catalog not available — skip silently
        return;
      }
    }
    manifest = JSON.parse(manifestContent) as SkillManifest;
  } catch {
    return;
  }

  const target = options.deploymentTarget;

  // Filter skills by target and bundle
  const matchingSkills = manifest.skills.filter((s) => {
    const targetMatch = s.targets.includes('all') || s.targets.includes(target);
    const bundleMatch = s.bundle?.includes(bundle);
    return targetMatch && bundleMatch;
  });

  if (matchingSkills.length === 0) return;

  const skillsDir = path.join(targetDir, 'skills');

  for (const skill of matchingSkills) {
    const skillTargetDir = path.join(skillsDir, skill.name);
    await ensureDir(skillTargetDir);

    // Resolve source skill directory
    let sourceDir: string | undefined;
    const bundledSource = path.resolve(__dirname, '..', '..', '..', '..', 'skills', 'catalog', skill.path);
    if (await fileExists(path.join(bundledSource, 'SKILL.md'))) {
      sourceDir = bundledSource;
    } else {
      try {
        const require_ = createRequire(__filename);
        const pkgCatalog = path.dirname(require_.resolve('@frontmcp/skills/catalog/skills-manifest.json'));
        const pkgSource = path.join(pkgCatalog, skill.path);
        if (await fileExists(path.join(pkgSource, 'SKILL.md'))) {
          sourceDir = pkgSource;
        }
      } catch {
        // Package not available
      }
    }

    if (!sourceDir) continue;

    // Copy SKILL.md (binary-safe)
    const skillMdSrc = path.join(sourceDir, 'SKILL.md');
    if (await fileExists(skillMdSrc)) {
      await copyFile(skillMdSrc, path.join(skillTargetDir, 'SKILL.md'));
    }

    // Copy resource directories if present (binary-safe recursive copy)
    if (skill.hasResources) {
      for (const resDir of ['scripts', 'references', 'assets']) {
        const srcRes = path.join(sourceDir, resDir);
        if (await fileExists(srcRes)) {
          await cp(srcRes, path.join(skillTargetDir, resDir), { recursive: true });
        }
      }
    }

    console.log(c('green', `✓ added skill: ${skill.name}`));
  }

  console.log(c('gray', `  ${matchingSkills.length} skills added (bundle: ${bundle})`));
}

async function scaffoldDeploymentFiles(targetDir: string, options: CreateOptions): Promise<void> {
  const { deploymentTarget, redisSetup, projectName } = options;

  switch (deploymentTarget) {
    case 'node': {
      const ciDir = path.join(targetDir, 'ci');
      await ensureDir(ciDir);
      await scaffoldFileIfMissing(
        targetDir,
        path.join(ciDir, 'Dockerfile'),
        generateDockerfile(options.packageManager),
      );

      const dockerCompose = redisSetup === 'docker' ? generateDockerComposeWithRedis() : generateDockerComposeNoRedis();
      await scaffoldFileIfMissing(targetDir, path.join(ciDir, 'docker-compose.yml'), dockerCompose);
      await scaffoldFileIfMissing(targetDir, path.join(ciDir, '.env.docker'), TEMPLATE_ENV_DOCKER_CI);
      await scaffoldFileIfMissing(targetDir, path.join(targetDir, '.dockerignore'), TEMPLATE_DOCKERIGNORE);
      break;
    }

    case 'vercel':
      await scaffoldFileIfMissing(
        targetDir,
        path.join(targetDir, 'vercel.json'),
        TEMPLATE_VERCEL_JSON(sanitizeForFolder(projectName)),
      );
      break;

    case 'lambda': {
      const ciDir = path.join(targetDir, 'ci');
      await ensureDir(ciDir);
      await scaffoldFileIfMissing(targetDir, path.join(ciDir, 'template.yaml'), TEMPLATE_SAM_YAML(projectName));
      break;
    }

    case 'cloudflare':
      await scaffoldFileIfMissing(
        targetDir,
        path.join(targetDir, 'wrangler.toml'),
        TEMPLATE_WRANGLER_TOML(sanitizeForFolder(projectName)),
      );
      break;
  }

  // Always create .env.example at root
  const envExample =
    deploymentTarget === 'node' && redisSetup !== 'none' ? TEMPLATE_ENV_EXAMPLE : TEMPLATE_ENV_EXAMPLE_BASIC;
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, '.env.example'), envExample);
}

// Basic .env.example without Redis
const TEMPLATE_ENV_EXAMPLE_BASIC = `
# Application
PORT=3000
NODE_ENV=development
`;

async function scaffoldGitHubActions(
  targetDir: string,
  deploymentTarget: DeploymentTarget,
  pm: PackageManager,
): Promise<void> {
  const workflowDir = path.join(targetDir, '.github', 'workflows');
  await ensureDir(workflowDir);

  // Always create CI and E2E workflows
  await scaffoldFileIfMissing(targetDir, path.join(workflowDir, 'ci.yml'), generateGhCi(pm));
  await scaffoldFileIfMissing(targetDir, path.join(workflowDir, 'e2e.yml'), generateGhE2e(pm));

  // Create deployment workflow based on target
  const deployTemplate = getDeployWorkflowTemplate(deploymentTarget, pm);
  await scaffoldFileIfMissing(targetDir, path.join(workflowDir, 'deploy.yml'), deployTemplate);
}

function getDeployWorkflowTemplate(target: DeploymentTarget, pm: PackageManager): string {
  switch (target) {
    case 'node':
      return TEMPLATE_GH_DEPLOY_DOCKER;
    case 'vercel':
      return generateGhDeployVercel(pm);
    case 'lambda':
      return generateGhDeployLambda(pm);
    case 'cloudflare':
      return generateGhDeployCloudflare(pm);
  }
}

async function scaffoldProject(options: CreateOptions): Promise<void> {
  const { projectName, deploymentTarget, redisSetup, enableGitHubActions, packageManager } = options;

  const folder = sanitizeForFolder(projectName);
  const pkgName = sanitizeForNpm(projectName);
  const targetDir = path.resolve(process.cwd(), folder);

  // Validate directory
  try {
    const targetStat = await stat(targetDir);
    if (!targetStat.isDirectory()) {
      console.error(
        c('red', `Refusing to scaffold into non-directory path: ${path.relative(process.cwd(), targetDir)}`),
      );
      console.log(c('gray', 'Pick a different project name or remove/rename the existing file.'));
      process.exit(1);
    }
    if (!(await isDirEmpty(targetDir))) {
      console.error(
        c('red', `Refusing to scaffold into non-empty directory: ${path.relative(process.cwd(), targetDir)}`),
      );
      console.log(c('gray', 'Pick a different name or start with an empty folder.'));
      process.exit(1);
    }
  } catch (e: unknown) {
    if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
      await ensureDir(targetDir);
    } else {
      throw e;
    }
  }

  console.log(`\n${c('cyan', '[create]')} Creating project in ${c('bold', './' + folder)}`);
  console.log(c('gray', `  Deployment: ${deploymentTarget}`));
  if (deploymentTarget === 'node') {
    console.log(c('gray', `  Redis: ${redisSetup}`));
  }
  console.log(c('gray', `  Package manager: ${packageManager}`));
  console.log(c('gray', `  GitHub Actions: ${enableGitHubActions ? 'Yes' : 'No'}`));
  console.log('');

  process.chdir(targetDir);

  // Initialize tsconfig
  await runInit(targetDir);

  // Create package.json with deployment-specific scripts
  const selfVersion = getSelfVersion();
  await upsertPackageJsonWithTarget(targetDir, pkgName, selfVersion, deploymentTarget, packageManager);

  // Scaffold base files
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);

  // Issue #400 — emit a starter frontmcp.config.ts so subsequent CLI runs
  // pick up entry / port / env / client snippets without re-typing them.
  await scaffoldFileIfMissing(
    targetDir,
    path.join(targetDir, 'frontmcp.config.ts'),
    renderFrontmcpConfigTemplate(projectName, deploymentTarget),
  );

  // E2E scaffolding (jest config auto-generated by `frontmcp test`)
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'e2e', 'server.e2e.spec.ts'), TEMPLATE_E2E_TEST_TS);

  // Skills scaffolding
  await scaffoldSkills(targetDir, options);

  // Git configuration
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, '.gitignore'), TEMPLATE_GITIGNORE);

  // Node version
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, '.nvmrc'), '24\n');

  // Deployment-specific files
  await scaffoldDeploymentFiles(targetDir, options);

  // GitHub Actions
  if (enableGitHubActions) {
    await scaffoldGitHubActions(targetDir, deploymentTarget, packageManager);
  }

  // Dynamic README
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'README.md'), generateReadme(options));

  // CLAUDE.md and AGENTS.md for AI coding assistants
  const skillEntries = await loadSkillEntriesForClaudeMd(options);
  await scaffoldFileIfMissing(
    targetDir,
    path.join(targetDir, 'CLAUDE.md'),
    generateClaudeMd(pkgName, packageManager, skillEntries),
  );
  await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'AGENTS.md'), generateAgentsMd(pkgName, packageManager));

  // Initialize git repository
  try {
    const { execSync } = await import('child_process');
    execSync('git init', { cwd: targetDir, stdio: 'ignore' });
    execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
    execSync('git commit -m "Initial commit"', { cwd: targetDir, stdio: 'ignore' });
    console.log(`${c('green', '✓')} Initialized git repository`);
  } catch {
    // git may not be installed — silently skip
  }

  // Print next steps
  printNextSteps(folder, deploymentTarget, redisSetup, enableGitHubActions, packageManager);
}

function printNextSteps(
  folder: string,
  deploymentTarget: DeploymentTarget,
  redisSetup: RedisSetup,
  enableGitHubActions: boolean,
  pm: PackageManager,
): void {
  const cfg = PM_CONFIG[pm];
  console.log('\nNext steps:');
  console.log(`  1) cd ${folder}`);
  console.log(`  2) ${cfg.userInstall}`);
  console.log(`  3) ${cfg.run} dev      `, c('gray', '# tsx watcher + async tsc type-check'));
  console.log(`  4) ${cfg.run} inspect  `, c('gray', '# launch MCP Inspector'));
  console.log(`  5) ${cfg.run} build    `, c('gray', '# compile with tsc via frontmcp build'));
  console.log(`  6) ${cfg.run} test     `, c('gray', '# run E2E tests'));

  if (deploymentTarget === 'node') {
    console.log('');
    console.log(c('cyan', 'Docker:'));
    console.log(
      `  ${cfg.run} docker:up   `,
      c('gray', `# start${redisSetup === 'docker' ? ' Redis +' : ''} app in Docker`),
    );
    console.log(`  ${cfg.run} docker:down `, c('gray', '# stop Docker services'));
  }

  if (deploymentTarget === 'vercel') {
    console.log('');
    console.log(c('cyan', 'Deploy to Vercel:'));
    console.log('  npx vercel          ', c('gray', '# deploy to Vercel'));
  }

  if (deploymentTarget === 'lambda') {
    console.log('');
    console.log(c('cyan', 'Deploy to AWS Lambda:'));
    console.log(`  ${cfg.run} deploy      `, c('gray', '# deploy with SAM'));
  }

  if (deploymentTarget === 'cloudflare') {
    console.log('');
    console.log(c('cyan', 'Deploy to Cloudflare:'));
    console.log(`  ${cfg.run} deploy      `, c('gray', '# deploy with Wrangler'));
  }

  if (enableGitHubActions) {
    console.log('');
    console.log(c('cyan', 'GitHub Actions:'));
    console.log('  .github/workflows/  ', c('gray', '# CI, E2E, and deploy workflows ready'));
  }
}

// =============================================================================
// Package.json with Target-Specific Scripts
// =============================================================================

async function upsertPackageJsonWithTarget(
  cwd: string,
  nameOverride: string | undefined,
  selfVersion: string,
  deploymentTarget: DeploymentTarget,
  pm: PackageManager = 'npm',
) {
  const pkgPath = path.join(cwd, 'package.json');
  const existing = await readJSON<PackageJson>(pkgPath);

  const frontmcpLibRange = `~${selfVersion}`;

  const baseScripts: Record<string, string> = {
    dev: 'frontmcp dev',
    build: 'frontmcp build',
    inspect: 'frontmcp inspector',
    doctor: 'frontmcp doctor',
    test: 'frontmcp test',
  };

  // Add target-specific scripts
  if (deploymentTarget === 'node') {
    baseScripts['docker:up'] = 'docker compose -f ci/docker-compose.yml up';
    baseScripts['docker:down'] = 'docker compose -f ci/docker-compose.yml down';
    baseScripts['docker:build'] = 'docker compose -f ci/docker-compose.yml build';
  }

  if (deploymentTarget === 'lambda') {
    baseScripts['deploy'] = 'cd ci && sam build && sam deploy';
  }

  if (deploymentTarget === 'cloudflare') {
    baseScripts['deploy'] = 'wrangler deploy';
  }

  const base = {
    name: nameOverride ?? pkgNameFromCwd(cwd),
    version: '0.1.0',
    private: true,
    type: 'commonjs',
    main: 'src/main.ts',
    scripts: baseScripts,
    engines: PM_CONFIG[pm].engines,
    dependencies: {
      '@frontmcp/sdk': frontmcpLibRange,
      '@frontmcp/adapters': frontmcpLibRange,
      frontmcp: selfVersion,
      tslib: '^2.5.0',
      zod: '^4.0.0',
      'reflect-metadata': '^0.2.2',
    },
    devDependencies: {
      '@frontmcp/testing': frontmcpLibRange,
      '@swc/core': '^1.11.29',
      '@swc/helpers': '^0.5.20',
      '@swc/jest': '^0.2.37',
      jest: '^29.7.0',
      '@types/jest': '^29.5.14',
      tsx: '^4.20.6',
      '@types/node': '^24.0.0',
      typescript: '^5.5.3',
    },
  };

  if (!existing) {
    await writeJSON(pkgPath, base);
    console.log(c('green', '✅ Created package.json (synced @frontmcp libs to CLI version + exact frontmcp)'));
    return;
  }

  const merged: PackageJson = { ...base, ...existing };

  merged.name = existing.name || base.name;
  merged.main = existing.main || base.main;
  merged.type = existing.type || base.type;

  // Preserve user scripts, add base scripts only if missing
  merged.scripts = {
    ...baseScripts,
    ...(existing.scripts || {}),
  };

  merged.engines = {
    ...(existing.engines || {}),
    ...base.engines,
  };

  merged.dependencies = {
    ...(existing.dependencies || {}),
    ...base.dependencies,
  };

  merged.devDependencies = {
    ...(existing.devDependencies || {}),
    ...base.devDependencies,
  };

  await writeJSON(pkgPath, merged);
  console.log(c('green', '✅ Updated package.json (synced @frontmcp libs + frontmcp to current CLI version)'));
}

// =============================================================================
// Main Entry Point
// =============================================================================

export async function runCreate(projectArg?: string, flags?: CreateFlags): Promise<void> {
  // Nx monorepo mode (non-interactive with --nx flag)
  if (flags?.nx) {
    const name = projectArg || 'frontmcp-app';
    await scaffoldNxWorkspace(name, flags);
    return;
  }

  // Non-interactive mode: use --yes flag or non-TTY environment
  if (flags?.yes || !isInteractive()) {
    const options = getDefaults(projectArg);
    // Override defaults with any provided flags
    if (flags?.target) options.deploymentTarget = flags.target;
    if (flags?.redis) options.redisSetup = flags.redis;
    if (flags?.cicd !== undefined) options.enableGitHubActions = flags.cicd;
    if (flags?.pm) options.packageManager = flags.pm;
    if (flags?.skills) options.skillsBundle = flags.skills;
    if (projectArg) options.projectName = projectArg;

    if (!options.projectName) {
      console.error(c('red', 'Error: project name is required in non-interactive mode.\n'));
      console.log(`Usage: ${c('bold', 'npx frontmcp create <project-name> --yes')}`);
      process.exit(1);
    }

    await scaffoldProject(options);
    return;
  }

  // Interactive mode
  const p = await clack();
  p.intro('Create a new FrontMCP project');

  const options = await collectOptions(projectArg, flags);
  if (!options.nxScaffolded) await scaffoldProject(options);

  p.outro('Done!');
}
