import { Extractor, ExtractorConfig, ExtractorResult } from '@microsoft/api-extractor'
import { ApiMethodSignature, ApiModel, ApiParameterListMixin, ApiReturnTypeMixin } from '@microsoft/api-extractor-model'
import { Command } from 'commander'
import { writeFileSync } from 'fs'
import { OpenAPIV3 } from 'openapi-types'
import { resolve } from 'path'
import * as TJS from 'ts-json-schema-generator'

import module from 'module'

const requireCjs = module.createRequire(import.meta.url)

interface Method {
  packageName: string
  pluginInterfaceName: string
  operationId: string
  description?: string
  parameters?: string
  response: string
}

const genericTypes = ['boolean', 'string', 'number', 'any', 'Array<string>']

function createSchema(generator: TJS.SchemaGenerator, symbol: string) {
  if (genericTypes.includes(symbol)) {
    return { components: { schemas: {} } }
  }

  const fixedSymbol = symbol.replace(/Array\<(.*)\>/gm, '$1').replace(/(\\:[\w]?Certificate)/gm, ': any')

  const schema = generator.createSchema(fixedSymbol)

  const newSchema = {
    components: {
      schemas: schema.definitions,
    },
  }

  let schemaStr = JSON.stringify(newSchema, null, 2)

  schemaStr = schemaStr.replace(/#\/definitions\//gm, '#/components/schemas/')
  schemaStr = schemaStr.replace(/\"patternProperties\":{([^:]*):{[^}]*}}/gm, '"pattern": $1')
  schemaStr = schemaStr.replace(/Verifiable\<(.*)\>/gm, 'Verifiable-$1')
  schemaStr = schemaStr.replace(/Where\<(.*)\>/gm, 'Where-$1')
  schemaStr = schemaStr.replace(/Order\<(.*)\>/gm, 'Order-$1')
  schemaStr = schemaStr.replace(/FindArgs\<(.*)\>/gm, 'FindArgs-$1')
  schemaStr = schemaStr.replace(/https \:\/\//gm, 'https://')
  // a bug in the schema generator stack mangles @link tags with text.
  schemaStr = schemaStr.replace(/\{@link\s+([^|}]+?)\s([^|}]+)\s}/g, '{@link $1 | $2 }')
  return JSON.parse(schemaStr)
}

function getReference(response: string): OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject {
  if (!response) {
    return { type: 'object' }
  }

  if (response.slice(0, 6) === 'Array<') {
    const symbol = response.replace('Array<', '').replace('>', '') as 'string' | 'number' | 'boolean' | 'object' | 'integer'
    return {
      type: 'array',
      items: genericTypes.includes(symbol) ? { type: symbol } : { $ref: '#/components/schemas/' + symbol },
    }
  }
  response = response.replace(/(\\:?[\w]*Certificate)/gm, 'any')
  if (response === 'any') {
    return { type: 'object' }
  }

  if (['string', 'number', 'boolean', 'object', 'integer'].includes(response)) {
    // @ts-ignore
    return { type: response }
  } else {
    return { $ref: '#/components/schemas/' + response }
  }
}

const dev = new Command('dev').description('Plugin developer tools')

dev
  .command('generate-plugin-schema')
  .description('generate plugin schema')
  .option('-c, --extractorConfig <string>', 'API Extractor config file', './api-extractor.json')
  .option('-p, --packageConfig <string>', 'package.json file containing a Veramo plugin interface config', './package.json')

  .action(async (options) => {
    const apiExtractorJsonPath: string = resolve(options.extractorConfig)
    const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath)

    const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
      localBuild: true,
      showVerboseMessages: true,
    })

    if (!extractorResult.succeeded) {
      console.error(`API Extractor completed with ${extractorResult.errorCount} errors` + ` and ${extractorResult.warningCount} warnings`)
      process.exitCode = 1
    }

    const packageConfig = requireCjs(resolve(options.packageConfig))
    const interfaces: any = {}

    for (const pluginInterfaceName in packageConfig?.veramo?.pluginInterfaces) {
      const entryFile = packageConfig.veramo.pluginInterfaces[pluginInterfaceName]
      const api = {
        components: {
          schemas: {},
          methods: {},
        },
      }

      const generator = TJS.createGenerator({
        path: resolve(entryFile),
        encodeRefs: false,
        additionalProperties: true,
        skipTypeCheck: true,
        // functions: 'hide',
      })

      const apiModel: ApiModel = new ApiModel()
      const apiPackage = apiModel.loadPackage(extractorConfig.apiJsonFilePath)

      const entry = apiPackage.entryPoints[0]

      const pluginInterface = entry.findMembersByName(pluginInterfaceName)[0]

      for (const member of pluginInterface.members) {
        const method: Partial<Method> = {}
        method.pluginInterfaceName = pluginInterfaceName
        method.operationId = member.displayName
        // console.log(member)
        method.parameters = (member as ApiParameterListMixin).parameters[0]?.parameterTypeExcerpt?.text
        method.response = (member as ApiReturnTypeMixin).returnTypeExcerpt.text.replace('Promise<', '').replace('>', '')

        const methodSignature = member as ApiMethodSignature
        method.description = methodSignature.tsdocComment?.summarySection
          ?.getChildNodes()[0]
          // @ts-ignore
          ?.getChildNodes()[0]?.text

        method.description = method.description || ''

        if (method.parameters) {
          // @ts-ignore
          api.components.schemas = {
            // @ts-ignore
            ...api.components.schemas,
            ...createSchema(generator, method.parameters).components.schemas,
          }
        }

        // @ts-ignore
        api.components.schemas = {
          // @ts-ignore
          ...api.components.schemas,
          ...createSchema(generator, method.response).components.schemas,
        }

        // @ts-ignore
        api.components.methods[method.operationId] = {
          description: method.description,
          arguments: getReference(method.parameters),
          returnType: getReference(method.response),
        }
      }

      interfaces[pluginInterfaceName] = api
    }

    writeFileSync(resolve('./plugin.schema.json'), JSON.stringify(interfaces, null, 2))
  })

dev
  .command('extract-api')
  .description('Extract API')
  .option('-c, --extractorConfig <string>', 'API Extractor config file', './api-extractor.json')
  .action(async (options) => {
    const apiExtractorJsonPath: string = resolve(options.extractorConfig)
    const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath)

    const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
      localBuild: true,
      showVerboseMessages: true,
    })

    if (!extractorResult.succeeded) {
      console.error(`API Extractor completed with ${extractorResult.errorCount} errors` + ` and ${extractorResult.warningCount} warnings`)
      process.exitCode = 1
    }
  })

export { dev }
