import {
  ContentHandling,
  IntegrationType,
  PassthroughBehavior
} from '@aws-cdk/aws-apigateway'
import fs from 'fs'
import {METHODS} from 'http'
import {
  OpenApiBuilder,
  OpenAPIObject,
  OperationObject,
  PathItemObject
} from 'openapi3-ts'
import path from 'path'
import * as yaml from 'yaml'

export interface Options {
  lambdaFunctionName?: string
  outputFile?: string
  outputFormat: 'json' | 'yaml'
  cors?: {timeout: number}
}

function specFromJSON(str: string): OpenApiBuilder {
  return new OpenApiBuilder(JSON.parse(str))
}

function specFromYaml(str: string): OpenApiBuilder {
  return new OpenApiBuilder(yaml.parse(str))
}

function extensionForOperation(
  method: string,
  op: OperationObject,
  options: Options
): object {
  if (method === 'options') {
    if (!options.cors) {
      return {}
    }

    return {
      type: IntegrationType.MOCK,
      passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
      timeoutInMillis: options.cors.timeout,
      requestTemplates: {'application/json': '{"statusCode": 200}'},
      responses: {
        default: {
          statusCode: 200,
          responseParameters: {
            'method.response.header.Access-Control-Allow-Methods':
              "'GET,OPTIONS,POST'",
            'method.response.header.Access-Control-Allow-Headers':
              "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
            'method.response.header.Access-Control-Allow-Origin': "'*'"
          }
        }
      }
    }
  }

  return {
    type: IntegrationType.AWS_PROXY,
    httpMethod: 'POST',
    uri: `arn:\${AWS::Partition}:apigateway:\${AWS::Region}:lambda:path/2015-03-31/functions/arn:\${AWS::Partition}:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${options.lambdaFunctionName}/invocations`,
    passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
    contentHandling: ContentHandling.CONVERT_TO_TEXT,
    responses: Object.keys(op.responses).reduce((responses, statusCode) => {
      // const response: ResponseObject = op.responses[statusCode]

      return {...responses, [statusCode]: {statusCode}}
    }, {})
  }
}

function addExtensionsToPath(
  pathItem: PathItemObject,
  options: Options
): PathItemObject {
  for (const method of METHODS.map(m => m.toLowerCase())) {
    const operation: OperationObject = pathItem[method]

    if (!operation) {
      continue
    }

    const extension = extensionForOperation(method, operation, options)

    pathItem[method] = extension
      ? {
          ...operation,
          'x-amazon-apigateway-integration': extension
        }
      : operation
  }

  return pathItem
}

export function addExtensions(
  input: string | OpenAPIObject,
  options: Options = {outputFormat: 'yaml'}
): string {
  if (!options.lambdaFunctionName) {
    throw new Error(
      `openapi-aws-extensions currently only supports Lambda integrations. The \`lambdaFunctionName\` option is required.`
    )
  }

  let builder: OpenApiBuilder

  if (typeof input === 'string') {
    if (fs.existsSync(input) === false) {
      throw new Error(`No specification file at path: ${input}`)
    }

    const ext = path.extname(input).replace('.', '')
    const fileContents = fs.readFileSync(input, 'utf-8')

    switch (ext) {
      case 'json':
        builder = specFromJSON(fileContents)
        break

      case 'yml':
      case 'yaml':
        builder = specFromYaml(fileContents)
        break

      default:
        throw new Error(`Unsupported specification file extension: "${ext}"`)
    }

    if (options.outputFormat !== ext) {
      options.outputFormat = ext === 'yml' ? 'yaml' : ext
    }
  } else {
    builder = new OpenApiBuilder(input)
  }

  for (const pathName of Object.keys(builder.rootDoc.paths)) {
    builder.rootDoc.paths[pathName] = addExtensionsToPath(
      builder.rootDoc.paths[pathName],
      options
    )
  }

  const result =
    options.outputFormat === 'json'
      ? builder.getSpecAsJson()
      : builder.getSpecAsYaml()

  if (options.outputFile) {
    fs.writeFileSync(options.outputFile, result, {encoding: 'utf-8'})
  }

  return result
}
