import {
  type IntegrationDefinition,
  type BotDefinition,
  type InterfaceDefinition,
  ActionDefinition,
  z,
  EventDefinition,
} from '@botpress/sdk'
import * as apiUtils from '../api'
import type commandDefinitions from '../command-definitions'
import * as errors from '../errors'
import { BaseLinter } from '../linter/base-linter'
import { BotLinter } from '../linter/bot-linter'
import { IntegrationLinter } from '../linter/integration-linter'
import { InterfaceLinter } from '../linter/interface-linter'
import { ProjectCommand } from './project-command'

const _getIssuesDetectedMessage = (linter: BaseLinter<unknown>, nonEmptyPrefix: string = '') => {
  const message = linter
    .getIssuesCountBySeverity()
    .map(({ name, count }) => `${count} ${name}(s)`)
    .join(', ')

  return message.length > 0 ? `${nonEmptyPrefix}${message}` : ''
}

export type LintCommandDefinition = typeof commandDefinitions.lint
export class LintCommand extends ProjectCommand<LintCommandDefinition> {
  public async run(): Promise<void> {
    const { projectType, resolveProjectDefinition } = this.readProjectDefinitionFromFS()
    const projectDef = await resolveProjectDefinition()
    if (projectDef.bpLintDisabled) {
      this.logger.warn(
        'Linting is disabled for this project because of a bplint directive. To enable linting, remove the "bplint-disable" directive from the project definition file'
      )
      return
    }

    switch (projectType) {
      case 'integration': {
        const projectDef = await resolveProjectDefinition()
        return this._runLintForIntegration(projectDef.definition)
      }
      case 'bot': {
        const projectDef = await resolveProjectDefinition()
        return this._runLintForBot(projectDef.definition)
      }
      case 'interface': {
        const projectDef = await resolveProjectDefinition()
        return this._runLintForInterface(projectDef.definition)
      }
      default:
        throw new errors.UnsupportedProjectType()
    }
  }

  private async _runLintForInterface(definition: InterfaceDefinition): Promise<void> {
    for (const [actionName, actionDef] of Object.entries(definition.actions)) {
      definition.actions[actionName] = this._dereferenceActionDefinition(actionDef)
    }

    for (const [eventName, eventDef] of Object.entries(definition.events)) {
      definition.events[eventName] = this._dereferenceEventDefinition(eventDef)
    }

    const parsedInterfaceDefinition = await apiUtils.prepareCreateInterfaceBody(definition)
    const linter = new InterfaceLinter(parsedInterfaceDefinition, this.logger)

    await linter.lint()
    linter.logResults(this.logger)

    const issueCountsSuffix = _getIssuesDetectedMessage(linter, ' - ')

    if (linter.hasErrors()) {
      throw new errors.BotpressCLIError(`Interface definition contains linting errors${issueCountsSuffix}`)
    }

    this.logger.success(`Interface definition is valid${issueCountsSuffix}`)
  }

  private async _runLintForBot(definition: BotDefinition): Promise<void> {
    const strippedDefinition = this._stripAutoGeneratedContentFromBot(definition)
    const parsedBotDefinition = await apiUtils.prepareCreateBotBody(strippedDefinition)
    const linter = new BotLinter(parsedBotDefinition, this.logger)

    await linter.lint()
    linter.logResults(this.logger)

    const issueCountsSuffix = _getIssuesDetectedMessage(linter, ' - ')

    if (linter.hasErrors()) {
      throw new errors.BotpressCLIError(`Bot definition contains linting errors${issueCountsSuffix}`)
    }

    this.logger.success(`Bot definition is valid${issueCountsSuffix}`)
  }

  private _stripAutoGeneratedContentFromBot(definition: BotDefinition) {
    return {
      ...definition,
      integrations: {},
    } as BotDefinition
  }

  private async _runLintForIntegration(definition: IntegrationDefinition): Promise<void> {
    const strippedDefinition = this._stripAutoGeneratedContentFromIntegration(definition)
    const parsedIntegrationDefinition = await this.prepareCreateIntegrationBody(strippedDefinition)
    const linter = new IntegrationLinter(
      { ...parsedIntegrationDefinition, secrets: strippedDefinition.secrets },
      this.logger
    )

    await linter.lint()
    linter.logResults(this.logger)

    const issueCountsSuffix = _getIssuesDetectedMessage(linter, ' - ')

    if (linter.hasErrors()) {
      throw new errors.BotpressCLIError(`Integration definition contains linting errors${issueCountsSuffix}`)
    }

    this.logger.success(`Integration definition is valid${issueCountsSuffix}`)
  }

  private _stripAutoGeneratedContentFromIntegration(definition: IntegrationDefinition) {
    const { actionNames, eventNames } = this._getAutoGeneratedContentOfIntegration(definition)

    return {
      ...definition,
      actions: Object.fromEntries(Object.entries(definition.actions ?? {}).filter(([key]) => !actionNames.has(key))),
      events: Object.fromEntries(Object.entries(definition.events ?? {}).filter(([key]) => !eventNames.has(key))),
    } as IntegrationDefinition
  }

  private _getAutoGeneratedContentOfIntegration(definition: IntegrationDefinition) {
    const actionNames = new Set<string>()
    const eventNames = new Set<string>()

    const interfacesStatements = definition.interfaces ?? {}
    for (const iface of Object.values(interfacesStatements)) {
      for (const actionDefinition of Object.values(iface.actions)) {
        actionNames.add(actionDefinition.name)
      }
      for (const eventDefinition of Object.values(iface.events)) {
        eventNames.add(eventDefinition.name)
      }
    }

    return { actionNames, eventNames } as const
  }

  private _dereferenceActionDefinition = (actionDef: ActionDefinition): ActionDefinition => {
    const inputRefs = actionDef.input.schema.getReferences()
    const outputRefs = actionDef.output.schema.getReferences()

    const inputRefSchemas = Object.fromEntries(inputRefs.map((ref) => [ref, this._replaceRef(ref)]))
    const outputRefSchemas = Object.fromEntries(outputRefs.map((ref) => [ref, this._replaceRef(ref)]))

    actionDef.input.schema = actionDef.input.schema.dereference(inputRefSchemas) as z.ZodObject
    actionDef.output.schema = actionDef.output.schema.dereference(outputRefSchemas) as z.ZodObject

    return actionDef
  }

  private _dereferenceEventDefinition = (eventDef: EventDefinition): EventDefinition => {
    const refs = eventDef.schema.getReferences()

    const refSchemas = Object.fromEntries(refs.map((ref) => [ref, this._replaceRef(ref)]))

    eventDef.schema = eventDef.schema.dereference(refSchemas) as z.ZodObject

    return eventDef
  }

  private _replaceRef = (refUri: string): z.ZodObject => z.object({}).title(refUri).describe(refUri)
}
