#!/usr/bin/env node
/**
 * AGENTS.md Auto-Generator
 *
 * Generates AGENTS.md from product.json + repo directory scan.
 * Also creates/updates symlinks: CLAUDE.md, .cursorrules, .windsurfrules
 *
 * Usage:
 *   npx tsx agents-md.ts --repo /path/to/product-repo
 *   npx tsx agents-md.ts --repo /path/to/product-repo --dry-run
 *   npx tsx agents-md.ts --repo /path/to/product-repo --update   # preserves <!-- CUSTOM --> sections
 *
 * @module @bytelyst/create-app/generators/agents-md
 */

/* eslint-disable no-console -- This generator is a CLI; console output is its user interface. */

import { promises as fs } from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';

// ── CLI ──────────────────────────────────────────────────────────────────────

interface Options {
  repo: string;
  dryRun: boolean;
  update: boolean;
}

function parseArgs(): Options {
  const args = process.argv.slice(2);
  const options: Options = { repo: '.', dryRun: false, update: false };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--repo' || arg === '-r') options.repo = args[++i];
    else if (arg === '--dry-run' || arg === '-d') options.dryRun = true;
    else if (arg === '--update' || arg === '-u') options.update = true;
    else if (arg === '--help' || arg === '-h') {
      showHelp();
      process.exit(0);
    }
  }

  return options;
}

function showHelp(): void {
  console.log(`
AGENTS.md Auto-Generator

Generates AGENTS.md from product.json + repo scan, plus symlinks for
CLAUDE.md, .cursorrules, and .windsurfrules.

Usage:
  npx tsx agents-md.ts --repo <path> [--dry-run] [--update]

Options:
  --repo, -r       Path to product repo root (default: ".")
  --dry-run, -d    Preview without writing files
  --update, -u     Preserve <!-- CUSTOM --> sections in existing AGENTS.md
  --help, -h       Show this help

Custom Sections:
  Wrap any hand-written content in <!-- CUSTOM:key --> / <!-- /CUSTOM:key -->
  markers. The --update flag preserves these sections when regenerating.
`);
}

// ── Product.json Loader ──────────────────────────────────────────────────────

interface ProductManifest {
  productId: string;
  displayName: string;
  tagline?: string;
  domain?: string;
  backendPort?: number;
  primarySurface?: string;
  mobileCompanion?: boolean;
  bundleIds?: Record<string, string>;
  bundleId?: string;
  version?: string;
  description?: string;
  licensePrefix?: string;
  configDirName?: string;
  envVarPrefix?: string;
}

async function loadProductJson(repoPath: string): Promise<ProductManifest> {
  const candidates = [
    path.join(repoPath, 'shared', 'product.json'),
    path.join(repoPath, 'product.json'),
  ];

  for (const p of candidates) {
    try {
      const raw = await fs.readFile(p, 'utf-8');
      return JSON.parse(raw);
    } catch {
      // try next
    }
  }

  throw new Error('product.json not found in shared/ or repo root');
}

// ── Repo Scanner ─────────────────────────────────────────────────────────────

interface RepoInfo {
  repoName: string;
  hasFastifyBackend: boolean;
  hasNextWeb: boolean;
  hasExpoMobile: boolean;
  hasSwiftIos: boolean;
  hasKotlinAndroid: boolean;
  hasKmpShared: boolean;
  backendModules: string[];
  backendTestCount: number;
  webTestCount: number;
  mobileTestCount: number;
  backendLibFiles: string[];
  webLibFiles: string[];
  cosmosContainers: string[];
  techStack: { layer: string; tech: string }[];
  buildCommands: string[];
}

async function dirExists(p: string): Promise<boolean> {
  try {
    const st = await fs.stat(p);
    return st.isDirectory();
  } catch {
    return false;
  }
}

async function fileExists(p: string): Promise<boolean> {
  try {
    await fs.access(p);
    return true;
  } catch {
    return false;
  }
}

function countTestsInFiles(dir: string): number {
  try {
    const output = execSync(
      `grep -r "\\b\\(it\\|test\\)(" "${dir}" --include="*.test.ts" --include="*.test.tsx" --include="*.spec.ts" 2>/dev/null | wc -l`,
      { encoding: 'utf-8', timeout: 5000 }
    );
    return parseInt(output.trim()) || 0;
  } catch {
    return 0;
  }
}

async function listDirs(dir: string): Promise<string[]> {
  try {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    return entries.filter(e => e.isDirectory()).map(e => e.name);
  } catch {
    return [];
  }
}

async function listFiles(dir: string): Promise<string[]> {
  try {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    return entries.filter(e => e.isFile()).map(e => e.name);
  } catch {
    return [];
  }
}

async function scanRepo(repoPath: string, manifest: ProductManifest): Promise<RepoInfo> {
  const repoName = path.basename(repoPath);

  // Detect surfaces
  const hasFastifyBackend = await dirExists(path.join(repoPath, 'backend', 'src'));
  const hasNextWeb =
    (await fileExists(path.join(repoPath, 'web', 'next.config.ts'))) ||
    (await fileExists(path.join(repoPath, 'web', 'next.config.js'))) ||
    (await fileExists(path.join(repoPath, 'mindlyst-native', 'web', 'next.config.ts')));
  const hasExpoMobile =
    (await fileExists(path.join(repoPath, 'mobile', 'app.json'))) ||
    (await fileExists(path.join(repoPath, 'app.json')));
  const hasSwiftIos = await dirExists(path.join(repoPath, 'ios'));
  const hasKotlinAndroid = await dirExists(path.join(repoPath, 'android'));
  const hasKmpShared = await dirExists(path.join(repoPath, 'shared', 'src'));

  // Backend modules
  const modulesDir = path.join(repoPath, 'backend', 'src', 'modules');
  const backendModules = await listDirs(modulesDir);

  // Backend lib files
  const libDir = path.join(repoPath, 'backend', 'src', 'lib');
  const backendLibFiles = (await listFiles(libDir)).filter(
    f => f.endsWith('.ts') && !f.endsWith('.test.ts')
  );

  // Web lib files
  let webLibDir = path.join(repoPath, 'web', 'src', 'lib');
  if (!(await dirExists(webLibDir))) {
    webLibDir = path.join(repoPath, 'web', 'src', 'components', 'lib');
  }
  const webLibFiles = (await listFiles(webLibDir)).filter(
    f => f.endsWith('.ts') && !f.endsWith('.test.ts')
  );

  // Test counts
  const backendTestCount = hasFastifyBackend
    ? countTestsInFiles(path.join(repoPath, 'backend', 'src'))
    : 0;

  let webTestDir = path.join(repoPath, 'web', 'src');
  if (!(await dirExists(webTestDir))) {
    webTestDir = path.join(repoPath, 'mindlyst-native', 'web', 'src');
  }
  const webTestCount = hasNextWeb ? countTestsInFiles(webTestDir) : 0;

  const mobileTestDir = hasExpoMobile
    ? (await dirExists(path.join(repoPath, 'mobile')))
      ? path.join(repoPath, 'mobile')
      : repoPath
    : '';
  const mobileTestCount = mobileTestDir ? countTestsInFiles(mobileTestDir) : 0;

  // Cosmos containers — scan backend types files
  const cosmosContainers: string[] = [];
  for (const mod of backendModules) {
    const typesFile = path.join(modulesDir, mod, 'types.ts');
    if (await fileExists(typesFile)) {
      cosmosContainers.push(mod.replace(/-/g, '_'));
    }
  }

  // Tech stack
  const techStack: { layer: string; tech: string }[] = [];
  if (hasFastifyBackend)
    techStack.push({
      layer: 'Backend',
      tech: `Fastify 5, TypeScript ESM, Zod, jose (JWT), @bytelyst/datastore`,
    });
  if (hasNextWeb)
    techStack.push({ layer: 'Web', tech: 'Next.js 16 (App Router), React 19, TypeScript' });
  if (hasExpoMobile)
    techStack.push({ layer: 'Mobile', tech: 'React Native (Expo), TypeScript, expo-router' });
  if (hasSwiftIos) techStack.push({ layer: 'iOS', tech: 'SwiftUI (iOS 17+)' });
  if (hasKotlinAndroid)
    techStack.push({ layer: 'Android', tech: 'Jetpack Compose, Material 3, Kotlin' });
  if (hasKmpShared) techStack.push({ layer: 'Shared', tech: 'Kotlin Multiplatform (KMP)' });
  techStack.push({
    layer: 'Platform',
    tech: 'platform-service (port 4003) for auth, flags, telemetry, billing',
  });
  techStack.push({
    layer: 'Database',
    tech: `Azure Cosmos DB via @bytelyst/datastore — productId: "${manifest.productId}"`,
  });

  // Build commands
  const buildCommands: string[] = [];
  if (hasFastifyBackend) {
    const port = manifest.backendPort ?? 4000;
    buildCommands.push(`cd backend && npm run dev              # Dev server (port ${port})`);
    buildCommands.push(`cd backend && npm run typecheck        # tsc --noEmit`);
    buildCommands.push(`cd backend && npm test                 # Vitest tests`);
  }
  if (hasNextWeb) {
    buildCommands.push(`cd web && npm run dev                  # Dev server`);
    buildCommands.push(`cd web && npm run typecheck            # tsc --noEmit`);
    buildCommands.push(`cd web && npm run build                # Production build`);
  }
  if (hasExpoMobile) {
    buildCommands.push(`cd mobile && npm start                 # Expo dev server`);
    buildCommands.push(`cd mobile && npm run typecheck         # tsc --noEmit`);
  }

  return {
    repoName,
    hasFastifyBackend,
    hasNextWeb,
    hasExpoMobile,
    hasSwiftIos,
    hasKotlinAndroid,
    hasKmpShared,
    backendModules,
    backendTestCount,
    webTestCount,
    mobileTestCount,
    backendLibFiles,
    webLibFiles,
    cosmosContainers,
    techStack,
    buildCommands,
  };
}

// ── Markdown Generator ───────────────────────────────────────────────────────

function generateAgentsMd(manifest: ProductManifest, info: RepoInfo): string {
  const { productId, displayName, domain, tagline } = manifest;
  const repoDesc = tagline ?? `${displayName} product`;

  const lines: string[] = [];

  // ── Header
  lines.push(`# AGENTS.md — AI Coding Agent Instructions`);
  lines.push('');
  lines.push(
    `> **For:** Claude Code, OpenAI Codex, Cursor, GitHub Copilot, Windsurf Cascade, and any AI coding agent.`
  );
  lines.push(`> **Repo:** \`${info.repoName}\` — ${repoDesc}.`);
  lines.push('');
  lines.push('---');
  lines.push('');

  // ── 1. Project Identity
  lines.push('## 1. Project Identity');
  lines.push('');
  lines.push('| Key | Value |');
  lines.push('|-----|-------|');
  lines.push(`| **Product** | ${displayName} |`);
  lines.push(`| **Product ID** | \`${productId}\` |`);
  if (domain) lines.push(`| **Domain** | ${domain} |`);
  lines.push(`| **Repo** | \`${info.repoName}\` |`);
  lines.push(`| **Ecosystem** | ByteLyst (shares platform-service with other ByteLyst products) |`);
  lines.push('');

  // ── 2. Repo Layout (simplified tree)
  lines.push('## 2. Repo Layout');
  lines.push('');
  lines.push('```');
  lines.push(`${info.repoName}/`);
  if (info.hasFastifyBackend) {
    const port = manifest.backendPort ?? 4000;
    lines.push(
      `├── backend/                          # Fastify 5 + TypeScript ESM backend (port ${port})`
    );
    lines.push(`│   ├── src/`);
    if (info.backendLibFiles.length > 0) {
      lines.push(`│   │   ├── lib/                      # Shared backend wiring`);
      for (const f of info.backendLibFiles.slice(0, 8)) {
        lines.push(`│   │   │   ├── ${f}`);
      }
      if (info.backendLibFiles.length > 8) {
        lines.push(`│   │   │   └── ... (${info.backendLibFiles.length - 8} more)`);
      }
    }
    if (info.backendModules.length > 0) {
      lines.push(`│   │   ├── modules/`);
      for (const m of info.backendModules) {
        lines.push(`│   │   │   ├── ${m}/`);
      }
    }
    lines.push(`│   │   └── server.ts`);
    lines.push(`│   ├── package.json`);
    lines.push(`│   └── tsconfig.json`);
    lines.push('│');
  }
  if (info.hasNextWeb) {
    lines.push(`├── web/                              # Next.js 16 + React 19 (App Router)`);
    lines.push(`│   ├── src/`);
    lines.push(`│   │   ├── app/                      # App Router pages`);
    if (info.webLibFiles.length > 0) {
      lines.push(`│   │   └── lib/                      # Pure TS clients + config`);
    }
    lines.push(`│   ├── package.json`);
    lines.push(`│   └── tsconfig.json`);
    lines.push('│');
  }
  if (info.hasExpoMobile) {
    lines.push(`├── mobile/                           # React Native + Expo`);
    lines.push('│');
  }
  if (info.hasSwiftIos) {
    lines.push(`├── ios/                              # SwiftUI native app`);
    lines.push('│');
  }
  if (info.hasKotlinAndroid) {
    lines.push(`├── android/                          # Jetpack Compose`);
    lines.push('│');
  }
  lines.push(`├── shared/`);
  lines.push(`│   └── product.json                  # Canonical product identity`);
  lines.push(`├── AGENTS.md                         # This file`);
  lines.push(`└── README.md`);
  lines.push('```');
  lines.push('');

  // ── 3. Tech Stack
  lines.push('## 3. Tech Stack');
  lines.push('');
  lines.push('| Layer | Technology |');
  lines.push('|-------|-----------|');
  for (const { layer, tech } of info.techStack) {
    lines.push(`| **${layer}** | ${tech} |`);
  }

  // Test counts
  const totalTests = info.backendTestCount + info.webTestCount + info.mobileTestCount;
  if (totalTests > 0) {
    const parts: string[] = [];
    if (info.backendTestCount > 0) parts.push(`${info.backendTestCount} backend`);
    if (info.webTestCount > 0) parts.push(`${info.webTestCount} web`);
    if (info.mobileTestCount > 0) parts.push(`${info.mobileTestCount} mobile`);
    lines.push(`| **Tests** | Vitest — ~${totalTests} tests (${parts.join(' + ')}) |`);
  }
  lines.push('');

  // ── 4. Coding Conventions
  lines.push('## 4. Coding Conventions');
  lines.push('');
  lines.push('### MUST follow');
  lines.push('');
  lines.push(`- Every Cosmos document MUST include a \`productId: "${productId}"\` field`);
  if (info.hasFastifyBackend) {
    lines.push('- Backend modules follow `types.ts` → `repository.ts` → `routes.ts` pattern');
    lines.push(
      '- All repositories use `@bytelyst/datastore` getCollection() — never direct Cosmos SDK calls'
    );
  }
  if (info.hasNextWeb) {
    lines.push('- Web engine logic in `web/src/lib/` — pure TS, no React imports');
    lines.push('- Web components in `web/src/components/` — React UI only');
  }
  if (info.hasExpoMobile) {
    lines.push('- Mobile engine logic in `mobile/src/lib/` — pure TS, no React Native imports');
  }
  lines.push(
    '- Commit messages: `type(scope): description` — types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`'
  );
  lines.push('');
  lines.push('### MUST NOT do');
  lines.push('');
  lines.push(
    '- Never use `console.log` in production code — use `req.log` or `app.log` in Fastify'
  );
  lines.push('- Never use `any` type — use Zod inference or explicit types');
  lines.push('- Never hardcode colors — use theme tokens');
  lines.push('- Never hardcode API URLs — use env vars or config');
  lines.push(`- Never hardcode product ID — use \`productConfig.productId\``);
  lines.push('- Never modify tests to make them pass — fix the actual code');
  lines.push('- Never delete existing comments or documentation unless explicitly asked');
  lines.push('- Never add emojis to code unless explicitly asked');
  lines.push('');

  // ── 5. Build & Test Commands
  if (info.buildCommands.length > 0) {
    lines.push('## 5. Build & Test Commands');
    lines.push('');
    lines.push('```bash');
    for (const cmd of info.buildCommands) {
      lines.push(cmd);
    }
    lines.push('```');
    lines.push('');
  }

  // ── 6. Backend API Modules (if applicable)
  if (info.hasFastifyBackend && info.backendModules.length > 0) {
    lines.push('## 6. Backend Modules');
    lines.push('');
    lines.push('| Module | Container | Description |');
    lines.push('|--------|-----------|-------------|');
    for (const mod of info.backendModules) {
      const container = mod.replace(/-/g, '_');
      lines.push(`| \`${mod}\` | \`${container}\` | ${mod.replace(/-/g, ' ')} |`);
    }
    lines.push('');
  }

  // ── Custom section placeholder
  lines.push('<!-- CUSTOM:extra -->');
  lines.push('<!-- /CUSTOM:extra -->');
  lines.push('');

  return lines.join('\n');
}

// ── Custom Section Preservation ──────────────────────────────────────────────

function extractCustomSections(content: string): Map<string, string> {
  const sections = new Map<string, string>();
  const regex = /<!-- CUSTOM:(\w+) -->\n([\s\S]*?)<!-- \/CUSTOM:\1 -->/g;
  let match;
  while ((match = regex.exec(content)) !== null) {
    sections.set(match[1], match[2]);
  }
  return sections;
}

function mergeCustomSections(newContent: string, existing: Map<string, string>): string {
  let result = newContent;
  for (const [key, value] of existing) {
    const placeholder = `<!-- CUSTOM:${key} -->\n<!-- /CUSTOM:${key} -->`;
    const replacement = `<!-- CUSTOM:${key} -->\n${value}<!-- /CUSTOM:${key} -->`;
    result = result.replace(placeholder, replacement);
  }
  return result;
}

// ── Symlink Manager ──────────────────────────────────────────────────────────

async function ensureSymlinks(repoPath: string, dryRun: boolean): Promise<void> {
  const targets = ['CLAUDE.md', '.cursorrules', '.windsurfrules'];

  for (const target of targets) {
    const linkPath = path.join(repoPath, target);
    const exists = await fileExists(linkPath);

    if (exists) {
      try {
        const stat = await fs.lstat(linkPath);
        if (stat.isSymbolicLink()) {
          const linkTarget = await fs.readlink(linkPath);
          if (linkTarget === 'AGENTS.md') {
            continue; // already correct
          }
        }
      } catch {
        // not a symlink
      }
    }

    if (dryRun) {
      console.log(`   📝 Would create symlink: ${target} → AGENTS.md`);
    } else {
      try {
        if (exists) await fs.unlink(linkPath);
        await fs.symlink('AGENTS.md', linkPath);
        console.log(`   ✅ ${target} → AGENTS.md`);
      } catch (err) {
        console.log(
          `   ⚠️  Could not create symlink ${target}: ${err instanceof Error ? err.message : String(err)}`
        );
      }
    }
  }
}

// ── Main ─────────────────────────────────────────────────────────────────────

async function main(): Promise<void> {
  const { repo, dryRun, update } = parseArgs();
  const repoPath = path.resolve(repo);

  console.log(`\n📄 AGENTS.md Generator`);
  console.log(`   Repo: ${repoPath}`);
  if (dryRun) console.log('   ⚠️  DRY RUN — no files will be written\n');
  if (update) console.log('   🔄 UPDATE mode — preserving custom sections\n');

  // Load product.json
  const manifest = await loadProductJson(repoPath);
  console.log(`   Product: ${manifest.displayName} (${manifest.productId})`);

  // Scan repo
  const info = await scanRepo(repoPath, manifest);
  console.log(`   Backend modules: ${info.backendModules.length}`);
  console.log(`   Tests: ~${info.backendTestCount + info.webTestCount + info.mobileTestCount}`);
  console.log('');

  // Generate content
  let content = generateAgentsMd(manifest, info);

  // Merge custom sections if updating
  if (update) {
    const agentsPath = path.join(repoPath, 'AGENTS.md');
    try {
      const existing = await fs.readFile(agentsPath, 'utf-8');
      const customSections = extractCustomSections(existing);
      if (customSections.size > 0) {
        console.log(`   🔄 Preserving ${customSections.size} custom section(s)`);
        content = mergeCustomSections(content, customSections);
      }
    } catch {
      console.log('   ℹ️  No existing AGENTS.md to preserve custom sections from');
    }
  }

  if (dryRun) {
    console.log('── AGENTS.md ──────────────────────────────────────');
    console.log(content);
    console.log('\n── Symlinks ──────────────────────────────────────');
    await ensureSymlinks(repoPath, true);
    console.log('\n✨ Dry run complete.');
    return;
  }

  // Write AGENTS.md
  const agentsPath = path.join(repoPath, 'AGENTS.md');
  await fs.writeFile(agentsPath, content, 'utf-8');
  console.log(`   ✅ AGENTS.md written`);

  // Ensure symlinks
  await ensureSymlinks(repoPath, false);

  console.log(`\n✨ AGENTS.md generated for ${manifest.displayName}.`);
}

main().catch(err => {
  console.error('❌ Error:', err instanceof Error ? err.message : String(err));
  process.exit(1);
});
