import { generateOpenApi } from "@ts-rest/open-api";
import { contract } from "@monkeytype/contracts/index";
import { writeFileSync, mkdirSync } from "fs";
import { EndpointMetadata, PermissionId } from "@monkeytype/contracts/util/api";
import type { OpenAPIObject, OperationObject } from "openapi3-ts";
import {
  RateLimitIds,
  getLimits,
  RateLimiterId,
  Window,
} from "@monkeytype/contracts/rate-limit/index";
import { formatDuration } from "date-fns";

type SecurityRequirementObject = {
  [name: string]: string[];
};

export function getOpenApi(): OpenAPIObject {
  const openApiDocument = generateOpenApi(
    contract,
    {
      openapi: "3.1.0",
      info: {
        title: "Monkeytype API",
        description:
          "Documentation for the endpoints provided by the Monkeytype API server.\n\nNote that authentication is performed with the Authorization HTTP header in the format `Authorization: ApeKey YOUR_APE_KEY`\n\nThere is a rate limit of `30 requests per minute` across all endpoints with some endpoints being more strict. Rate limit rates are shared across all ape keys.",
        version: "2.0.0",
        termsOfService: "https://monkeytype.com/terms-of-service",
        contact: {
          name: "Support",
          email: "support@monkeytype.com",
        },
        "x-logo": {
          url: "https://monkeytype.com/images/mtfulllogo.png",
        },
        license: {
          name: "GPL-3.0",
          url: "https://www.gnu.org/licenses/gpl-3.0.html",
        },
      },
      servers: [
        {
          url: "https://api.monkeytype.com",
          description: "Production server",
        },
      ],
      components: {
        securitySchemes: {
          BearerAuth: {
            type: "http",
            scheme: "bearer",
          },
          ApeKey: {
            type: "http",
            scheme: "ApeKey",
          },
        },
      },
      tags: [
        {
          name: "users",
          description: "User account data.",
          "x-displayName": "Users",
          "x-public": "yes",
        },
        {
          name: "configs",
          description:
            "User specific configs like test settings, theme or tags.",
          "x-displayName": "User configs",
          "x-public": "no",
        },
        {
          name: "presets",
          description: "User specific configuration presets.",
          "x-displayName": "User presets",
          "x-public": "no",
        },
        {
          name: "results",
          description: "User test results",
          "x-displayName": "Test results",
          "x-public": "yes",
        },
        {
          name: "ape-keys",
          description: "Ape keys provide access to certain API endpoints.",
          "x-displayName": "Ape Keys",
          "x-public": "no",
        },
        {
          name: "public",
          description: "Public endpoints such as typing stats.",
          "x-displayName": "Public",
          "x-public": "yes",
        },
        {
          name: "leaderboards",
          description: "All-time and daily leaderboards of the fastest typers.",
          "x-displayName": "Leaderboards",
        },
        {
          name: "connections",
          description: "Connections between users.",
          "x-displayName": "Connections",
          "x-public": "no",
        },
        {
          name: "psas",
          description: "Public service announcements.",
          "x-displayName": "PSAs",
          "x-public": "yes",
        },
        {
          name: "quotes",
          description: "Quote ratings and new quote submissions",
          "x-displayName": "Quotes",
          "x-public": "yes",
        },
        {
          name: "admin",
          description:
            "Various administrative endpoints. Require user to have admin permissions.",
          "x-displayName": "Admin",
          "x-public": "no",
        },
        {
          name: "configuration",
          description: "Server configuration",
          "x-displayName": "Server configuration",
          "x-public": "yes",
        },
        {
          name: "development",
          description:
            "Development related endpoints. Only available on dev environment",
          "x-displayName": "Development",
          "x-public": "no",
        },
        {
          name: "webhooks",
          description: "Endpoints for incoming webhooks.",
          "x-displayName": "Webhooks",
          "x-public": "yes",
        },
      ],
    },

    {
      jsonQuery: true,
      setOperationId: "concatenated-path",
      operationMapper: (operation, route) => {
        const metadata = route.metadata as EndpointMetadata;
        if (!operation.description?.trim()?.endsWith(".")) {
          operation.description += ".";
        }
        operation.description += "\n\n";

        addAuth(operation, metadata);
        addRateLimit(operation, metadata);
        addRequiredConfiguration(operation, metadata);
        addTags(operation, metadata);
        return operation;
      },
    },
  );
  return openApiDocument;
}

function addAuth(
  operation: OperationObject,
  metadata: EndpointMetadata | undefined,
): void {
  const auth = metadata?.authenticationOptions ?? {};
  const permissions = getRequiredPermissions(metadata) ?? [];
  const security: SecurityRequirementObject[] = [];
  if (!auth.isPublic && !auth.isPublicOnDev) {
    security.push({ BearerAuth: permissions });

    if (auth.acceptApeKeys === true) {
      security.push({ ApeKey: permissions });
    }
  }

  const includeInPublic = auth.isPublic === true || auth.acceptApeKeys === true;
  operation["x-public"] = includeInPublic ? "yes" : "no";
  operation.security = security;

  if (permissions.length !== 0) {
    operation.description += `**Required permissions:** ${permissions.join(
      ", ",
    )}\n\n`;
  }
}

function getRequiredPermissions(
  metadata: EndpointMetadata | undefined,
): PermissionId[] | undefined {
  if (metadata === undefined || metadata.requirePermission === undefined) {
    return undefined;
  }

  if (Array.isArray(metadata.requirePermission)) {
    return metadata.requirePermission;
  }
  return [metadata.requirePermission];
}

function addTags(
  operation: OperationObject,
  metadata: EndpointMetadata | undefined,
): void {
  if (metadata === undefined || metadata.openApiTags === undefined) return;
  operation.tags = Array.isArray(metadata.openApiTags)
    ? metadata.openApiTags
    : [metadata.openApiTags];
}

function addRateLimit(
  operation: OperationObject,
  metadata: EndpointMetadata | undefined,
): void {
  if (metadata === undefined || metadata.rateLimit === undefined) return;
  // oxlint-disable-next-line no-unsafe-assignment
  const okResponse = operation.responses["200"];
  if (okResponse === undefined) return;

  operation.description += getRateLimitDescription(metadata.rateLimit);

  // oxlint-disable-next-line no-unsafe-assignment no-unsafe-member-access
  okResponse["headers"] = {
    // oxlint-disable-next-line no-unsafe-member-access
    ...okResponse["headers"],
    "x-ratelimit-limit": {
      schema: { type: "integer" },
      description: "The number of allowed requests in the current period",
    },
    "x-ratelimit-remaining": {
      schema: { type: "integer" },
      description: "The number of remaining requests in the current period",
    },
    "x-ratelimit-reset": {
      schema: { type: "integer" },
      description: "The timestamp of the start of the next period",
    },
  };
}

function getRateLimitDescription(limit: RateLimiterId | RateLimitIds): string {
  const limits = getLimits(limit);

  let result = `**Rate limit:** This operation can be called up to ${
    limits.limiter.max
  } times ${formatWindow(limits.limiter.window)} for regular users`;

  if (limits.apeKeyLimiter !== undefined) {
    result += ` and up to ${limits.apeKeyLimiter.max} times ${formatWindow(
      limits.apeKeyLimiter.window,
    )} with ApeKeys`;
  }

  return result + ".\n\n";
}

function formatWindow(window: Window): string {
  if (typeof window === "number") {
    const seconds = Math.floor(window / 1000);
    const duration = formatDuration({
      hours: Math.floor(seconds / 3600),
      minutes: Math.floor(seconds / 60) % 60,
      seconds: seconds % 60,
    });

    return `every ${duration}`;
  }
  return "per " + window;
}

function addRequiredConfiguration(
  operation: OperationObject,
  metadata: EndpointMetadata | undefined,
): void {
  if (metadata === undefined || metadata.requireConfiguration === undefined) {
    return;
  }

  //@ts-expect-error somehow path doesnt exist
  operation.description += `**Required configuration:** This operation can only be called if the [configuration](#tag/configuration/operation/configuration.get) for  \`${metadata.requireConfiguration.path}\` is \`true\`.\n\n`;
}

//detect if we run this as a main
if (require.main === module) {
  const args = process.argv.slice(2);
  if (args.length !== 1) {
    console.error("Provide filename.");
    process.exit(1);
  }
  const outFile = args[0] as string;

  //create directories if needed
  const lastSlash = outFile.lastIndexOf("/");
  if (lastSlash > 1) {
    const dir = outFile.substring(0, lastSlash);
    mkdirSync(dir, { recursive: true });
  }

  const openapi = getOpenApi();
  writeFileSync(args[0] as string, JSON.stringify(openapi, null, 2));
}
