import "reflect-metadata";
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import * as path from "node:path";
import ts from "typescript";
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "../app.module";

type HttpMethod =
  | "get"
  | "post"
  | "put"
  | "patch"
  | "delete"
  | "options"
  | "head"
  | "all";

interface TsDecoratorMeta {
  name: string;
  firstArg?: string;
}

interface TsParameterMeta {
  name: string;
  type: string | null;
  decorators: TsDecoratorMeta[];
}

interface TsMethodMeta {
  operationId: string;
  controller: string;
  controllerRoute: string;
  methodName: string;
  httpMethod: HttpMethod;
  routePath: string;
  returnType: string | null;
  hasPublicDecorator: boolean;
  methodDecorators: string[];
  params: TsParameterMeta[];
  filePath: string;
}

const HTTP_DECORATOR_TO_METHOD: Record<string, HttpMethod> = {
  Get: "get",
  Post: "post",
  Put: "put",
  Patch: "patch",
  Delete: "delete",
  Options: "options",
  Head: "head",
  All: "all",
};

function getDecorators(node: ts.Node): readonly ts.Decorator[] {
  return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
}

function parseDecorator(
  decorator: ts.Decorator,
  sourceFile: ts.SourceFile,
): TsDecoratorMeta | null {
  const expression = decorator.expression;

  if (ts.isIdentifier(expression)) {
    return { name: expression.text };
  }

  if (ts.isCallExpression(expression)) {
    const called = expression.expression;
    if (!ts.isIdentifier(called)) {
      return null;
    }

    const firstArg = expression.arguments[0];
    let firstArgText: string | undefined;
    if (firstArg) {
      if (
        ts.isStringLiteral(firstArg) ||
        ts.isNoSubstitutionTemplateLiteral(firstArg)
      ) {
        firstArgText = firstArg.text;
      } else {
        firstArgText = firstArg.getText(sourceFile);
      }
    }

    return {
      name: called.text,
      firstArg: firstArgText,
    };
  }

  return null;
}

function collectControllerFiles(dirPath: string): string[] {
  const files: string[] = [];
  const entries = readdirSync(dirPath, { withFileTypes: true });

  for (const entry of entries) {
    const absolutePath = path.join(dirPath, entry.name);
    if (entry.isDirectory()) {
      files.push(...collectControllerFiles(absolutePath));
      continue;
    }

    if (entry.isFile() && entry.name.endsWith(".controller.ts")) {
      files.push(absolutePath);
    }
  }

  return files;
}

function normalizeRoutePart(value: string | undefined): string {
  if (!value || value === "''" || value === '""') {
    return "";
  }
  return value.trim().replace(/^\/+|\/+$/g, "");
}

function buildSwaggerPath(
  globalPrefix: string,
  controllerRoute: string,
  routePath: string,
): string {
  const parts = [
    normalizeRoutePart(globalPrefix),
    normalizeRoutePart(controllerRoute),
    normalizeRoutePart(routePath),
  ].filter(Boolean);

  return `/${parts.join("/")}`;
}

function collectTsEndpointMetadata(
  projectRoot: string,
): Map<string, TsMethodMeta> {
  const modulesDir = path.join(projectRoot, "src", "modules");
  const controllerFiles = collectControllerFiles(modulesDir);
  const metadataByOperationId = new Map<string, TsMethodMeta>();

  for (const filePath of controllerFiles) {
    const sourceText = readFileSync(filePath, "utf8");
    const sourceFile = ts.createSourceFile(
      filePath,
      sourceText,
      ts.ScriptTarget.Latest,
      true,
      ts.ScriptKind.TS,
    );

    ts.forEachChild(sourceFile, (node) => {
      if (!ts.isClassDeclaration(node) || !node.name) {
        return;
      }

      const className = node.name.text;
      const classDecorators = getDecorators(node)
        .map((decorator) => parseDecorator(decorator, sourceFile))
        .filter((decorator): decorator is TsDecoratorMeta =>
          Boolean(decorator),
        );

      const controllerDecorator = classDecorators.find(
        (decorator) => decorator.name === "Controller",
      );
      if (!controllerDecorator) {
        return;
      }

      const controllerRoute = normalizeRoutePart(controllerDecorator.firstArg);

      for (const member of node.members) {
        if (!ts.isMethodDeclaration(member) || !member.name) {
          continue;
        }

        const methodDecorators = getDecorators(member)
          .map((decorator) => parseDecorator(decorator, sourceFile))
          .filter((decorator): decorator is TsDecoratorMeta =>
            Boolean(decorator),
          );

        const httpDecorator = methodDecorators.find(
          (decorator) => decorator.name in HTTP_DECORATOR_TO_METHOD,
        );
        if (!httpDecorator) {
          continue;
        }

        const methodName = member.name.getText(sourceFile);
        const httpMethod = HTTP_DECORATOR_TO_METHOD[httpDecorator.name];
        const routePath = normalizeRoutePart(httpDecorator.firstArg);
        const returnType = member.type
          ? member.type.getText(sourceFile).trim()
          : null;

        const params: TsParameterMeta[] = member.parameters.map((param) => {
          const paramDecorators = getDecorators(param)
            .map((decorator) => parseDecorator(decorator, sourceFile))
            .filter((decorator): decorator is TsDecoratorMeta =>
              Boolean(decorator),
            );

          return {
            name: param.name.getText(sourceFile),
            type: param.type ? param.type.getText(sourceFile).trim() : null,
            decorators: paramDecorators,
          };
        });

        const operationId = `${className}_${methodName}`;
        metadataByOperationId.set(operationId, {
          operationId,
          controller: className,
          controllerRoute,
          methodName,
          httpMethod,
          routePath,
          returnType,
          hasPublicDecorator: methodDecorators.some(
            (decorator) => decorator.name === "Public",
          ),
          methodDecorators: methodDecorators.map((decorator) => decorator.name),
          params,
          filePath,
        });
      }
    });
  }

  return metadataByOperationId;
}

function refName(ref?: string): string | null {
  if (!ref || typeof ref !== "string") {
    return null;
  }
  const parts = ref.split("/");
  return parts[parts.length - 1] ?? null;
}

function collectSchemaRefs(
  value: unknown,
  refs = new Set<string>(),
): Set<string> {
  if (!value || typeof value !== "object") {
    return refs;
  }

  const recordValue = value as Record<string, unknown>;
  const maybeRef = recordValue.$ref;
  if (typeof maybeRef === "string") {
    const name = refName(maybeRef);
    if (name) {
      refs.add(name);
    }
  }

  for (const nested of Object.values(recordValue)) {
    collectSchemaRefs(nested, refs);
  }

  return refs;
}

function schemaTypeSummary(schema: unknown): string {
  if (!schema || typeof schema !== "object") {
    return "unknown";
  }

  const schemaObj = schema as Record<string, unknown>;
  if (typeof schemaObj.$ref === "string") {
    return refName(schemaObj.$ref) ?? "unknown";
  }

  const type = typeof schemaObj.type === "string" ? schemaObj.type : "object";
  if (type === "array") {
    return `array<${schemaTypeSummary(schemaObj.items)}>`;
  }

  if (Array.isArray(schemaObj.enum) && schemaObj.enum.length > 0) {
    return `${type}(${schemaObj.enum.join(" | ")})`;
  }

  return type;
}

function normalizeParameters(parameters: unknown[] = []) {
  const parsed = parameters
    .map((parameter) => parameter as Record<string, unknown>)
    .filter(Boolean)
    .map((parameter) => {
      const schema = (parameter.schema ?? {}) as Record<string, unknown>;
      return {
        name: typeof parameter.name === "string" ? parameter.name : "",
        in: typeof parameter.in === "string" ? parameter.in : "",
        required: Boolean(parameter.required),
        description:
          typeof parameter.description === "string"
            ? parameter.description
            : null,
        type: schemaTypeSummary(schema),
        enum: Array.isArray(schema.enum) ? schema.enum : [],
        default: schema.default ?? null,
        format: typeof schema.format === "string" ? schema.format : null,
      };
    });

  return {
    path: parsed.filter((item) => item.in === "path"),
    query: parsed.filter((item) => item.in === "query"),
    header: parsed.filter((item) => item.in === "header"),
    cookie: parsed.filter((item) => item.in === "cookie"),
  };
}

function normalizeRequestBody(requestBody: unknown) {
  if (!requestBody || typeof requestBody !== "object") {
    return null;
  }

  const requestBodyObj = requestBody as Record<string, unknown>;
  if (typeof requestBodyObj.$ref === "string") {
    return {
      required: false,
      contentTypes: [],
      schemaTypes: [],
      schemaRefs: [refName(requestBodyObj.$ref)].filter(Boolean),
      raw: requestBodyObj,
    };
  }

  const content = (requestBodyObj.content ?? {}) as Record<
    string,
    Record<string, unknown>
  >;
  const contentTypes = Object.keys(content);
  const schemaTypes: string[] = [];
  const refs = new Set<string>();

  for (const mediaType of Object.values(content)) {
    const schema = mediaType.schema;
    schemaTypes.push(schemaTypeSummary(schema));
    collectSchemaRefs(schema, refs);
  }

  return {
    required: Boolean(requestBodyObj.required),
    contentTypes,
    schemaTypes,
    schemaRefs: [...refs].sort(),
    raw: requestBodyObj,
  };
}

function normalizeResponses(responses: Record<string, unknown>) {
  return Object.entries(responses)
    .sort((a, b) => Number(a[0]) - Number(b[0]))
    .map(([statusCode, response]) => {
      const responseObj = response as Record<string, unknown>;
      const content = (responseObj.content ?? {}) as Record<
        string,
        Record<string, unknown>
      >;
      const contentTypes = Object.keys(content);
      const refs = new Set<string>();
      const schemaTypes: string[] = [];

      for (const mediaType of Object.values(content)) {
        const schema = mediaType.schema;
        schemaTypes.push(schemaTypeSummary(schema));
        collectSchemaRefs(schema, refs);
      }

      return {
        status: Number(statusCode),
        description:
          typeof responseObj.description === "string"
            ? responseObj.description
            : "",
        contentTypes,
        schemaTypes,
        schemaRefs: [...refs].sort(),
        hasSchema: contentTypes.length > 0,
        raw: responseObj,
      };
    });
}

async function run() {
  const projectRoot = process.cwd();
  const outputDir = path.join(projectRoot, "mds");
  const outputFile = path.join(
    outputDir,
    "backend_endpoints_swagger_summary.json",
  );

  // Predictions module is conditionally loaded with REDIS_ENABLED in AppModule.
  // Force-enable here to include all backend endpoints in one Swagger export.
  process.env.REDIS_ENABLED = "true";

  const tsMetadata = collectTsEndpointMetadata(projectRoot);

  const app = await NestFactory.create(AppModule, { logger: false });
  app.setGlobalPrefix("api");

  const swaggerConfig = new DocumentBuilder()
    .setTitle("Suggest Bet Backend API")
    .setDescription("Auto-generated endpoint summary from Swagger document")
    .setVersion("1.0")
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, swaggerConfig);
  const paths = document.paths ?? {};

  const endpoints: Array<Record<string, unknown>> = [];
  const seenOperationIds = new Set<string>();
  const globalPrefix = "api";

  const sortedPaths = Object.keys(paths).sort((a, b) => a.localeCompare(b));
  for (const endpointPath of sortedPaths) {
    const pathItem = paths[endpointPath] as Record<string, unknown>;

    const methods = Object.keys(pathItem)
      .filter((method) =>
        ["get", "post", "put", "patch", "delete", "options", "head"].includes(
          method,
        ),
      )
      .sort((a, b) => a.localeCompare(b));

    for (const method of methods) {
      const operation = pathItem[method] as Record<string, unknown>;
      const operationId =
        typeof operation.operationId === "string" ? operation.operationId : "";

      if (operationId) {
        seenOperationIds.add(operationId);
      }

      const tsMeta = operationId ? tsMetadata.get(operationId) : undefined;
      const tags = Array.isArray(operation.tags)
        ? operation.tags.map((tag) => String(tag))
        : [];

      const parameters = normalizeParameters(
        Array.isArray(operation.parameters) ? operation.parameters : [],
      );
      const requestBody = normalizeRequestBody(operation.requestBody);
      const responses = normalizeResponses(
        (operation.responses ?? {}) as Record<string, unknown>,
      );
      const security = Array.isArray(operation.security)
        ? operation.security
        : [];
      const securitySchemes = security.flatMap((rule) =>
        Object.keys((rule ?? {}) as Record<string, unknown>),
      );

      const tsBodyParams =
        tsMeta?.params
          .filter((param) =>
            param.decorators.some((decorator) => decorator.name === "Body"),
          )
          .map((param) => ({
            name: param.name,
            type: param.type,
            bodyKey:
              param.decorators.find((decorator) => decorator.name === "Body")
                ?.firstArg ?? null,
          })) ?? [];

      endpoints.push({
        inSwagger: true,
        operationId,
        method: method.toUpperCase(),
        path: endpointPath,
        tag: tags[0] ?? null,
        tags,
        summary:
          typeof operation.summary === "string" ? operation.summary : null,
        description:
          typeof operation.description === "string"
            ? operation.description
            : null,
        auth: {
          swaggerSecurityRequired: security.length > 0,
          swaggerSecuritySchemes: [...new Set(securitySchemes)].sort(),
          hasPublicDecorator: tsMeta?.hasPublicDecorator ?? false,
        },
        request: {
          parameters,
          body: requestBody,
          tsBodyParams,
        },
        response: {
          tsReturnType: tsMeta?.returnType ?? null,
          statuses: responses,
        },
        source: tsMeta
          ? {
              controller: tsMeta.controller,
              methodName: tsMeta.methodName,
              filePath: path.relative(projectRoot, tsMeta.filePath),
            }
          : null,
      });
    }
  }

  // Add controller methods that are not present in Swagger document.
  for (const [operationId, tsMeta] of tsMetadata.entries()) {
    if (seenOperationIds.has(operationId)) {
      continue;
    }

    endpoints.push({
      inSwagger: false,
      operationId,
      method: tsMeta.httpMethod.toUpperCase(),
      path: buildSwaggerPath(
        globalPrefix,
        tsMeta.controllerRoute,
        tsMeta.routePath,
      ),
      tag: tsMeta.controller.replace(/Controller$/, ""),
      tags: [tsMeta.controller.replace(/Controller$/, "")],
      summary: null,
      description: "Not present in generated Swagger document",
      auth: {
        swaggerSecurityRequired: null,
        swaggerSecuritySchemes: [],
        hasPublicDecorator: tsMeta.hasPublicDecorator,
      },
      request: {
        parameters: {
          path: [],
          query: [],
          header: [],
          cookie: [],
        },
        body: null,
        tsBodyParams: tsMeta.params
          .filter((param) =>
            param.decorators.some((decorator) => decorator.name === "Body"),
          )
          .map((param) => ({
            name: param.name,
            type: param.type,
            bodyKey:
              param.decorators.find((decorator) => decorator.name === "Body")
                ?.firstArg ?? null,
          })),
      },
      response: {
        tsReturnType: tsMeta.returnType,
        statuses: [],
      },
      source: {
        controller: tsMeta.controller,
        methodName: tsMeta.methodName,
        filePath: path.relative(projectRoot, tsMeta.filePath),
      },
    });
  }

  endpoints.sort((a, b) => {
    const pathA = typeof a.path === "string" ? a.path : "";
    const pathB = typeof b.path === "string" ? b.path : "";
    if (pathA !== pathB) {
      return pathA.localeCompare(pathB);
    }
    return (typeof a.method === "string" ? a.method : "").localeCompare(
      typeof b.method === "string" ? b.method : "",
    );
  });

  const tagStats = new Map<string, number>();
  for (const endpoint of endpoints) {
    const tag = typeof endpoint.tag === "string" ? endpoint.tag : "Unknown";
    tagStats.set(tag, (tagStats.get(tag) ?? 0) + 1);
  }

  const referencedSchemas = new Set<string>();
  for (const endpoint of endpoints) {
    const requestBody = (endpoint.request as Record<string, unknown>)
      .body as Record<string, unknown> | null;
    if (requestBody && Array.isArray(requestBody.schemaRefs)) {
      for (const schemaName of requestBody.schemaRefs) {
        if (typeof schemaName === "string") {
          referencedSchemas.add(schemaName);
        }
      }
    }

    const statuses = (endpoint.response as Record<string, unknown>)
      .statuses as Array<Record<string, unknown>>;
    for (const status of statuses ?? []) {
      if (!Array.isArray(status.schemaRefs)) {
        continue;
      }
      for (const schemaName of status.schemaRefs) {
        if (typeof schemaName === "string") {
          referencedSchemas.add(schemaName);
        }
      }
    }
  }

  const allSchemas = (document.components?.schemas ?? {}) as Record<
    string,
    unknown
  >;
  const schemaSnapshots: Record<string, unknown> = {};
  for (const schemaName of [...referencedSchemas].sort((a, b) =>
    a.localeCompare(b),
  )) {
    if (allSchemas[schemaName]) {
      schemaSnapshots[schemaName] = allSchemas[schemaName];
    }
  }

  const summary = {
    generatedAt: new Date().toISOString(),
    generatedBy: "src/scripts/export-swagger-endpoints-summary.ts",
    project: "Suggest-Bet-BE",
    swagger: {
      docsPath: "/api/docs",
      globalPrefix: "/api",
      endpointCountInSwagger: endpoints.filter((item) => item.inSwagger).length,
      endpointCountTotal: endpoints.length,
      warnings: [
        "Swagger output reflects loaded modules for current environment.",
        "This export forces REDIS_ENABLED=true to include conditional Prediction endpoints.",
      ],
    },
    stats: {
      byTag: [...tagStats.entries()]
        .sort((a, b) => a[0].localeCompare(b[0]))
        .map(([tag, count]) => ({ tag, count })),
      endpointsWithoutSummary: endpoints
        .filter((endpoint) => !endpoint.summary)
        .map((endpoint) => ({
          operationId: endpoint.operationId,
          method: endpoint.method,
          path: endpoint.path,
        })),
      endpointsWithoutResponseSchema: endpoints
        .filter((endpoint) =>
          (
            (endpoint.response as Record<string, unknown>).statuses as Array<
              Record<string, unknown>
            >
          ).some((status) => status.hasSchema === false),
        )
        .map((endpoint) => ({
          operationId: endpoint.operationId,
          method: endpoint.method,
          path: endpoint.path,
        })),
    },
    endpoints,
    referencedSchemas: schemaSnapshots,
  };

  mkdirSync(outputDir, { recursive: true });
  writeFileSync(outputFile, JSON.stringify(summary, null, 2), "utf8");

  await app.close();

  console.log(`✅ Swagger endpoint summary exported: ${outputFile}`);

  console.log(
    `   Endpoints in swagger: ${summary.swagger.endpointCountInSwagger}, total (with TS scan): ${summary.swagger.endpointCountTotal}`,
  );
}

void run().catch((error: unknown) => {
  console.error("❌ Failed to export Swagger endpoint summary");

  console.error(error);
  process.exit(1);
});
