/*
 * Copyright (C) 2018-2021 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {OpenAPIV3} from 'openapi-types';
import {
  isLightweightClass,
  lightweightProjectFromPath,
  LightweightProjectWithIndex,
} from '@openstapps/easy-ast';
import {RouteInstanceWithMeta, RouteWithMetaInformation} from './types/routes.js';
import {rejectNil} from './util/collections.js';
import {capitalize} from './util/string.js';
import path from 'path';

/**
 * Gather relevant information of routes
 *
 * This gathers the information for all routes that implement the abstract class SCAbstractRoute.
 * Furthermore it instantiates every route and adds it to the information.
 */
export async function gatherRouteInformation(path: string): Promise<RouteWithMetaInformation[]> {
  const project = new LightweightProjectWithIndex(lightweightProjectFromPath(path));

  // find all classes that implement the SCAbstractRoute
  return rejectNil(
    await Promise.all(
      Object.values(project.definitions)
        .filter(isLightweightClass)
        .map(async node => {
          if (!node.extendedDefinitions?.some(it => it.referenceName === 'SCAbstractRoute')) {
            return undefined;
          }

          const instantiatedRoute = (await project.instantiateDefinitionByName(
            node.name,
          )) as RouteInstanceWithMeta;
          // instantiate all errors
          instantiatedRoute.errors = await Promise.all(
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            instantiatedRoute.errorNames.map(async (error: any) =>
              // eslint-disable-next-line @typescript-eslint/ban-types
              Object.assign((await project.instantiateDefinitionByName(error.name)) as object, {
                name: error.name,
              }),
            ),
          );
          instantiatedRoute.responseBodyDescription =
            // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
            project.definitions[instantiatedRoute.responseBodyName]?.comment?.shortSummary!;
          instantiatedRoute.requestBodyDescription =
            // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
            project.definitions[instantiatedRoute.requestBodyName]?.comment?.shortSummary!;

          return {
            description: {
              shortText: node.comment?.shortSummary,
              text: node.comment?.description,
            },
            name: node.name!,
            route: instantiatedRoute,
          };
        }),
    ),
  );
}

/**
 * Generate documentation snippet for one route
 * @param routeWithInfo A route instance with its meta information
 * @param outDirectorySchemasPath Path to directory that will contain relevant schemas for the route
 * @param tagsToKeep Tags / keywords that can be used for grouping routes
 */
export function generateOpenAPIForRoute(
  routeWithInfo: RouteWithMetaInformation,
  outDirectorySchemasPath: string,
  tagsToKeep: string[],
): OpenAPIV3.PathItemObject {
  const route = routeWithInfo.route;
  const openapiPath: OpenAPIV3.PathItemObject = {};

  openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods] = {
    summary: capitalize(routeWithInfo.description.shortText?.replace(/(Route to |Route for )/gim, '')),
    description: routeWithInfo.description.text,
    requestBody: {
      description: route.responseBodyDescription ?? undefined,
      content: {
        'application/json': {
          schema: {
            $ref: path.join(outDirectorySchemasPath, `${route.requestBodyName}.json`),
          },
        },
      },
    },
    parameters: [
      {
        name: 'X-StApps-Version',
        in: 'header',
        schema: {
          type: 'string',
          example: '2.0.0',
        },
        required: true,
      },
    ],
    responses: {},
    tags: routeWithInfo.tags?.filter(value => tagsToKeep.includes(value)),
  };

  openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]!.responses![route.statusCodeSuccess] = {
    description: route.responseBodyDescription,
    content: {
      'application/json': {
        schema: {
          $ref: path.join(outDirectorySchemasPath, `${route.responseBodyName}.json`),
        },
      },
    },
  };

  for (const error of route.errors) {
    openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]!.responses![error.statusCode] = {
      description:
        error.message ?? capitalize(error.name.replaceAll(/([A-Z][a-z])/g, ' $1').replace('SC ', '')),
      content: {
        'application/json': {
          schema: {
            $ref: path.join(outDirectorySchemasPath, `${error.name}.json`),
          },
        },
      },
    };
  }

  if (typeof route.obligatoryParameters === 'object') {
    for (const [parameter, schemaDefinition] of Object.entries(route.obligatoryParameters)) {
      const openapiParameter: OpenAPIV3.ParameterObject = {
        in: 'path',
        name: parameter,
        required: true,
        schema: {
          // TODO make this less of a hack and search copied schemas for the first occurring definition
          $ref: `schema/SCSearchResponse.json#/definitions/${schemaDefinition}`,
        },
      };
      openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]?.parameters?.push(openapiParameter);
    }
  }

  return openapiPath;
}
