#!/usr/bin/env node

/**
 * Backend Builder Agent — SF-04
 *
 * 根据 PRD (SF-01) + Architecture (SF-02) 自动生成后端项目。
 *
 * 技术栈：Node.js + Fastify + TypeScript + SQLite + JWT + bcrypt
 *
 * 生成内容：
 *   - package.json / tsconfig.json / .env.example / README.md
 *   - src/index.ts — Fastify 服务入口
 *   - src/db/schema.ts — SQLite 建表语句
 *   - src/db/client.ts — better-sqlite3 封装
 *   - src/routes/*.ts — CRUD 路由
 *   - src/routes/auth.ts — 认证路由（register/login/me）
 *   - src/middleware/auth.ts — JWT 权限中间件
 *   - src/services/*.ts — 业务逻辑层
 *   - src/types/*.ts — TypeScript 类型定义
 *   - src/__tests__/*.test.ts — node:test 测试
 *
 * Usage:
 *   node scripts/backend-builder-agent.mjs --prd <path> --arch <path> [options]
 *   node scripts/backend-builder-agent.mjs --input-text "<需求>" [options]
 *
 * Options:
 *   --prd <path>          PRD JSON（SF-01 输出）
 *   --arch <path>         Architecture JSON（SF-02 输出）
 *   --input-text <text>   直接传入需求（自动调 SF-01 → SF-02 → SF-04）
 *   --output <dir>        输出目录（default: backend/）
 *   --verbose             详细输出
 *   --help                显示帮助
 *
 * @module backend-builder-agent
 */

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

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

// ═══════════════════════════════════════════════════════
// Utilities
// ═══════════════════════════════════════════════════════

function safeName(s) {
  return (s || "item")
    .replace(/[^a-zA-Z0-9_\u4e00-\u9fff]/g, "_")
    .replace(/_{2,}/g, "_")
    .replace(/^_|_$/g, "")
    || "item";
}

function capitalize(s) {
  return s.charAt(0).toUpperCase() + s.slice(1);
}

function camelCase(s) {
  return s.replace(/[-_]([a-zA-Z])/g, (_, c) => c.toUpperCase());
}

function pascalCase(s) {
  return capitalize(camelCase(s));
}

/** 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")) {
    // Check compound words: "reading_status" → last segment is "status"
    const lastSeg = s.includes("_") ? s.split("_").pop() : s;
    if (SINGULAR_EXCEPTIONS.has(lastSeg)) return s;
    return s.slice(0, -1);
  }
  return s;
}



// ═══════════════════════════════════════════════════════
// 1. TypeScript Types Generator
// ═══════════════════════════════════════════════════════

function generateTypes(contract) {
  const entities = contract.entities || [];
  const lines = [
    `// Auto-generated types (from Model Contract)`,
    ``,
  ];

  // ─── Entity interfaces ───────────────────────────
  for (const entity of entities) {
    const name = entity.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}: ${tsType(f)};`);
    }
    lines.push(`}`);
    lines.push(``);
  }

  // ─── CreateInput types ────────────────────────────
  for (const entity of entities) {
    const name = entity.name;
    const createFields = createInputFields(entity);
    if (createFields.length === 0) continue;

    lines.push(`export interface Create${name}Input {`);
    for (const f of createFields) {
      const fname = toCamel(contract, f.name);
      const optional = f.required ? "" : "?";
      lines.push(`  ${fname}${optional}: ${tsType(f)};`);
    }
    lines.push(`}`);
    lines.push(``);
  }

  // ─── UpdateInput types (all optional) ─────────────
  for (const entity of entities) {
    const name = entity.name;
    const updateFields = updateInputFields(entity);
    if (updateFields.length === 0) continue;

    lines.push(`export interface Update${name}Input {`);
    for (const f of updateFields) {
      const fname = toCamel(contract, f.name);
      lines.push(`  ${fname}?: ${tsType(f)};`);
    }
    lines.push(`}`);
    lines.push(``);
  }

  // ─── Auth types (always included) ────────────────
  lines.push(`// ─── Auth ───────────────────────────────────────────`);
  lines.push(`export interface LoginInput {`);
  lines.push(`  username: string;`);
  lines.push(`  password: string;`);
  lines.push(`}`);
  lines.push(``);
  lines.push(`export interface RegisterInput {`);
  lines.push(`  username: string;`);
  lines.push(`  password: string;`);
  lines.push(`  nickname?: string;`);
  lines.push(`}`);
  lines.push(``);
  lines.push(`export interface AuthResponse {`);
  lines.push(`  token: string;`);
  lines.push(`  user: User;`);
  lines.push(`}`);
  lines.push(``);

  // API response wrappers
  lines.push(`// ─── API ────────────────────────────────────────────`);
  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(``);

  // JWT payload
  lines.push(`// ─── JWT ────────────────────────────────────────────`);
  lines.push(`export interface JwtPayload {`);
  lines.push(`  userId: string;`);
  lines.push(`  username: string;`);
  lines.push(`  role: string;`);
  lines.push(`  iat?: number;`);
  lines.push(`  exp?: number;`);
  lines.push(`}`);
  lines.push(``);

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

// ═══════════════════════════════════════════════════════
// 2. Database Schema Generator (SQLite)
// ═══════════════════════════════════════════════════════

function generateDBSchema(contract) {
  const entities = contract.entities || [];
  const sqlParts = [];

  for (const entity of entities) {
    const tableName = entity.table;
    const cols = [];

    for (const f of entity.fields) {
      const colName = f.name;
      const colType = sqliteType(f);

      let constraints = "";
      if (f.isPrimary) {
        constraints = "PRIMARY KEY";
        // Auto-generate UUID for primary keys
        if (f.isAuto || colType === "TEXT") {
          constraints += " DEFAULT (lower(hex(randomblob(16))))";
        }
      }
      if (f.required && !f.isPrimary) {
        constraints = "NOT NULL";
      }
      if (f.unique) {
        constraints = constraints ? `${constraints} UNIQUE` : "UNIQUE";
      }
      if (f.defaultValue !== undefined && !f.isAuto) {
        const defVal = f.defaultValue.startsWith("'") || f.defaultValue.startsWith("\"") ? f.defaultValue : `'${f.defaultValue}'`;
        constraints = constraints ? `${constraints} DEFAULT ${defVal}` : `DEFAULT ${defVal}`;
      }
      if (f.fkEntity && f.fkColumn) {
        const ref = `REFERENCES ${f.fkEntity}(${f.fkColumn})`;
        constraints = constraints ? `${constraints} ${ref}` : ref;
      }

      cols.push(`  ${colName} ${colType}${constraints ? " " + constraints : ""}`);
    }

    sqlParts.push(`CREATE TABLE IF NOT EXISTS ${tableName} (\n${cols.join(",\n")}\n);`);
  }

  // Generate the TS file
  const escapedSql = sqlParts.map(s => JSON.stringify(s)).join(",\n  ");

  return `// Auto-generated SQLite schema
import type { Database } from "sql.js";

export function createTables(db: Database): void {
  const statements = [
  ${escapedSql}
  ];
  for (const sql of statements) {
    const trimmed = sql.trim();
    if (trimmed) db.run(trimmed);
  }
}
`;
}

// ═══════════════════════════════════════════════════════
// 3. Database Client Generator
// ═══════════════════════════════════════════════════════

function generateDBClient() {
  return `// SQLite database client (sql.js — pure WASM, no native deps)
import initSqlJs, { type Database, type BindParams } from "sql.js";
import { createTables } from "./schema.js";

let db: Database | null = null;
let initPromise: Promise<Database> | null = null;

/** Initialize the database (call once at startup). */
export async function initDb(dbPath?: string): Promise<Database> {
  if (db) return db;
  if (initPromise) return initPromise;

  initPromise = (async () => {
    const SQL = await initSqlJs();
    const path = dbPath || process.env.DATABASE_URL || ":memory:";

    // Try to load existing database from file
    let buffer: ArrayLike<number> | undefined;
    if (path !== ":memory:") {
      try {
        const fs = await import("node:fs/promises");
        const data = await fs.readFile(path);
        buffer = new Uint8Array(data);
      } catch {
        // File doesn't exist yet — start fresh
      }
    }

    db = new SQL.Database(buffer);
    db.run("PRAGMA foreign_keys = ON");
    createTables(db);
    return db;
  })();

  return initPromise;
}

/** Get the initialized database (must call initDb first). */
export function getDb(): Database {
  if (!db) throw new Error("Database not initialized. Call initDb() first.");
  return db;
}

/** Save database to disk. */
export async function saveDb(dbPath?: string): Promise<void> {
  if (!db) return;
  const path = dbPath || process.env.DATABASE_URL || "./data/app.db";
  if (path === ":memory:") return;
  const fs = await import("node:fs/promises");
  const { dirname } = await import("node:path");
  await fs.mkdir(dirname(path), { recursive: true });
  const data = db.export();
  await fs.writeFile(path, Buffer.from(data));
}

export async function closeDb(): Promise<void> {
  if (db) {
    await saveDb();
    db.close();
    db = null;
    initPromise = null;
  }
}

// Helper: run a query and return all rows as objects
export function queryAll<T = Record<string, unknown>>(sql: string, params: BindParams = []): T[] {
  const d = getDb();
  const stmt = d.prepare(sql);
  if (params) stmt.bind(params);
  const results: T[] = [];
  while (stmt.step()) {
    const row = stmt.getAsObject();
    results.push(row as unknown as T);
  }
  stmt.free();
  return results;
}

// Helper: run a query and return the first row
export function queryOne<T = Record<string, unknown>>(sql: string, params: BindParams = []): T | undefined {
  const rows = queryAll<T>(sql, params);
  return rows[0];
}

// Helper: run a mutation and return { changes, lastInsertRowid }
export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } {
  const d = getDb();
  d.run(sql, params);
  return {
    changes: d.getRowsModified(),
    lastInsertRowid: 0,
  };
}
`;
}

// ═══════════════════════════════════════════════════════
// 4. Auth Middleware Generator
// ═══════════════════════════════════════════════════════

function generateAuthMiddleware() {
  return `// JWT Authentication Middleware
import type { FastifyRequest, FastifyReply } from "fastify";
import type { JwtPayload } from "../types/index.js";

/**
 * Verify JWT token and attach user to request.
 */
export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise<void> {
  try {
    await request.jwtVerify();
  } catch (err) {
    reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 });
  }
}

/** Helper to get typed user from request (after authenticate). */
export function getUser(request: FastifyRequest): JwtPayload {
  return request.user as unknown as JwtPayload;
}

/**
 * Require admin role.
 * Must be used after authenticate.
 */
export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise<void> {
  const user = request.user as unknown as JwtPayload | undefined;
  if (!user || user.role !== "admin") {
    reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 });
  }
}

/**
 * Optional auth: attach user if token present, but don't fail if missing.
 */
export async function optionalAuth(request: FastifyRequest): Promise<void> {
  try {
    await request.jwtVerify();
  } catch {
    // No token or invalid — continue without user
  }
}
`;
}

// ═══════════════════════════════════════════════════════
// 5. Service Generator
// ═══════════════════════════════════════════════════════

function generateServices(contract) {
  const entities = contract.entities || [];
  const files = {};

  // Skip auth entities (users) — auth has its own service
  const serviceEntities = entities.filter(e => e.table !== "users");

  for (const entity of serviceEntities) {
    const tableName = entity.table;
    const name = entity.name;
    const singular = singularize(tableName);
    const inputFields = createInputFields(entity);
    const updateFields = updateInputFields(entity);

    // Build the db column names and camelCase field names for the template
    const createCols = [];
    const createCamelFields = [];
    const updateColFields = [];
    for (const f of inputFields) {
      const colName = f.name;
      const camelName = toCamel(contract, colName);
      createCols.push(colName);
      createCamelFields.push(camelName);
      updateColFields.push({ col: colName, camel: camelName });
    }

    const hasCreatedAt = hasField(entity, "created_at");
    const hasUpdatedAt = hasField(entity, "updated_at");

    // Build column list including id and timestamp cols
    const allCols = ["id", ...createCols];
    if (hasCreatedAt) allCols.push("created_at");
    if (hasUpdatedAt) allCols.push("updated_at");

    const allPlaceholders = allCols.map(() => "?").join(", ");

    // Build values expression
    const valueParts = ["id"];
    for (const camel of createCamelFields) {
      valueParts.push(`input.${camel} ?? null`);
    }
    if (hasCreatedAt) valueParts.push("now");
    if (hasUpdatedAt) valueParts.push("now");
    const valuesExpr = valueParts.join(", ");

    // Build update set expressions
    const updateLines = [];
    for (const { col, camel } of updateColFields) {
      updateLines.push(`    if (input.${camel} !== undefined) { sets.push("${col} = ?"); values.push(input.${camel}); }`);
    }

    const serviceFile = `services/${singular}.ts`;

    files[`src/${serviceFile}`] = `// Auto-generated ${name} service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { ${name}, Create${name}Input, Update${name}Input } from "../types/index.js";

export class ${name}Service {
  /** List all ${tableName} */
  list(): ${name}[] {
    return queryAll<${name}>("SELECT * FROM ${tableName} ORDER BY created_at DESC");
  }

  /** Get by ID */
  getById(id: string): ${name} | undefined {
    return queryOne<${name}>("SELECT * FROM ${tableName} WHERE id = ?", [id]);
  }

  /** Create */
  create(input: Create${name}Input): ${name} {
    const id = crypto.randomUUID();
    const now = new Date().toISOString();
    const cols = ["${allCols.join('", "')}"];
    const values = [${valuesExpr}];

    execute(\`INSERT INTO ${tableName} (\${cols.join(", ")}) VALUES (${allPlaceholders})\`, values);
    return this.getById(id)!;
  }

  /** Update */
  update(id: string, input: Update${name}Input): ${name} | undefined {
    const existing = this.getById(id);
    if (!existing) return undefined;

    const sets: string[] = [];
    const values: unknown[] = [];
${updateLines.join("\n")}

    if (sets.length === 0) return existing;

    ${hasUpdatedAt ? 'sets.push("updated_at = ?");\n    values.push(new Date().toISOString());' : ''}

    values.push(id);
    execute(\`UPDATE ${tableName} SET \${sets.join(", ")} WHERE id = ?\`, values);
    return this.getById(id)!;
  }

  /** Delete */
  delete(id: string): boolean {
    const result = execute("DELETE FROM ${tableName} WHERE id = ?", [id]);
    return result.changes > 0;
  }
}
`;
  }

  return files;
}

// ═══════════════════════════════════════════════════════
// 6. Route Generator (CRUD)
// ═══════════════════════════════════════════════════════

function generateRoutes(contract) {
  const entities = contract.entities || [];
  const files = {};

  // Skip auth entities (users) — auth has its own routes
  const routeEntities = entities.filter(e => e.table !== "users");

  for (const entity of routeEntities) {
    const tableName = entity.table;
    const name = entity.name;
    const singular = singularize(tableName);
    const routeFile = `src/routes/${tableName}.ts`;

    files[routeFile] = `// Auto-generated ${name} routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { ${name}Service } from "../services/${singular}.js";
import type { Create${name}Input, Update${name}Input } from "../types/index.js";

const service = new ${name}Service();

export async function ${tableName}Routes(app: FastifyInstance): Promise<void> {
  // All routes require authentication
  app.addHook("onRequest", authenticate);

  // GET /api/${tableName} — list
  app.get("/", async (request, reply) => {
    const items = service.list();
    return { data: items };
  });

  // GET /api/${tableName}/:id — get by id
  app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
    const item = service.getById(request.params.id);
    if (!item) {
      return reply.status(404).send({ error: "Not Found", message: "${name} not found", statusCode: 404 });
    }
    return { data: item };
  });

  // POST /api/${tableName} — create
  app.post<{ Body: Create${name}Input }>("/", async (request, reply) => {
    const item = service.create(request.body);
    return reply.status(201).send({ data: item });
  });

  // PUT /api/${tableName}/:id — update
  app.put<{ Params: { id: string }; Body: Update${name}Input }>("/:id", async (request, reply) => {
    const item = service.update(request.params.id, request.body);
    if (!item) {
      return reply.status(404).send({ error: "Not Found", message: "${name} not found", statusCode: 404 });
    }
    return { data: item };
  });

  // DELETE /api/${tableName}/:id — delete
  app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
    const deleted = service.delete(request.params.id);
    if (!deleted) {
      return reply.status(404).send({ error: "Not Found", message: "${name} not found", statusCode: 404 });
    }
    return reply.status(204).send();
  });
}
`;
  }

  return files;
}

// ═══════════════════════════════════════════════════════
// 7. Auth Routes Generator
// ═══════════════════════════════════════════════════════

function generateAuthRoutes() {
  return `// Authentication routes
import type { FastifyInstance } from "fastify";
import bcrypt from "bcrypt";
import { queryOne, execute } from "../db/client.js";
import { authenticate } from "../middleware/auth.js";
import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js";

const SALT_ROUNDS = 10;

export async function authRoutes(app: FastifyInstance): Promise<void> {
  // POST /api/auth/register
  app.post<{ Body: RegisterInput }>("/register", async (request, reply) => {
    const { username, password, nickname } = request.body;

    if (!username || !password) {
      return reply.status(400).send({
        error: "Bad Request",
        message: "Username and password are required",
        statusCode: 400,
      });
    }

    if (password.length < 6) {
      return reply.status(400).send({
        error: "Bad Request",
        message: "Password must be at least 6 characters",
        statusCode: 400,
      });
    }

    const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]);
    if (existing) {
      return reply.status(409).send({
        error: "Conflict",
        message: "Username already exists",
        statusCode: 409,
      });
    }

    const id = crypto.randomUUID();
    const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
    const now = new Date().toISOString();

    execute(
      "INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
      [id, username, passwordHash, nickname || username, now, now]
    );

    const user = queryOne<User>(
      "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?",
      [id]
    );
    if (!user) {
      return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 });
    }

    const token = app.jwt.sign({ userId: id, username, role: user.role });

    return reply.status(201).send({ token, user } satisfies AuthResponse);
  });

  // POST /api/auth/login
  app.post<{ Body: LoginInput }>("/login", async (request, reply) => {
    const { username, password } = request.body;

    if (!username || !password) {
      return reply.status(400).send({
        error: "Bad Request",
        message: "Username and password are required",
        statusCode: 400,
      });
    }

    const user = queryOne<User & { password_hash: string }>(
      "SELECT * FROM users WHERE username = ?",
      [username]
    );

    if (!user) {
      return reply.status(401).send({
        error: "Unauthorized",
        message: "Invalid username or password",
        statusCode: 401,
      });
    }

    const valid = await bcrypt.compare(password, user.password_hash);
    if (!valid) {
      return reply.status(401).send({
        error: "Unauthorized",
        message: "Invalid username or password",
        statusCode: 401,
      });
    }

    const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role });

    const { password_hash, ...safeUser } = user;

    return { token, user: safeUser } satisfies AuthResponse;
  });

  // GET /api/auth/me — current user info
  app.get("/me", { onRequest: [authenticate] }, async (request, reply) => {
    const jwtUser = request.user as unknown as { userId: string };
    const user = queryOne<User>(
      "SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?",
      [jwtUser.userId]
    );

    if (!user) {
      return reply.status(404).send({
        error: "Not Found",
        message: "User not found",
        statusCode: 404,
      });
    }

    return { data: user };
  });
}
`;
}

// ═══════════════════════════════════════════════════════
// 8. Server Entry Point Generator
// ═══════════════════════════════════════════════════════

function generateIndex(contract, projectName) {
  const entities = (contract.entities || []).filter(e => e.table !== "users");
  const routeImports = entities.map(e => {
    return `import { ${e.table}Routes } from "./routes/${e.table}.js";`;
  }).join("\n");

  const routeRegistrations = entities.map(e => {
    return `  await app.register(${e.table}Routes, { prefix: "/api/${e.table}" });`;
  }).join("\n");

  return `// ${projectName} — Fastify Backend Server
import Fastify from "fastify";
import cors from "@fastify/cors";
import fjwt from "@fastify/jwt";
import { initDb, closeDb } from "./db/client.js";
import { authRoutes } from "./routes/auth.js";
${routeImports}

const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-${crypto.randomUUID().slice(0, 8)}";

export async function buildApp() {
  const app = Fastify({
    logger: {
      level: process.env.LOG_LEVEL || "info",
      transport: process.env.NODE_ENV !== "production"
        ? { target: "pino-pretty", options: { colorize: true } }
        : undefined,
    },
  });

  // Init database
  await initDb();

  // Plugins
  await app.register(cors, {
    origin: process.env.CORS_ORIGIN || "*",
    methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
  });

  await app.register(fjwt, { secret: JWT_SECRET });

  // Routes
  await app.register(authRoutes, { prefix: "/api/auth" });
${routeRegistrations}

  // Health check
  app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() }));

  // Graceful shutdown
  app.addHook("onClose", async () => {
    closeDb();
  });

  return app;
}

// Start server if called directly (not when imported by tests)
const port = parseInt(process.env.PORT || "3001", 10);
const host = process.env.HOST || "0.0.0.0";

async function main() {
  const app = await buildApp();
  try {
    await app.listen({ port, host });
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
}

// Guard: only run when executed directly, not when imported
const isMain = process.argv[1] && (import.meta.url === \`file://\${process.argv[1]}\` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js"));
if (isMain) {
  main();
}
`;
}

// ═══════════════════════════════════════════════════════
// 9. Config Files Generator
// ═══════════════════════════════════════════════════════

function generatePackageJson(projectName) {
  const name = safeName(projectName || "backend").toLowerCase().replace(/\s+/g, "-");
  return JSON.stringify({
    name,
    version: "0.1.0",
    private: true,
    type: "module",
    scripts: {
      dev: "tsx watch src/index.ts",
      build: "tsc",
      start: "node dist/index.js",
      test: "node --import tsx --test src/__tests__/*.test.ts",
      "test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts",
    },
    dependencies: {
      "fastify": "^5.0.0",
      "@fastify/cors": "^10.0.0",
      "@fastify/jwt": "^9.0.0",
      "sql.js": "^1.12.0",
      "bcrypt": "^5.1.0",
    },
    devDependencies: {
      "@types/node": "^22.0.0",
      "@types/bcrypt": "^5.0.0",
      "typescript": "^5.6.0",
      "tsx": "^4.0.0",
      "pino-pretty": "^11.0.0",
    },
  }, null, 2);
}

function generateTsConfig() {
  return JSON.stringify({
    compilerOptions: {
      target: "ES2022",
      module: "ESNext",
      moduleResolution: "bundler",
      lib: ["ES2022"],
      outDir: "./dist",
      rootDir: "./src",
      strict: true,
      esModuleInterop: true,
      skipLibCheck: true,
      forceConsistentCasingInFileNames: true,
      resolveJsonModule: true,
      declaration: true,
      sourceMap: true,
    },
    include: ["src/**/*"],
    exclude: ["node_modules", "dist", "src/__tests__"],
  }, null, 2);
}

function generateEnvExample(projectName) {
  return `# ${projectName} Backend Environment Variables

# Server
PORT=3001
HOST=0.0.0.0
NODE_ENV=development
LOG_LEVEL=info

# Database
DATABASE_URL=./data/app.db

# Auth
JWT_SECRET=change-me-to-a-random-secret-key

# CORS
CORS_ORIGIN=*
`;
}

function generateReadme(projectName, prdSummary, contract) {
  const entities = (contract.entities || []).filter(e => e.table !== "users");
  const entityList = entities.map(e => `- **${e.table}** — ${e.description || ""}`).join("\n");

  return `# ${projectName} — Backend API

> ${prdSummary || "Auto-generated backend service"}

## Tech Stack

- **Runtime**: Node.js
- **Framework**: Fastify 5
- **Language**: TypeScript
- **Database**: SQLite (better-sqlite3)
- **Auth**: JWT + bcrypt

## Getting Started

\`\`\`bash
# Install dependencies
npm install

# Development (hot reload)
npm run dev

# Build
npm run build

# Production start
npm run start

# Run tests
npm test
\`\`\`

## Project Structure

\`\`\`
src/
├── index.ts          # Server entry point
├── db/
│   ├── schema.ts     # SQLite schema
│   └── client.ts     # Database client
├── routes/
│   ├── auth.ts       # Auth routes (register/login/me)
│   └── *.ts          # CRUD routes
├── services/
│   └── *.ts          # Business logic
├── middleware/
│   └── auth.ts       # JWT middleware
├── types/
│   └── index.ts      # TypeScript types
└── __tests__/
    └── *.test.ts     # Tests
\`\`\`

## API Endpoints

### Auth
- \`POST /api/auth/register\` — Register
- \`POST /api/auth/login\` — Login
- \`GET  /api/auth/me\` — Current user (auth required)

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

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

// ═══════════════════════════════════════════════════════
// 10. Test Generator
// ═══════════════════════════════════════════════════════

function generateTests(contract, projectName) {
  const entities = contract.entities || [];
  const tests = {};

  // Auth test
  tests["__tests__/auth.test.ts"] = `// Auth routes test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";

let app: FastifyInstance;
let token: string;

before(async () => {
  process.env.JWT_SECRET = "test-secret";
  process.env.DATABASE_URL = ":memory:";
  app = await buildApp();
  await app.ready();
});

after(async () => {
  await app.close();
});

describe("POST /api/auth/register", () => {
  it("registers a new user", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/auth/register",
      payload: { username: "testuser", password: "password123" },
    });
    assert.equal(res.statusCode, 201);
    const body = res.json();
    assert.ok(body.token);
    assert.equal(body.user.username, "testuser");
    token = body.token;
  });

  it("rejects duplicate username", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/auth/register",
      payload: { username: "testuser", password: "password123" },
    });
    assert.equal(res.statusCode, 409);
  });

  it("rejects short password", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/auth/register",
      payload: { username: "user2", password: "123" },
    });
    assert.equal(res.statusCode, 400);
  });
});

describe("POST /api/auth/login", () => {
  it("logs in with correct credentials", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/auth/login",
      payload: { username: "testuser", password: "password123" },
    });
    assert.equal(res.statusCode, 200);
    const body = res.json();
    assert.ok(body.token);
    token = body.token;
  });

  it("rejects wrong password", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/auth/login",
      payload: { username: "testuser", password: "wrongpassword" },
    });
    assert.equal(res.statusCode, 401);
  });
});

describe("GET /api/auth/me", () => {
  it("returns current user with valid token", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/auth/me",
      headers: { authorization: \`Bearer \${token}\` },
    });
    assert.equal(res.statusCode, 200);
    const body = res.json();
    assert.equal(body.data.username, "testuser");
  });

  it("rejects without token", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/auth/me",
    });
    assert.equal(res.statusCode, 401);
  });
});
`;

  // CRUD test template for each entity (skip users — auth has own tests)
  const testEntities = entities.filter(e => e.table !== "users");

  for (const entity of testEntities) {
    const tableName = entity.table;
    const name = entity.name;
    const singular = singularize(tableName);

    // Build sample create payload from input fields
    const inputFields = createInputFields(entity);
    const samplePayload = {};
    let hasUserFk = false;

    for (const f of inputFields) {
      const fname = toCamel(contract, f.name);
      const ft = tsType(f);
      // FK fields referencing users — use registered user's real ID
      if (f.fkEntity === "users" && f.fkColumn === "id") {
        samplePayload[fname] = "__REG_USER_ID__";
        hasUserFk = true;
      } else if (ft === "number") {
        samplePayload[fname] = 1;
      } else if (ft === "boolean") {
        samplePayload[fname] = true;
      } else {
        samplePayload[fname] = `sample-${fname.toLowerCase()}`;
      }
    }

    // Serialize payload for use in template
    const payloadJson = JSON.stringify(samplePayload, null, 4)
      .split("\n").map((line, i) => i === 0 ? line : `      ${line}`).join("\n");

    const userIdExpr = hasUserFk ? `regRes.json().user.id` : `"00000000-0000-0000-0000-000000000001"`;

    tests[`__tests__/${tableName}.test.ts`] = `// ${name} CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";

let app: FastifyInstance;
let token: string;
let createdId: string;
let payload: Record<string, unknown>;

before(async () => {
  process.env.JWT_SECRET = "test-secret";
  process.env.DATABASE_URL = ":memory:";
  app = await buildApp();
  await app.ready();

  // Register and login to get a token
  const regRes = await app.inject({
    method: "POST",
    url: "/api/auth/register",
    payload: { username: \`test_\${Date.now()}\`, password: "password123" },
  });
  token = regRes.json().token;

  // Resolve user FK references with the registered user's real ID
  payload = ${payloadJson.replace(/"__REG_USER_ID__"/g, userIdExpr)};
});

after(async () => {
  await app.close();
});

describe("GET /api/${tableName}", () => {
  it("returns empty list", async () => {
    const res = await app.inject({
      method: "GET",
      url: "/api/${tableName}",
      headers: { authorization: \`Bearer \${token}\` },
    });
    assert.equal(res.statusCode, 200);
    const body = res.json();
    assert.ok(Array.isArray(body.data));
  });
});

describe("POST /api/${tableName}", () => {
  it("creates a ${singular}", async () => {
    const res = await app.inject({
      method: "POST",
      url: "/api/${tableName}",
      headers: { authorization: \`Bearer \${token}\` },
      payload,
    });
    assert.equal(res.statusCode, 201);
    const body = res.json();
    assert.ok(body.data.id);
    createdId = body.data.id;
  });
});

describe("GET /api/${tableName}/:id", () => {
  it("returns the created ${singular}", async () => {
    const res = await app.inject({
      method: "GET",
      url: \`/api/${tableName}/\${createdId}\`,
      headers: { authorization: \`Bearer \${token}\` },
    });
    assert.equal(res.statusCode, 200);
    const body = res.json();
    assert.equal(body.data.id, createdId);
  });

  it("returns 404 for nonexistent id", async () => {
    const res = await app.inject({
      method: "GET",
      url: \`/api/${tableName}/nonexistent\`,
      headers: { authorization: \`Bearer \${token}\` },
    });
    assert.equal(res.statusCode, 404);
  });
});

describe("PUT /api/${tableName}/:id", () => {
  it("updates the ${singular}", async () => {
    const res = await app.inject({
      method: "PUT",
      url: \`/api/${tableName}/\${createdId}\`,
      headers: { authorization: \`Bearer \${token}\` },
      payload,
    });
    assert.equal(res.statusCode, 200);
  });
});

describe("DELETE /api/${tableName}/:id", () => {
  it("deletes the ${singular}", async () => {
    const res = await app.inject({
      method: "DELETE",
      url: \`/api/${tableName}/\${createdId}\`,
      headers: { authorization: \`Bearer \${token}\` },
    });
    assert.equal(res.statusCode, 204);
  });

  it("returns 404 after delete", async () => {
    const res = await app.inject({
      method: "GET",
      url: \`/api/${tableName}/\${createdId}\`,
      headers: { authorization: \`Bearer \${token}\` },
    });
    assert.equal(res.statusCode, 404);
  });
});
`;
  }

  return tests;
}

// ═══════════════════════════════════════════════════════
// 11. Main Builder Function
// ═══════════════════════════════════════════════════════

/**
 * Build a complete backend project from PRD + Architecture.
 *
 * @param {object} prd — SF-01 PRD
 * @param {object} arch — SF-02 Architecture
 * @returns {object} { files, stats, error, message }
 */
export function buildBackend(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 || "BackendApp";
  const contract = arch.contract || createContract(prd, { databaseSchema: arch.databaseSchema || [], domain: arch.domain });
  const prdSummary = prd.summary || "";

  const files = {};

  // ── Config files ──
  files["package.json"] = generatePackageJson(projectName);
  files["tsconfig.json"] = generateTsConfig();
  files[".env.example"] = generateEnvExample(projectName);
  files["README.md"] = generateReadme(projectName, prdSummary, contract);

  // ── Types ──
  files["src/types/index.ts"] = generateTypes(contract);
  files["src/types/fastify.d.ts"] = `// Augment Fastify request with JWT user
import type { JwtPayload } from "./index.js";

declare module "fastify" {
  interface FastifyRequest {
    user?: JwtPayload;
  }
}
`;
  files["src/types/sql.js.d.ts"] = `// Type declarations for sql.js (no native types package available)
declare module "sql.js" {
  export interface Database {
    run(sql: string, params?: BindParams): void;
    exec(sql: string): void;
    prepare(sql: string): Statement;
    export(): Uint8Array;
    close(): void;
    getRowsModified(): number;
  }

  export interface Statement {
    bind(params?: BindParams): boolean;
    step(): boolean;
    getAsObject<T = Record<string, unknown>>(): T;
    getColumnNames(): string[];
    free(): boolean;
  }

  export type BindParams = unknown[] | Record<string, unknown>;

  export interface SqlJsStatic {
    Database: new (data?: ArrayLike<number>) => Database;
  }

  export default function initSqlJs(config?: Record<string, unknown>): Promise<SqlJsStatic>;
}
`;

  // ── Database ──
  files["src/db/schema.ts"] = generateDBSchema(contract);
  files["src/db/client.ts"] = generateDBClient();

  // ── Middleware ──
  files["src/middleware/auth.ts"] = generateAuthMiddleware();

  // ── Services ──
  Object.assign(files, generateServices(contract));

  // ── Routes (already include src/ prefix)
  Object.assign(files, generateRoutes(contract));
  files["src/routes/auth.ts"] = generateAuthRoutes();

  // ── Server entry ──
  files["src/index.ts"] = generateIndex(contract, projectName);

  // ── Tests (with src/ prefix fixup)
  const tests = generateTests(contract, projectName);
  for (const [key, value] of Object.entries(tests)) {
    files[`src/${key}`] = value;
  }

  // ── .gitignore ──
  files[".gitignore"] = `node_modules/
dist/
data/
.env
*.db
*.db-journal
*.db-wal
`;

  // ── Stats ──
  const allPaths = Object.keys(files);
  const totalFiles = allPaths.length;
  const routes = allPaths.filter(p => p.startsWith("src/routes/") && p !== "src/routes/auth.ts");
  const services = allPaths.filter(p => p.startsWith("src/services/"));
  const testFiles = allPaths.filter(p => p.startsWith("src/__tests__/"));

  const stats = {
    totalFiles,
    routes: routes.length,
    services: services.length,
    tests: testFiles.length,
    dbSchema: contract.entities.length,
    routeFiles: routes,
    serviceFiles: services,
    testFiles,
  };

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

// ═══════════════════════════════════════════════════════
// 12. 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 writeBackend(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);
  }
}

// ═══════════════════════════════════════════════════════
// 13. 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(`
Backend Builder Agent — SF-04

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

Options:
  --prd <path>          PRD JSON（SF-01 输出）
  --arch <path>         Architecture JSON（SF-02 输出）
  --input-text <text>   直接传入需求（自动调 SF-01 → SF-02 → SF-04）
  --output <dir>        输出目录（default: backend/）
  --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 = buildBackend(prd, arch);
  if (result.error) { console.error(result.message); process.exit(1); }

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

  if (opts.verbose) {
    console.error(`Project: ${prd.projectName}`);
    console.error(`Files: ${result.stats.totalFiles}`);
    console.error(`Routes: ${result.stats.routes}, Services: ${result.stats.services}, Tests: ${result.stats.tests}`);
  }

  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();
}
