#!/usr/bin/env node

/**
 * Fullstack Composer Agent — SF-05
 *
 * 组合 frontend-builder-agent (SF-03) + backend-builder-agent (SF-04)，
 * 从同一个 SF-02 RequirementPackage 生成完整可运行的全栈项目。
 *
 * 输出结构：
 *   apps/web/          — Next.js 前端 (SF-03)
 *   apps/api/          — Fastify 后端 (SF-04)
 *   packages/shared-types/  — 前后端共享类型
 *   packages/shared-config/ — 前后端共享配置
 *   根目录 — workspace package.json / README / scripts
 *
 * Usage:
 *   node scripts/fullstack-composer-agent.mjs --prd <path> --arch <path> [options]
 *   node scripts/fullstack-composer-agent.mjs --input-text "<需求>" [options]
 *
 * Options:
 *   --prd <path>          PRD JSON（SF-01 输出）
 *   --arch <path>         Architecture JSON（SF-02 输出）
 *   --input-text <text>   直接传入需求
 *   --output <dir>        输出目录（default: fullstack/）
 *   --verbose             详细输出
 *   --help                显示帮助
 *
 * @module fullstack-composer-agent
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createContract, toCamel, tsType as contractTsType } from "./model-contract.mjs";

const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
const WORKSPACE = resolve(__dirname, "..");
const DEFAULT_OUTPUT = resolve(WORKSPACE, "fullstack");

// ═══════════════════════════════════════════════════════
// 1. Shared Types Generator
// ═══════════════════════════════════════════════════════

function sqliteTypeForTs(field) {
  const t = (field.type || "").toUpperCase();
  if (t.includes("INT") || t.includes("SERIAL") || t.includes("BIGINT") || t.includes("DECIMAL") || t.includes("NUMERIC") || t.includes("FLOAT") || t.includes("DOUBLE") || t.includes("REAL")) return "number";
  if (t.includes("BOOL")) return "boolean";
  return "string";
}

function isRequired(field) {
  const c = (field.constraints || "").toUpperCase();
  return c.includes("NOT NULL") || c.includes("PK");
}

function pascalCase(s) {
  return s.charAt(0).toUpperCase() + s.slice(1).replace(/[-_]([a-zA-Z])/g, (_, c) => c.toUpperCase());
}

/** Words where final "s" is part of the root, not a plural marker. */
const SINGULAR_EXCEPTIONS = new Set([
  "status", "bus", "campus", "focus", "bonus", "virus", "genius",
  "census", "ensus", "consensus", "apparatus", "fetus", "hiatus",
]);

function singularize(s) {
  if (s.endsWith("ies")) return s.slice(0, -3) + "y";
  if (s.endsWith("ses") || s.endsWith("xes") || s.endsWith("ches") || s.endsWith("shes")) return s.slice(0, -2);
  if (s.endsWith("s") && !s.endsWith("ss")) {
    const lastSeg = s.includes("_") ? s.split("_").pop() : s;
    if (SINGULAR_EXCEPTIONS.has(lastSeg)) return s;
    return s.slice(0, -1);
  }
  return s;
}

/**
 * Generate shared types that both frontend and backend can import.
 * Includes entity types + API response wrappers only.
 */
function generateSharedTypes(contract) {
  const entities = contract.entities || [];
  const lines = [
    `// Shared types for fullstack project`,
    `// Auto-generated — used by both apps/web and apps/api`,
    ``,
    `// ─── Entities ────────────────────────────────────────`,
  ];

  const generated = new Set();

  // Include User and all entities from contract
  for (const entity of entities) {
    const name = entity.name;
    if (generated.has(name)) continue;
    generated.add(name);

    lines.push(`export interface ${name} {`);
    for (const f of entity.fields) {
      const fname = toCamel(contract, f.name);
      const optional = f.required ? "" : "?";
      lines.push(`  ${fname}${optional}: ${contractTsType(f)};`);
    }
    lines.push(`}`);
    lines.push(``);
  }

  // ─── API Response Wrappers ──────────────────────────
  lines.push(`// ─── API Response Wrappers ───────────────────────────`);
  lines.push(`export interface ApiResponse<T> {`);
  lines.push(`  data: T;`);
  lines.push(`  message?: string;`);
  lines.push(`}`);
  lines.push(``);
  lines.push(`export interface PaginatedResponse<T> {`);
  lines.push(`  data: T[];`);
  lines.push(`  total: number;`);
  lines.push(`  page: number;`);
  lines.push(`  pageSize: number;`);
  lines.push(`}`);
  lines.push(``);
  lines.push(`export interface ErrorResponse {`);
  lines.push(`  error: string;`);
  lines.push(`  message: string;`);
  lines.push(`  statusCode: number;`);
  lines.push(`}`);
  lines.push(``);

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

// ═══════════════════════════════════════════════════════
// 2. Shared Config Generator
// ═══════════════════════════════════════════════════════

function generateSharedConfig() {
  return {
    "src/index.ts": `// Shared configuration for fullstack project

/** API base URL — reads from env or defaults */
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";

/** API prefix for all endpoints */
export const API_PREFIX = "/api";

/** Full API URL */
export const API_URL = \`\${API_BASE_URL}\${API_PREFIX}\`;

/** Auth token storage key */
export const AUTH_TOKEN_KEY = "auth_token";

/** Default page size for paginated endpoints */
export const DEFAULT_PAGE_SIZE = 20;
`,
    "package.json": JSON.stringify({
      name: "@shared/config",
      version: "0.1.0",
      private: true,
      main: "./src/index.ts",
      types: "./src/index.ts",
    }, null, 2),
    "tsconfig.json": JSON.stringify({
      compilerOptions: {
        target: "ES2022",
        module: "ESNext",
        moduleResolution: "bundler",
        strict: true,
        esModuleInterop: true,
        skipLibCheck: true,
        declaration: true,
        outDir: "./dist",
      },
      include: ["src"],
    }, null, 2),
  };
}

// ═══════════════════════════════════════════════════════
// 3. Shared Types Package Generator
// ═══════════════════════════════════════════════════════

function generateSharedTypesPackage(contract) {
  return {
    "src/index.ts": generateSharedTypes(contract),
    "package.json": JSON.stringify({
      name: "@shared/types",
      version: "0.1.0",
      private: true,
      main: "./src/index.ts",
      types: "./src/index.ts",
    }, null, 2),
    "tsconfig.json": JSON.stringify({
      compilerOptions: {
        target: "ES2022",
        module: "ESNext",
        moduleResolution: "bundler",
        strict: true,
        esModuleInterop: true,
        skipLibCheck: true,
        declaration: true,
        outDir: "./dist",
      },
      include: ["src"],
    }, null, 2),
  };
}

// ═══════════════════════════════════════════════════════
// 4. Root Files Generator
// ═══════════════════════════════════════════════════════

function generateRootFiles(projectName, prdSummary, contract) {
  const safeName = (projectName || "fullstack").toLowerCase().replace(/[^a-z0-9-]/g, "-");

  const entities = contract.entities || [];
  const tableList = entities.map(e => `- **${e.table}** — ${e.description || ""}`).join("\n");

  return {
    "package.json": JSON.stringify({
      name: safeName,
      version: "0.1.0",
      private: true,
      workspaces: [
        "apps/*",
        "packages/*",
      ],
      scripts: {
        dev: "node scripts/dev.mjs",
        build: "node scripts/build.mjs",
        "dev:web": "npm run dev -w apps/web",
        "dev:api": "npm run dev -w apps/api",
        "build:web": "npm run build -w apps/web",
        "build:api": "npm run build -w apps/api",
        test: "npm run test -w apps/api",
        lint: "npm run lint -w apps/web",
      },
    }, null, 2),

    ".env.example": `# ─── API Server ───────────────────────────────────
API_PORT=3001
API_HOST=0.0.0.0
NODE_ENV=development
DATABASE_URL=./data/app.db
JWT_SECRET=change-me-in-production
CORS_ORIGIN=http://localhost:3000

# ─── Web Client ───────────────────────────────────
NEXT_PUBLIC_API_URL=http://localhost:3001
WEB_PORT=3000
`,

    "README.md": `# ${projectName} — Fullstack Project

> ${prdSummary || "Auto-generated fullstack application"}

## Architecture

\`\`\`
apps/
├── web/        # Next.js 15 + TypeScript + Tailwind CSS
└── api/        # Fastify 5 + TypeScript + SQLite (sql.js)

packages/
├── shared-types/   # @shared/types — shared TypeScript interfaces
└── shared-config/  # @shared/config — shared configuration
\`\`\`

## Quick Start

\`\`\`bash
# Install all dependencies (root + workspaces)
npm install

# Start both frontend and backend in dev mode
npm run dev

# Or start individually
npm run dev:web    # http://localhost:3000
npm run dev:api    # http://localhost:3001
\`\`\`

## Build

\`\`\`bash
# Build everything
npm run build

# Or individually
npm run build:web
npm run build:api
\`\`\`

## Testing

\`\`\`bash
# Run API tests
npm test
\`\`\`

## API Endpoints

### Auth
- \`POST /api/auth/register\`
- \`POST /api/auth/login\`
- \`GET  /api/auth/me\`

### Resources
${entities.map(e => {
  const tn = e.table;
  return `#### ${tn}\n- \`GET    /api/${tn}\` — List\n- \`GET    /api/${tn}/:id\` — Get\n- \`POST   /api/${tn}\` — Create\n- \`PUT    /api/${tn}/:id\` — Update\n- \`DELETE /api/${tn}/:id\` — Delete`;
}).join("\n\n")}

### System
- \`GET /api/health\` — Health check

## Environment Variables

See \`.env.example\` for all available configuration.
`,

    ".gitignore": `node_modules/
dist/
.next/
data/
.env
*.db
*.db-journal
*.db-wal
`,
  };
}

// ═══════════════════════════════════════════════════════
// 5. Scripts Generator
// ═══════════════════════════════════════════════════════

function generateScripts() {
  return {
    "scripts/dev.mjs": `#!/usr/bin/env node

/**
 * Dev script — starts both web and api in parallel.
 * Usage: node scripts/dev.mjs
 */

import { spawn } from "node:child_process";

function start(name, command, args, cwd) {
  const child = spawn(command, args, {
    cwd,
    stdio: "inherit",
    shell: true,
    env: { ...process.env, FORCE_COLOR: "1" },
  });
  child.on("error", (err) => console.error(\`[\${name}] Failed: \${err.message}\`));
  child.on("exit", (code) => {
    if (code !== 0 && code !== null) console.error(\`[\${name}] Exited with code \${code}\`);
  });
  return child;
}

const ROOT = new URL("..", import.meta.url).pathname;

console.log("🚀 Starting fullstack dev servers...\\n");

const api = start("api", "npm", ["run", "dev"], \`\${ROOT}/apps/api\`);
const web = start("web", "npm", ["run", "dev"], \`\${ROOT}/apps/web\`);

process.on("SIGINT", () => { api.kill(); web.kill(); process.exit(0); });
process.on("SIGTERM", () => { api.kill(); web.kill(); process.exit(0); });
`,

    "scripts/build.mjs": `#!/usr/bin/env node

/**
 * Build script — builds both web and api.
 * Usage: node scripts/build.mjs
 */

import { execSync } from "node:child_process";

const ROOT = new URL("..", import.meta.url).pathname;

function run(cmd, cwd) {
  console.log(\`\\n🔨 \${cmd} (in \${cwd})\`);
  execSync(cmd, { cwd, stdio: "inherit" });
}

console.log("🏗️  Building fullstack project...\\n");

try {
  run("npm run build", \`\${ROOT}/apps/api\`);
  run("npm run build", \`\${ROOT}/apps/web\`);
  console.log("\\n✅ Build complete!");
} catch (e) {
  console.error("\\n❌ Build failed:", e.message);
  process.exit(1);
}
`,
  };
}

// ═══════════════════════════════════════════════════════
// 6. Frontend Post-Processor
// ═══════════════════════════════════════════════════════

/**
 * Adapt the generated frontend to work in a monorepo with shared packages.
 * - Add @shared/types and @shared/config as dependencies
 * - Update services/api.ts to use @shared/config for API base URL
 */
function postProcessFrontend(files, projectName) {
  const modified = { ...files };
  const baseName = (projectName || "app").toLowerCase().replace(/[^a-z0-9-]/g, "-");

  // ── Update package.json ──
  if (modified["package.json"]) {
    const pkg = JSON.parse(modified["package.json"]);
    pkg.name = `${baseName}-web`;
    pkg.dependencies = pkg.dependencies || {};
    pkg.dependencies["@shared/types"] = "*";
    pkg.dependencies["@shared/config"] = "*";
    modified["package.json"] = JSON.stringify(pkg, null, 2);
  }

  // ── Update services/api.ts (after prefixSrc, located at src/services/api.ts) ──
  if (modified["src/services/api.ts"]) {
    modified["src/services/api.ts"] = modified["src/services/api.ts"].replace(
      /(const API_BASE = .*?;)/s,
      `import { API_BASE_URL, API_PREFIX } from "@shared/config";

const API_BASE = \`\${API_BASE_URL}\${API_PREFIX}\`;`
    );
  }

  // ── Create a shared-types re-export in src/types ──
  // We keep domain-specific types but re-export shared types
  if (modified["src/types/index.ts"]) {
    modified["src/types/index.ts"] = `// Re-export shared types for convenience
export type {
  User,
  ApiResponse,
  PaginatedResponse,
  ErrorResponse,
} from "@shared/types";

${modified["src/types/index.ts"]}
`;
  }

  return modified;
}

// ═══════════════════════════════════════════════════════
// 7. Backend Post-Processor
// ═══════════════════════════════════════════════════════

function postProcessBackend(files, projectName, contract) {
  const modified = { ...files };
  const baseName = (projectName || "app").toLowerCase().replace(/[^a-z0-9-]/g, "-");
  const entities = contract.entities || [];

  // ── Update package.json ──
  if (modified["package.json"]) {
    const pkg = JSON.parse(modified["package.json"]);
    pkg.name = `${baseName}-api`;
    pkg.dependencies = pkg.dependencies || {};
    pkg.dependencies["@shared/types"] = "*";
    pkg.dependencies["@shared/config"] = "*";
    modified["package.json"] = JSON.stringify(pkg, null, 2);
  }

  // ── Rewrite types to use @shared/types for entities & API wrappers ──
  if (modified["src/types/index.ts"]) {
    let content = modified["src/types/index.ts"];

    // Build set of entity type names from contract
    const entityNames = new Set(["User"]);
    for (const entity of entities.filter(e => e.table !== "users")) {
      entityNames.add(entity.name);
    }

    // Remove specific duplicate type declarations (blocks ending with })
    const duplicateTypes = [...entityNames, "ApiResponse", "PaginatedResponse", "ErrorResponse"];
    for (const typeName of duplicateTypes) {
      // Remove `export interface TypeName ... }` including generics
      content = content.replace(
        new RegExp(`export interface ${typeName}(<[^>]*>)?\\s*\\{[^}]*\\}\\n\\n`, "g"),
        ""
      );
    }
    // Clean extra blank lines
    content = content.replace(/\n{3,}/g, "\n\n");

    // Build re-export list dynamically from schema
    const reExports = [...entityNames];
    const reExportLines = reExports.map(n => `  ${n},`).join("\n");

    // Use explicit import + re-export pattern for reliable TS resolution
    const entityImportNames = reExports.join(",\n  ");
    modified["src/types/index.ts"] = `// Import and re-export shared entity types
import type {
  ${entityImportNames},
  ApiResponse,
  PaginatedResponse,
  ErrorResponse,
} from "@shared/types";

export type {
  ${entityImportNames},
  ApiResponse,
  PaginatedResponse,
  ErrorResponse,
};

${content}`;
  }

  return modified;
}

// ═══════════════════════════════════════════════════════
// 8. Main Composer Function
// ═══════════════════════════════════════════════════════

/**
 * Compose a fullstack project from PRD + Architecture.
 *
 * @param {object} prd — SF-01 PRD
 * @param {object} arch — SF-02 Architecture
 * @returns {object} { files, stats, error, message }
 */
export async function composeFullstack(prd, arch) {
  if (!prd || prd.error) {
    return { error: "INVALID_PRD", message: "Invalid or missing PRD", files: {}, stats: {} };
  }
  if (!arch || arch.error) {
    return { error: "INVALID_ARCH", message: "Invalid or missing Architecture", files: {}, stats: {} };
  }

  const projectName = prd.projectName || arch.projectName || "FullstackApp";
  const prdSummary = prd.summary || "";
  const contract = arch.contract || createContract(prd, { databaseSchema: arch.databaseSchema || [], domain: arch.domain });

  // ── Dynamic imports of builder agents ──
  const fbMod = await import("./frontend-builder-agent.mjs");
  const bbMod = await import("./backend-builder-agent.mjs");

  // ── Build frontend ──
  const frontend = fbMod.buildFrontend(prd, arch);
  if (frontend.error) return { error: "FRONTEND_BUILD_FAILED", message: frontend.message, files: {}, stats: {} };

  // ── Build backend ──
  const backend = bbMod.buildBackend(prd, arch);
  if (backend.error) return { error: "BACKEND_BUILD_FAILED", message: backend.message, files: {}, stats: {} };

  // ── Post-process for monorepo ──
  // Prefix frontend paths with src/ for Next.js src directory structure
  const frontendSrc = prefixSrc(frontend.files);
  const webFiles = postProcessFrontend(frontendSrc, projectName);
  const apiFiles = postProcessBackend(backend.files, projectName, contract);

  // ── Shared packages ──
  const sharedTypesPkg = generateSharedTypesPackage(contract);
  const sharedConfigPkg = generateSharedConfig();

  // ── Root files ──
  const rootFiles = generateRootFiles(projectName, prdSummary, contract);

  // ── Scripts ──
  const scripts = generateScripts();

  // ══ Assemble final file tree ══
  const files = {};

  // apps/web/ (frontend)
  for (const [relPath, content] of Object.entries(webFiles)) {
    files[`apps/web/${relPath}`] = content;
  }

  // apps/api/ (backend)
  for (const [relPath, content] of Object.entries(apiFiles)) {
    files[`apps/api/${relPath}`] = content;
  }

  // packages/shared-types/
  for (const [relPath, content] of Object.entries(sharedTypesPkg)) {
    files[`packages/shared-types/${relPath}`] = content;
  }

  // packages/shared-config/
  for (const [relPath, content] of Object.entries(sharedConfigPkg)) {
    files[`packages/shared-config/${relPath}`] = content;
  }

  // Root files
  for (const [relPath, content] of Object.entries(rootFiles)) {
    files[relPath] = content;
  }

  // Scripts
  for (const [relPath, content] of Object.entries(scripts)) {
    files[relPath] = content;
  }

  // ══ Stats ══
  const allPaths = Object.keys(files);
  const webPaths = allPaths.filter(p => p.startsWith("apps/web/"));
  const apiPaths = allPaths.filter(p => p.startsWith("apps/api/"));
  const sharedPaths = allPaths.filter(p => p.startsWith("packages/"));
  const rootPaths = allPaths.filter(p => !p.includes("/"));

  const stats = {
    totalFiles: allPaths.length,
    webFiles: webPaths.length,
    apiFiles: apiPaths.length,
    sharedFiles: sharedPaths.length,
    rootFiles: rootPaths.length,
    dbSchema: contract.entities.length,
  };

  return { files, stats, error: null, message: null };
}

// ═══════════════════════════════════════════════════════
// 9. File I/O
// ═══════════════════════════════════════════════════════

export function loadJSON(path) {
  try {
    if (!existsSync(path)) return { data: null, error: `File not found: ${path}` };
    return { data: JSON.parse(readFileSync(path, "utf-8")), error: null };
  } catch (e) {
    return { data: null, error: `Failed to load: ${e.message}` };
  }
}

export function writeFullstack(result, outputDir) {
  mkdirSync(outputDir, { recursive: true });
  for (const [relPath, content] of Object.entries(result.files)) {
    const fullPath = resolve(outputDir, relPath);
    mkdirSync(dirname(fullPath), { recursive: true });
    writeFileSync(fullPath, content);
  }
}

/**
 * Prefix frontend source files with src/ for Next.js src directory structure.
 * Frontend builder outputs flat paths (app/layout.tsx, types/index.ts),
 * but monorepo structure uses apps/web/src/ prefix.
 * Config files (package.json, tsconfig.json, next.config.*, tailwind.config.*,
 * postcss.config.*, .env*) stay at root level.
 *
 * @param {object} raw — Frontend builder output paths (e.g. "app/layout.tsx")
 * @returns {object} — Source paths prefixed, config paths kept at root
 */
function prefixSrc(raw) {
  const CONFIG_FILES = /^(package\.json|tsconfig\.json|next\.config\.[a-z]+|tailwind\.config\.[a-z]+|postcss\.config\.[a-z]+|\.[a-z-]+)$/;
  const result = {};
  for (const [relPath, content] of Object.entries(raw)) {
    if (relPath.startsWith("src/")) {
      // Already prefixed — keep as-is
      result[relPath] = content;
    } else if (CONFIG_FILES.test(relPath)) {
      // Config files — keep at root
      result[relPath] = content;
    } else {
      // Source code — prefix with src/
      result[`src/${relPath}`] = content;
    }
  }
  return result;
}

// ═══════════════════════════════════════════════════════
// 10. CLI Entry
// ═══════════════════════════════════════════════════════

function parseArgs() {
  const args = process.argv.slice(2);
  const opts = { prd: null, arch: null, inputText: null, output: null, verbose: false, help: false };
  for (let i = 0; i < args.length; i++) {
    if (args[i] === "--prd" && args[i + 1]) opts.prd = args[++i];
    else if (args[i] === "--arch" && args[i + 1]) opts.arch = args[++i];
    else if (args[i] === "--input-text" && args[i + 1]) opts.inputText = args[++i];
    else if (args[i] === "--output" && args[i + 1]) opts.output = args[++i];
    else if (args[i] === "--verbose") opts.verbose = true;
    else if (args[i] === "--help" || args[i] === "-h") opts.help = true;
  }
  return opts;
}

async function main() {
  const opts = parseArgs();

  if (opts.help) {
    console.log(`
Fullstack Composer Agent — SF-05

Usage:
  node scripts/fullstack-composer-agent.mjs --prd <path> --arch <path> [options]
  node scripts/fullstack-composer-agent.mjs --input-text "<需求>" [options]

Options:
  --prd <path>          PRD JSON（SF-01 输出）
  --arch <path>         Architecture JSON（SF-02 输出）
  --input-text <text>   直接传入需求
  --output <dir>        输出目录（default: fullstack/）
  --verbose             详细输出
  --help                显示帮助
`);
    return;
  }

  let prd = null, arch = null;

  if (opts.prd && opts.arch) {
    const prdRes = loadJSON(opts.prd);
    const archRes = loadJSON(opts.arch);
    if (prdRes.error) { console.error(prdRes.error); process.exit(1); }
    if (archRes.error) { console.error(archRes.error); process.exit(1); }
    prd = prdRes.data;
    arch = archRes.data;
  } else if (opts.inputText) {
    try {
      const sf01 = await import("./project-intake-agent.mjs");
      const sf02 = await import("./architecture-agent.mjs");
      prd = sf01.generatePRD(opts.inputText);
      if (prd.error) { console.error(`SF-01: ${prd.message}`); process.exit(1); }
      arch = sf02.generateArchitecture(prd);
      if (arch.error) { console.error(`SF-02: ${arch.message}`); process.exit(1); }
    } catch (e) {
      console.error(`Pipeline error: ${e.message}`);
      process.exit(1);
    }
  }

  if (!prd || !arch) {
    console.error("Error: --prd + --arch or --input-text is required. Use --help.");
    process.exit(1);
  }

  const result = await composeFullstack(prd, arch);
  if (result.error) { console.error(result.message); process.exit(1); }

  const outputDir = opts.output ? resolve(opts.output) : DEFAULT_OUTPUT;
  writeFullstack(result, outputDir);

  if (opts.verbose) {
    console.error(`Project: ${prd.projectName}`);
    console.error(`Files: ${result.stats.totalFiles}`);
    console.error(`Web: ${result.stats.webFiles}, API: ${result.stats.apiFiles}, Shared: ${result.stats.sharedFiles}`);
  }

  console.log(JSON.stringify({
    projectName: prd.projectName,
    outputDir,
    stats: result.stats,
  }, null, 2));
}

if (process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]))) {
  main();
}
