const debug = require('debug')('openapi-cop:proxy');
const swaggerClient = require('swagger-client');
const swaggerParser = require('swagger-parser');
import { IncomingHttpHeaders } from 'http';
import * as refParser from 'json-schema-ref-parser';
import {
  OpenAPIValidator,
  Request as OasRequest,
  ValidationResult,
  Operation,
  SetMatchType,
} from 'openapi-backend';

import { SchemaValidationException } from '../types/errors';
import { ValidationResults } from '../types/validation';

/**
 * Checks if the OpenAPI document is a valid definition. Wrapper around
 * SwaggerParser.validate.
 * @param spec The object representation of the OpenAPI document
 */
export async function validateDocument(spec: any): Promise<void> {
  try {
    const api = await swaggerParser.validate(spec);
    debug(
      `    Validated API definition for "${api.info.title}" [version ${api.info.version}]`,
    );
  } catch (error) {
    throw new SchemaValidationException(error as string);
  }
}

/**
 * Resolves all references, including allOf relationships. Wrapper around
 * SwaggerClient.resolve.
 * @param spec The object representation of the OpenAPI document
 * @param baseDoc The path to the API base document (i.e. the 'swagger.json'
 * file)
 */
export async function resolve(spec: any, baseDoc: string): Promise<any> {
  const resolutionResult = await swaggerClient.resolve({
    pathDiscriminator: [],
    spec,
    baseDoc,
    allowMetaPatches: true,
    skipNormalization: true,
  });

  if (resolutionResult.errors.length > 0) {
    throw new Error(
      `Could not resolve references in OpenAPI document due to the following errors:
    ${JSON.stringify(resolutionResult.errors, null, 2)}`,
    );
  }

  const apiDoc = resolutionResult.spec;
  delete apiDoc['$$normalized']; // delete additional property that is added by
  // the resolver
  return apiDoc;
}

/**
 * Collects all file references and yields a single OpenAPI object with only
 * local references.
 * @param spec The object representation of the OpenAPI document
 * @param basePath The path to the API base document (i.e. the 'swagger.json'
 * file)
 */
export function dereference(
  spec: any,
  basePath: string,
): Promise<refParser.JSONSchema> {
  return refParser.dereference(basePath, spec, {});
}

/**
 * Validator to match requests to operations and validate
 * requests and responses using a OpenAPI document. Wrapper around
 * OpenAPIValidator.
 */
export class Validator {
  apiDoc: any;
  oasValidator: OpenAPIValidator;

  constructor(apiDoc: any) {
    this.apiDoc = apiDoc;

    this.oasValidator = new OpenAPIValidator({
      definition: apiDoc,
      ajvOpts: { unknownFormats: ['int32', 'int64', 'float', 'double'] },
    });
  }

  matchOperation(oasRequest: OasRequest): Operation | undefined {
    return this.oasValidator.router.matchOperation(oasRequest);
  }

  validateRequest(
    oasRequest: OasRequest,
    operation: Operation | undefined,
  ): ValidationResult {
    if (!operation || !operation.operationId) {
      return {
        valid: false,
        errors: [
          {
            keyword: 'operation',
            dataPath: '',
            schemaPath: '',
            params: [],
            message: `Unknown operation '${oasRequest.path}'`,
          },
        ],
      };
    }

    return this.oasValidator.validateRequest(oasRequest, operation);
  }

  validateResponse(
    responseBody: string,
    operation: Operation,
    statusCode: number,
  ): ValidationResult {
    return this.oasValidator.validateResponse(
      responseBody,
      operation,
      statusCode,
    );
  }

  validateResponseHeaders(
    headers: IncomingHttpHeaders,
    operation: Operation,
    statusCode: number,
  ): ValidationResult {
    return this.oasValidator.validateResponseHeaders(headers, operation, {
      statusCode,
      setMatchType: SetMatchType.Superset,
    });
  }
}

export function hasErrors(validationResults: ValidationResults): boolean {
  const isRequestValid =
    !validationResults.request || validationResults.request.valid;
  const isResponseValid =
    !validationResults.response || validationResults.response.valid;
  const areResponseHeadersValid =
    !validationResults.responseHeaders ||
    validationResults.responseHeaders.valid;
  return !isRequestValid || !isResponseValid || !areResponseHeadersValid;
}