#!/usr/bin/env node
/**
 * Generate REST route modules for each FHIR type that has operations.
 * Run from repo root: node packages/@openhi/constructs/scripts/generate-data-routes.mjs
 */
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
const ROUTES_DATA = path.join(ROOT, "src/data/rest-api/routes/data");
const OPERATIONS = path.join(ROOT, "src/data/operations/data");
const COMMON = path.join(ROOT, "src/data/rest-api/routes/common.ts");
const REST_API = path.join(ROOT, "src/data/rest-api/rest-api.ts");

// Get types that have operations (folder name -> PascalCase type from create-operation import)
const dirs = fs.readdirSync(OPERATIONS).filter((d) => {
  const p = path.join(OPERATIONS, d);
  return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, `${d}-create-operation.ts`));
});

const types = [];
for (const dir of dirs) {
  const createPath = path.join(OPERATIONS, dir, `${dir}-create-operation.ts`);
  const line = fs.readFileSync(createPath, "utf8").split("\n")[0];
  const match = line.match(/import type \{ ([^}]+) \}/);
  if (!match) continue;
  const imports = match[1].split(",").map((s) => s.trim());
  const type = imports.find((t) => t !== "Meta");
  if (type) types.push({ type, folder: dir });
}
types.sort((a, b) => a.type.localeCompare(b.type));

function basePathKey(type) {
  return type.toUpperCase().replace(/-/g, "");
}

function pluralRoute(type) {
  return type + "s";
}

function createRouteName(type, op) {
  const lower = type.charAt(0).toLowerCase() + type.slice(1);
  if (op === "list") return `list${pluralRoute(type)}Route`;
  if (op === "getById") return `get${type}ByIdRoute`;
  if (op === "create") return `create${type}Route`;
  if (op === "update") return `update${type}Route`;
  if (op === "delete") return `delete${type}Route`;
  return null;
}

function opName(type, op) {
  if (op === "list") return `list${pluralRoute(type)}Operation`;
  if (op === "getById") return `get${type}ByIdOperation`;
  if (op === "create") return `create${type}Operation`;
  if (op === "update") return `update${type}Operation`;
  if (op === "delete") return `delete${type}Operation`;
  return null;
}

for (const { type, folder } of types) {
  const routeDir = path.join(ROUTES_DATA, folder);
  const routerFile = path.join(routeDir, `${folder}.ts`);
  if (fs.existsSync(routerFile)) continue;

  fs.mkdirSync(routeDir, { recursive: true });
  const basePath = `/${type}`;
  const key = basePathKey(type);

  const createContent = `import type { ${type} } from "@openhi/types";
import type { Request, Response } from "express";
import { create${type}Operation } from "../../../../operations/data/${folder}/${folder}-create-operation";
import {
  BASE_PATH,
  requireJsonBodyAs,
  sendOperationOutcome500,
} from "../../common";

/** POST ${basePath} — create: accepts ${type} in body, persists via data layer, returns 201. */
export async function create${type}Route(
  req: Request,
  res: Response,
): Promise<Response> {
  const bodyResult = requireJsonBodyAs<${type}>(req, res);
  if ("errorResponse" in bodyResult) return bodyResult.errorResponse;

  const ctx = req.openhiContext!;
  const body = bodyResult.body;
  const resource: ${type} = {
    ...body,
    resourceType: "${type}",
  } as ${type};

  try {
    const result = await create${type}Operation({
      context: ctx,
      body: resource,
    });
    return res
      .status(201)
      .location(\`\${BASE_PATH.${key}}/\${result.id}\`)
      .json(result.resource);
  } catch (err: unknown) {
    return sendOperationOutcome500(res, err, "POST ${type} error:");
  }
}
`;

  const updateContent = `import type { ${type} } from "@openhi/types";
import type { Request, Response } from "express";
import { NotFoundError, domainErrorToHttpStatus } from "../../../../errors";
import { update${type}Operation } from "../../../../operations/data/${folder}/${folder}-update-operation";
import {
  requireJsonBodyAs,
  sendOperationOutcome404,
  sendOperationOutcome500,
} from "../../common";

/** PUT ${basePath}/:id — update: accepts ${type} in body, persists via data layer, returns 200. */
export async function update${type}Route(
  req: Request,
  res: Response,
): Promise<Response> {
  const bodyResult = requireJsonBodyAs<${type}>(req, res);
  if ("errorResponse" in bodyResult) return bodyResult.errorResponse;

  const id = String(req.params.id);
  const ctx = req.openhiContext!;
  const body = bodyResult.body;
  const resource: ${type} = {
    ...body,
    resourceType: "${type}",
    id,
  } as ${type};

  try {
    const result = await update${type}Operation({
      context: ctx,
      id,
      body: resource,
    });
    return res.json(result.resource);
  } catch (err: unknown) {
    const status = domainErrorToHttpStatus(err);
    if (status === 404) {
      const diagnostics =
        err instanceof NotFoundError ? err.message : \`${type} \${id} not found\`;
      return sendOperationOutcome404(res, diagnostics);
    }
    return sendOperationOutcome500(res, err, "PUT ${type} error:");
  }
}
`;

  const listContent = `import type { Request, Response } from "express";
import { list${pluralRoute(type)}Operation } from "../../../../operations/data/${folder}/${folder}-list-operation";
import {
  BASE_PATH,
  buildSearchsetBundle,
  sendOperationOutcome500,
} from "../../common";

/**
 * GET ${basePath} — search/list: returns a FHIR Bundle (searchset) from the data store.
 */
export async function list${pluralRoute(type)}Route(
  req: Request,
  res: Response,
): Promise<Response> {
  const ctx = req.openhiContext!;

  try {
    const result = await list${pluralRoute(type)}Operation({ context: ctx });
    const bundle = buildSearchsetBundle(BASE_PATH.${key}, result.entries);
    return res.json(bundle);
  } catch (err: unknown) {
    return sendOperationOutcome500(res, err, "GET ${basePath} list error:");
  }
}
`;

  const getByIdContent = `import type { Request, Response } from "express";
import { NotFoundError, domainErrorToHttpStatus } from "../../../../errors";
import { get${type}ByIdOperation } from "../../../../operations/data/${folder}/${folder}-get-by-id-operation";
import { sendOperationOutcome404, sendOperationOutcome500 } from "../../common";

/** GET ${basePath}/:id — read: returns a single ${type} resource from the data store or 404. */
export async function get${type}ByIdRoute(
  req: Request,
  res: Response,
): Promise<Response> {
  const id = String(req.params.id);
  const ctx = req.openhiContext!;

  try {
    const result = await get${type}ByIdOperation({ context: ctx, id });
    return res.json(result.resource);
  } catch (err: unknown) {
    const status = domainErrorToHttpStatus(err);
    if (status === 404) {
      const diagnostics =
        err instanceof NotFoundError ? err.message : \`${type} \${id} not found\`;
      return sendOperationOutcome404(res, diagnostics);
    }
    return sendOperationOutcome500(res, err, "GET ${type} error:");
  }
}
`;

  const deleteContent = `import type { Request, Response } from "express";
import { delete${type}Operation } from "../../../../operations/data/${folder}/${folder}-delete-operation";
import { sendOperationOutcome500 } from "../../common";

/** DELETE ${basePath}/:id — delete: removes from data store, returns 204. */
export async function delete${type}Route(
  req: Request,
  res: Response,
): Promise<Response> {
  const id = String(req.params.id);
  const ctx = req.openhiContext!;

  try {
    await delete${type}Operation({ context: ctx, id });
    return res.status(204).send();
  } catch (err: unknown) {
    return sendOperationOutcome500(res, err, "DELETE ${type} error:");
  }
}
`;

  const routerContent = `import express from "express";
import { create${type}Route } from "./${folder}-create-route";
import { delete${type}Route } from "./${folder}-delete-route";
import { get${type}ByIdRoute } from "./${folder}-get-by-id-route";
import { list${pluralRoute(type)}Route } from "./${folder}-list-route";
import { update${type}Route } from "./${folder}-update-route";

/**
 * ${type} REST router: ${basePath}
 * FHIR R4 ${type} resource.
 */
const router: express.Router = express.Router();

router.get("/", list${pluralRoute(type)}Route);
router.get("/:id", get${type}ByIdRoute);
router.post("/", create${type}Route);
router.put("/:id", update${type}Route);
router.delete("/:id", delete${type}Route);

export { router as ${folder}Router };
`;

  fs.writeFileSync(path.join(routeDir, `${folder}-create-route.ts`), createContent);
  fs.writeFileSync(path.join(routeDir, `${folder}-update-route.ts`), updateContent);
  fs.writeFileSync(path.join(routeDir, `${folder}-list-route.ts`), listContent);
  fs.writeFileSync(path.join(routeDir, `${folder}-get-by-id-route.ts`), getByIdContent);
  fs.writeFileSync(path.join(routeDir, `${folder}-delete-route.ts`), deleteContent);
  fs.writeFileSync(routerFile, routerContent);
  console.log(`Generated routes for ${type}`);
}

// Update common.ts BASE_PATH: collect all keys from generated + existing
const existingCommon = fs.readFileSync(COMMON, "utf8");
const basePathMatch = existingCommon.match(/export const BASE_PATH = \{([^}]+)\} as const;/s);
const existingKeys = new Set();
if (basePathMatch) {
  basePathMatch[1].split("\n").forEach((line) => {
    const m = line.match(/([A-Z_]+):/);
    if (m) existingKeys.add(m[1]);
  });
}
for (const { type } of types) {
  existingKeys.add(basePathKey(type));
}
const sortedKeys = [...existingKeys].sort();
const basePathEntries = sortedKeys.map((k) => `  ${k}: "/${k.charAt(0) + k.slice(1).toLowerCase().replace(/_/g, "")}",`).join("\n");
// Key to path: ACCOUNT -> /Account, ACTIVITY_DEFINITION -> /ActivityDefinition. So we need type from key.
// Actually BASE_PATH.PATIENT = "/Patient", so key PATIENT -> /Patient. So path is "/" + key with first letter upper and rest lower... but ACTIVITYDEFINITION -> /Activitydefinition. We need the actual type name. So we need a map from key to type. Keys in the code are the type in uppercase: PATIENT, ACCOUNT, ACTIVITYDEFINITION. So path = "/" + type (PascalCase). So we need to keep type when building. Let me build from types array.
const keyToPath = {};
for (const { type } of types) {
  keyToPath[basePathKey(type)] = `/${type}`;
}
// Add existing
keyToPath.CONFIGURATION = "/Configuration";
keyToPath.ENCOUNTER = "/Encounter";
keyToPath.PATIENT = "/Patient";
keyToPath.PRACTITIONER = "/Practitioner";
const sortedKeys2 = Object.keys(keyToPath).sort();
const basePathLines = sortedKeys2.map((k) => `  ${k}: "${keyToPath[k]}",`).join("\n");
const newCommon = existingCommon.replace(
  /export const BASE_PATH = \{[\s\S]*?\} as const;/,
  `export const BASE_PATH = {\n${basePathLines}\n} as const;`
);
fs.writeFileSync(COMMON, newCommon);
console.log("Updated common.ts BASE_PATH");

// Update rest-api.ts: add imports and app.use for all data route folders
const existingRest = fs.readFileSync(REST_API, "utf8");
const dataDirs = fs.readdirSync(ROUTES_DATA).filter((d) => {
  const p = path.join(ROUTES_DATA, d);
  return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, `${d}.ts`));
});
dataDirs.sort();
const paths = dataDirs.map((d) => `"/${d.charAt(0).toUpperCase() + d.slice(1).replace(/([a-z])([A-Z])/g, "$1$2")}"`);
// Path: folder "account" -> "/Account", "activitydefinition" -> "/ActivityDefinition". So we need PascalCase from folder. Folder is lowercase type name. So account -> Account, activitydefinition -> ActivityDefinition. We have the types array with type and folder. So path for folder "account" is "/Account" (type from types find by folder).
const pathForFolder = (folder) => {
  const t = types.find((x) => x.folder === folder);
  return t ? `/${t.type}` : "/" + folder.charAt(0).toUpperCase() + folder.slice(1);
};
const routerImports = dataDirs.map((d) => `import { ${d}Router } from "./routes/data/${d}/${d}";`).join("\n");
const middlewarePaths = ["/Configuration", ...dataDirs.map((d) => pathForFolder(d))].sort();
const appUseLines = dataDirs.map((d) => `app.use("${pathForFolder(d)}", ${d}Router);`).join("\n");
const middlewareArr = "[\n    " + middlewarePaths.map((p) => `"${p}"`).join(",\n    ") + "\n  ]";

let newRest = existingRest;
// Replace openHiContextMiddleware array
newRest = newRest.replace(/app\.use\(\s*\[[\s\S]*?\],\s*openHiContextMiddleware,?\s*\)/, `app.use(\n  ${middlewareArr},\n  openHiContextMiddleware,\n)`);
// Replace imports - keep configurationRouter and add all data routers
newRest = newRest.replace(
  /import \{ configurationRouter \} from "\.\/routes\/control\/configuration\/configuration";\n(import \{ \w+Router \} from "\.\/routes\/data\/[^"]+";\n)*/,
  `import { configurationRouter } from "./routes/control/configuration/configuration";\n${dataDirs.map((d) => `import { ${d}Router } from "./routes/data/${d}/${d}";`).join("\n")}\n`
);
// Replace app.use block - data routers then Configuration
const useComment = (folder) => {
  const t = types.find((x) => x.folder === folder);
  const name = t ? t.type : folder;
  return `// FHIR R4 ${name} resource`;
};
const dataRouterBlock = dataDirs.map((d) => `${useComment(d)}\napp.use("${pathForFolder(d)}", ${d}Router);`).join("\n\n");
newRest = newRest.replace(
  /(\/\/ FHIR R4 .* resource\napp\.use\("\/[^"]+", \w+Router\);?\n)+(\/\/ Control Plane.*\napp\.use\("\/Configuration", configurationRouter\);)/s,
  `${dataRouterBlock}\n\n// Control Plane Configuration resource\napp.use("/Configuration", configurationRouter);`
);
fs.writeFileSync(REST_API, newRest);
console.log("Updated rest-api.ts");
console.log(`Total: ${dataDirs.length} data route modules`);
