import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const args = process.argv.slice(2);
const configArg = args.find((arg) => !arg.startsWith("--"));
const dryRun = args.includes("--dry-run");
const skipApiGenerate = args.includes("--skip-api-generate");

if (!configArg) {
  console.error("Usage: pnpm gen:module <admin-module.json> [--dry-run] [--skip-api-generate]");
  process.exit(1);
}

const configPath = path.resolve(process.cwd(), configArg);
const rawConfig = JSON.parse(await readFile(configPath, "utf8"));
const moduleConfig = normalizeConfig(rawConfig);
const plannedWrites = [];

queueFileWrites(moduleConfig);
await updatePermissions(moduleConfig);
await updateRouteConfig(moduleConfig);
await updateOpenApiContract(moduleConfig);

if (dryRun) {
  console.log(`Dry run: ${plannedWrites.length} file changes planned.`);
  for (const write of plannedWrites) {
    console.log(`${write.exists ? "Update" : "Create"} ${path.relative(rootDir, write.filePath)}`);
  }
  process.exit(0);
}

for (const write of plannedWrites) {
  await mkdir(path.dirname(write.filePath), { recursive: true });
  if (!write.overwrite && existsSync(write.filePath)) {
    console.log(`Skipped existing ${path.relative(rootDir, write.filePath)}`);
    continue;
  }
  await writeFile(write.filePath, write.content);
  console.log(`${write.exists ? "Updated" : "Created"} ${path.relative(rootDir, write.filePath)}`);
}

if (!skipApiGenerate) {
  run("pnpm", ["exec", "openapi-typescript", "contracts/admin-openapi.json", "-o", "src/shared/api/generated/schema.d.ts"]);
  run(process.execPath, ["scripts/generate-api-client.mjs"]);
}

console.log(`Module ${moduleConfig.name} generated. Run pnpm check:contracts after updating the local API contract.`);

function normalizeConfig(config) {
  const name = toKebabCase(required(config.name, "name"));
  const camelName = toCamelCase(name);
  const pascalName = toPascalCase(name);
  const singularName = toPascalCase(config.singularName || singularize(name));
  const singularCamel = lowerFirst(singularName);
  const label = required(config.label, "label");
  const routePath = config.path || `/${name}`;
  const menuCode = config.menuCode || name;
  const permissionPrefix = config.permissionPrefix || singularize(name);
  const pageKey = config.pageKey || name;
  const menuKey = config.menuKey || toCamelCase(menuCode);
  const permissions = normalizePermissions(config.permissions || config.actions || defaultActions(), {
    label,
    permissionPrefix
  });
  const viewPermission = permissions.find((permission) => permission.action === "view") || permissions[0];

  if (!viewPermission) {
    throw new Error("At least one permission is required");
  }

  const apiRoutes = normalizeApiRoutes(config.apiRoutes || defaultApiRoutes(routePath, viewPermission.code), {
    name,
    permissions,
    routePath,
    singularName
  });

  return {
    apiRoutes,
    camelName,
    icon: config.icon || "menu",
    label,
    menuCode,
    menuKey,
    name,
    pageKey,
    parentMenuCode: config.parentMenuCode || "",
    pascalName,
    permissionPrefix,
    permissions,
    routePath,
    singularCamel,
    singularName,
    sort: Number(config.sort || 100),
    viewPermission
  };
}

function queueFileWrites(config) {
  const featureDir = path.join(rootDir, "src/features", config.name);
  const files = new Map([
    ["api.ts", renderApi(config)],
    ["routes.js", renderRoutes(config)],
    ["permissions.js", renderFeaturePermissions(config)],
    ["schema.ts", renderSchema(config)],
    [`hooks/use${config.pascalName}Page.js`, renderPageHook(config)],
    [`pages/${config.pascalName}Page.jsx`, renderPage(config)],
    [`${config.name}.module.css`, renderCss()],
    ["components/.gitkeep", ""]
  ]);

  for (const [relativePath, content] of files) {
    const filePath = path.join(featureDir, relativePath);
    planWrite(filePath, content, { overwrite: false });
  }

  const generatedDir = path.join(rootDir, "generated/admin-modules", config.name);
  planWrite(path.join(generatedDir, "permissions.md"), renderPermissionChecklist(config), { overwrite: true });
}

async function updatePermissions(config) {
  const filePath = path.join(rootDir, "src/app/permissions.ts");
  let source = await readFile(filePath, "utf8");
  const permissionEntries = config.permissions.map((permission) => ({
    key: permission.key,
    value: permission.code
  }));
  const menuEntry = { key: config.menuKey, value: config.menuCode };

  source = insertObjectEntries(source, "PERMISSIONS", permissionEntries);
  source = insertObjectEntries(source, "MENU_CODES", [menuEntry]);
  planWrite(filePath, source, { overwrite: true });
}

async function updateRouteConfig(config) {
  const filePath = path.join(rootDir, "src/app/router/routeConfig.ts");
  let source = await readFile(filePath, "utf8");
  const importLine = `import { ${config.camelName}Routes } from "@/features/${config.name}/routes.js";`;
  const routeSpread = `  ...${config.camelName}Routes,`;

  if (!source.includes(importLine)) {
    source = source.replace(/(import type \{ MenuCode \} from "@\/app\/permissions";)/, `${importLine}\n$1`);
  }

  if (!source.includes(routeSpread)) {
    const routesStart = source.indexOf("export const adminRoutes: AdminRoute[] = [");
    const routesEnd = source.indexOf("\n];", routesStart);
    if (routesStart < 0 || routesEnd < 0) {
      throw new Error("Cannot find adminRoutes block in routeConfig.ts");
    }
    const beforeRoutesEnd = source.slice(0, routesEnd);
    const afterRoutesEnd = source.slice(routesEnd);
    const separator = beforeRoutesEnd.trimEnd().endsWith(",") ? "\n" : ",\n";
    source = `${beforeRoutesEnd}${separator}${routeSpread}${afterRoutesEnd}`;
  }

  planWrite(filePath, source, { overwrite: true });
}

async function updateOpenApiContract(config) {
  const filePath = path.join(rootDir, "contracts/admin-openapi.json");
  const contract = JSON.parse(await readFile(filePath, "utf8"));
  contract.paths = contract.paths || {};

  for (const route of config.apiRoutes) {
    const openApiPath = toOpenApiPath(route.path);
    contract.paths[openApiPath] = contract.paths[openApiPath] || {};
    contract.paths[openApiPath][route.method.toLowerCase()] = {
      operationId: route.operationId,
      ...(route.permission
        ? {
            "x-permission": route.permission,
            "x-permissions": [route.permission]
          }
        : {}),
      ...(pathParams(openApiPath).length
        ? {
            parameters: pathParams(openApiPath).map((name) =>
              name === "id"
                ? { "$ref": "#/components/parameters/Id" }
                : { in: "path", name, required: true, schema: { type: "integer" } }
            )
          }
        : {}),
      responses: { "200": { "$ref": "#/components/responses/EmptyResponse" } }
    };
  }

  contract.paths = sortObject(contract.paths, (pathItem) => sortObject(pathItem, undefined, ["get", "post", "put", "patch", "delete"]));
  planWrite(filePath, `${JSON.stringify(contract, null, 2)}\n`, { overwrite: true });
}

function renderApi(config) {
  const imports = new Set(["apiRequest"]);
  const typeImports = new Set();
  const functions = config.apiRoutes.map((route) => renderApiFunction(route, config, typeImports));
  const needsEndpointObject = config.apiRoutes.some((route) => route.method !== "GET" || route.raw);
  const endpointImports = needsEndpointObject ? "API_ENDPOINTS, API_OPERATIONS, apiEndpointPath" : "API_OPERATIONS, apiEndpointPath";

  return `import { ${[...imports].join(", ")} } from "@/shared/api/request";
import { ${endpointImports} } from "@/shared/api/generated/endpoints";
${typeImports.size ? `import type { ${[...typeImports].sort().join(", ")} } from "@/shared/api/types";\n` : ""}
type ${config.singularName}Record = Record<string, unknown>;

${functions.join("\n\n")}
`;
}

function renderApiFunction(route, config, typeImports) {
  const operationKey = `API_OPERATIONS.${route.operationId}`;
  const endpointLine = `  const endpoint = API_ENDPOINTS.${route.operationId};`;

  if (route.raw) {
    typeImports.add("PageQuery");
    return `export function ${route.operationId}(query?: PageQuery): Promise<Response> {
  return apiRequest(apiEndpointPath(${operationKey}), { query, raw: true });
}`;
  }

  if (route.kind === "list") {
    typeImports.add("ApiPage");
    typeImports.add("PageQuery");
    return `export function ${route.operationId}(query?: PageQuery): Promise<ApiPage<${config.singularName}Record>> {
  return apiRequest<ApiPage<${config.singularName}Record>>(apiEndpointPath(${operationKey}), { query });
}`;
  }

  if (route.hasId) {
    typeImports.add("EntityId");
  }

  if (route.method === "DELETE") {
    return `export function ${route.operationId}(id: EntityId): Promise<unknown> {
${endpointLine}
  return apiRequest(apiEndpointPath(${operationKey}, { id }), { method: endpoint.method });
}`;
  }

  if (route.method === "GET") {
    const params = route.hasId ? "id: EntityId" : "";
    const pathParams = route.hasId ? ", { id }" : "";
    return `export function ${route.operationId}(${params}): Promise<${config.singularName}Record> {
  return apiRequest<${config.singularName}Record>(apiEndpointPath(${operationKey}${pathParams}));
}`;
  }

  const params = route.hasId ? "id: EntityId, payload: Record<string, unknown>" : "payload: Record<string, unknown>";
  const pathParams = route.hasId ? ", { id }" : "";
  return `export function ${route.operationId}(${params}): Promise<${config.singularName}Record> {
${endpointLine}
  return apiRequest<${config.singularName}Record, Record<string, unknown>>(apiEndpointPath(${operationKey}${pathParams}), {
    body: payload,
    method: endpoint.method
  });
}`;
}

function renderRoutes(config) {
  return `import { MENU_CODES, PERMISSIONS } from "@/app/permissions";

export const ${config.camelName}Routes = [
  {
    label: ${JSON.stringify(config.label)},
    loader: () => import("./pages/${config.pascalName}Page.jsx").then((module) => module.${config.pascalName}Page),
    menuCode: MENU_CODES.${config.menuKey},
    pageKey: ${JSON.stringify(config.pageKey)},
    path: ${JSON.stringify(config.routePath)},
    permission: PERMISSIONS.${config.viewPermission.key}
  }
];
`;
}

function renderFeaturePermissions(config) {
  const entries = config.permissions
    .map((permission) => `    ${abilityName(permission.action)}: can(PERMISSIONS.${permission.key})`)
    .join(",\n");

  return `import { useAuth } from "@/app/auth/AuthProvider.jsx";
import { PERMISSIONS } from "@/app/permissions";

export function use${config.singularName}Abilities() {
  const { can } = useAuth();

  return {
${entries}
  };
}
`;
}

function renderSchema(config) {
  return `import { z } from "zod";

export const ${config.singularCamel}FormSchema = z.record(z.string(), z.unknown());

export type ${config.singularName}Form = z.infer<typeof ${config.singularCamel}FormSchema>;
`;
}

function renderPageHook(config) {
  const listRoute = config.apiRoutes.find((route) => route.kind === "list") || config.apiRoutes[0];
  return `import { useCallback } from "react";
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
import { ${listRoute.operationId} } from "@/features/${config.name}/api";
import { use${config.singularName}Abilities } from "@/features/${config.name}/permissions.js";

const emptyData = { items: [], total: 0, page: 1, pageSize: 10 };

export function use${config.pascalName}Page() {
  const abilities = use${config.singularName}Abilities();
  const queryFn = useCallback(() => ${listRoute.operationId}({ page: 1, page_size: 10 }), []);
  const { data, error, loading, reload } = useAdminQuery(queryFn, {
    errorMessage: "加载失败",
    initialData: emptyData,
    queryKey: [${JSON.stringify(config.name)}]
  });

  return { abilities, data, error, loading, reload };
}
`;
}

function renderPage(config) {
  return `import { DataState } from "@/shared/ui/DataState.jsx";
import { use${config.pascalName}Page } from "@/features/${config.name}/hooks/use${config.pascalName}Page.js";
import styles from "@/features/${config.name}/${config.name}.module.css";

export function ${config.pascalName}Page() {
  const page = use${config.pascalName}Page();

  return (
    <DataState error={page.error} loading={page.loading} onRetry={page.reload}>
      <section className={styles.root} />
    </DataState>
  );
}
`;
}

function renderCss() {
  return `.root {
  min-height: 100%;
}
`;
}


function renderPermissionChecklist(config) {
  const rows = config.permissions
    .map((permission) => `| \`${permission.code}\` | ${permission.kind} | ${permission.name} |`)
    .join("\n");
  const routeRows = config.apiRoutes
    .map((route) => `| ${route.method} | \`${route.path}\` | \`${route.permission || "登录态"}\` | ${route.handler} |`)
    .join("\n");
  return `# ${config.label}

## 权限

| Code | Kind | Name |
| --- | --- | --- |
${rows}

## API 路由

| Method | Path | Permission | Handler |
| --- | --- | --- | --- |
${routeRows}
`;
}

function normalizePermissions(value, context) {
  return value.map((item) => {
    const permission = typeof item === "string" ? { action: item } : item;
    const action = permission.action || permission.key;
    const code = permission.code || `${context.permissionPrefix}:${action}`;
    const key = permission.permissionKey || toPermissionKey(code);
    return {
      action,
      code,
      key,
      kind: permission.kind || (action === "view" ? "menu" : "button"),
      name: permission.name || `${context.label}${actionLabel(action)}`
    };
  });
}

function normalizeApiRoutes(value, context) {
  const permissionByAction = new Map(context.permissions.map((permission) => [permission.action, permission.code]));
  return value.map((route) => {
    const method = (route.method || "GET").toUpperCase();
    const routePath = route.path || context.routePath;
    const permission = route.permission || permissionByAction.get(route.action || "view") || "";
    const operationId = route.operationId || inferOperationId({ method, routePath, route, context });
    return {
      handler: route.handler || toPascalCase(operationId),
      hasId: routePath.includes("{id}") || routePath.includes(":id"),
      kind: route.kind || inferRouteKind(method, routePath),
      method,
      operationId,
      path: toOpenApiPath(routePath),
      permission,
      raw: Boolean(route.raw) || routePath.includes("export")
    };
  });
}

function defaultActions() {
  return ["view", "create", "update", "delete", "export"];
}

function defaultApiRoutes(routePath, viewPermission) {
  return [{ action: "view", kind: "list", method: "GET", path: routePath, permission: viewPermission }];
}

function inferOperationId({ method, routePath, route, context }) {
  if (route.action === "export" || routePath.includes("export")) {
    return `export${context.pascalName}`;
  }
  if (route.kind === "list" || (method === "GET" && !routePath.includes("{id}") && !routePath.includes(":id"))) {
    return `list${context.pascalName}`;
  }
  if (method === "GET") {
    return `get${context.singularName}`;
  }
  if (method === "POST") {
    return routePath.includes(":id") || routePath.includes("{id}") ? `${route.action || "run"}${context.singularName}` : `create${context.singularName}`;
  }
  if (method === "PATCH" || method === "PUT") {
    return `update${context.singularName}`;
  }
  if (method === "DELETE") {
    return `delete${context.singularName}`;
  }
  return `${method.toLowerCase()}${context.singularName}`;
}

function inferRouteKind(method, routePath) {
  if (method === "GET" && !routePath.includes("{id}") && !routePath.includes(":id")) {
    return "list";
  }
  return "item";
}

function insertObjectEntries(source, objectName, entries) {
  const objectStart = source.indexOf(`export const ${objectName} = {`);
  if (objectStart < 0) {
    throw new Error(`Cannot find ${objectName} in permissions.ts`);
  }
  const objectEnd = source.indexOf("\n} as const;", objectStart);
  if (objectEnd < 0) {
    throw new Error(`Cannot find ${objectName} closing in permissions.ts`);
  }

  const before = source.slice(0, objectEnd);
  const after = source.slice(objectEnd);
  const existingBlock = source.slice(objectStart, objectEnd);
  const additions = entries
    .filter((entry) => !existingBlock.includes(`${entry.key}:`) && !existingBlock.includes(`"${entry.value}"`))
    .map((entry) => `  ${entry.key}: "${entry.value}"`);

  if (!additions.length) {
    return source;
  }

  const separator = before.trimEnd().endsWith(",") ? "\n" : ",\n";
  return `${before}${separator}${additions.join(",\n")}${after}`;
}

function planWrite(filePath, content, options) {
  plannedWrites.push({
    content,
    exists: existsSync(filePath),
    filePath,
    overwrite: options.overwrite
  });
}

function run(command, commandArgs) {
  const result = spawnSync(command, commandArgs, { cwd: rootDir, stdio: "inherit" });
  if (result.status !== 0) {
    process.exit(result.status || 1);
  }
}

function required(value, name) {
  if (!value) {
    throw new Error(`Missing required config field: ${name}`);
  }
  return value;
}

function pathParams(routePath) {
  return [...routePath.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
}

function toOpenApiPath(routePath) {
  return routePath.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
}


function toKebabCase(value) {
  return value
    .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
    .replace(/[^a-zA-Z0-9]+/g, "-")
    .replace(/^-|-$/g, "")
    .toLowerCase();
}

function toCamelCase(value) {
  return value.replace(/-([a-z0-9])/g, (_, letter) => letter.toUpperCase());
}

function toPascalCase(value) {
  const camel = toCamelCase(toKebabCase(value));
  return camel.charAt(0).toUpperCase() + camel.slice(1);
}

function lowerFirst(value) {
  return value.charAt(0).toLowerCase() + value.slice(1);
}

function singularize(value) {
  return value.endsWith("ies") ? `${value.slice(0, -3)}y` : value.endsWith("s") ? value.slice(0, -1) : value;
}

function toPermissionKey(code) {
  return toCamelCase(code.replace(/:/g, "-"));
}

function abilityName(action) {
  return `can${toPascalCase(action)}`;
}

function actionLabel(action) {
  const labels = {
    audit: "审核",
    config: "配置",
    create: "创建",
    delete: "删除",
    export: "导出",
    import: "导入",
    status: "状态变更",
    update: "更新",
    view: "查看"
  };
  return labels[action] || action;
}

function sortObject(object, mapValue = (value) => value, preferredOrder = []) {
  return Object.fromEntries(
    Object.entries(object)
      .sort(([left], [right]) => {
        const leftIndex = preferredOrder.indexOf(left);
        const rightIndex = preferredOrder.indexOf(right);
        if (leftIndex >= 0 || rightIndex >= 0) {
          return (leftIndex < 0 ? Number.MAX_SAFE_INTEGER : leftIndex) - (rightIndex < 0 ? Number.MAX_SAFE_INTEGER : rightIndex);
        }
        return left.localeCompare(right);
      })
      .map(([key, value]) => [key, mapValue(value)])
  );
}
