import { init, loadRemote } from '@module-federation/runtime'
import type { CypressInternal, CyPromptDriverDefaultShape, CyPromptMoreInfoNeededOptions } from './prompt-driver-types'
import type Emitter from 'component-emitter'
import $errUtils from '../../../cypress/error_utils'
import $stackUtils from '../../../cypress/stack_utils'
import { isNonRetriableCertErrorCode } from '@packages/server/lib/cloud/network/non_retriable_cert_error_codes'

interface CyPromptDriver { default: CyPromptDriverDefaultShape }

declare global {
  interface Window {
    getEventManager?: () => {
      ws: Emitter
      localBus: Emitter
    }
  }
}

interface BundleResultInError {
  error: Error
  timedOut: false
}

interface BundleResultTimedOut {
  error: undefined
  timedOut: true
}

interface BundleResultSuccess {
  error: undefined
  bundle: Awaited<ReturnType<CyPromptDriverDefaultShape['createCyPrompt']>>
  timedOut: false
}

type BundleResult = BundleResultInError | BundleResultTimedOut | BundleResultSuccess

let initializedModule: CyPromptDriverDefaultShape | null = null
const initializeModule = async (Cypress: Cypress.Cypress): Promise<CyPromptDriverDefaultShape> => {
  // Wait for the cy prompt bundle to be downloaded and ready
  const { success, error } = await Cypress.backend('wait:for:prompt:ready')

  if (error) {
    if ('code' in error && isNonRetriableCertErrorCode(error.code as string)) {
      $errUtils.throwErrByPath('prompt.promptProxyError', {
        args: {
          error,
        },
      })
    }

    $errUtils.throwErrByPath('prompt.promptDownloadError', {
      args: {
        error,
      },
    })
  }

  if (!success && !error) {
    $errUtils.throwErrByPath('prompt.promptDownloadError', {
      args: {
        error: new Error('error waiting for cy prompt bundle to be downloaded and ready'),
      },
    })
  }

  let module: CyPromptDriver | null = null

  // Once the cy prompt bundle is downloaded and ready,
  // we can initialize it via the module federation runtime
  try {
    init({
      remotes: [{
        alias: 'cy-prompt',
        type: 'module',
        name: 'cy-prompt',
        entryGlobalName: 'cy-prompt',
        entry: '/__cypress-cy-prompt/driver/cy-prompt.js',
        shareScope: 'default',
      }],
      name: 'driver',
    })

    // This cy-prompt.js file and any subsequent files are
    // served from the cy prompt bundle.
    module = await loadRemote<CyPromptDriver>('cy-prompt')
  } catch (error) {
    $errUtils.throwErrByPath('prompt.promptBundleNeedsRefresh')
  }

  if (!module?.default) {
    $errUtils.throwErrByPath('prompt.promptDownloadError', {
      args: {
        error: new Error('error loading cy prompt driver'),
      },
    })
  }

  initializedModule = module!.default

  return initializedModule
}

const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']): Promise<BundleResult> => {
  try {
    let cloudModule = initializedModule

    if (!cloudModule) {
      cloudModule = await initializeModule(Cypress)
    }

    if (!Cypress.isCrossOriginSpecBridge) {
      Cypress.primaryOriginCommunicator.removeAllListeners('prompt:more-info-needed')
      Cypress.primaryOriginCommunicator.on('prompt:more-info-needed', ({ testId, logId, onSave, onCancel }: CyPromptMoreInfoNeededOptions) => {
        window.getEventManager!().localBus.emit('prompt:more-info-needed', { testId, logId, onSave, onCancel })
      })

      Cypress.primaryOriginCommunicator.removeAllListeners('get:source:details:for:line')
      Cypress.primaryOriginCommunicator.on('get:source:details:for:line', ({ line, projectRoot }, { origin, responseEvent }) => {
        const sourceDetails = $stackUtils.getSourceDetailsForFirstLine(line, projectRoot)

        Cypress.primaryOriginCommunicator.toSpecBridge(origin, responseEvent, sourceDetails)
      })
    }

    return {
      bundle: await cloudModule.createCyPrompt({
        Cypress: Cypress as CypressInternal,
        cy,
        eventManager: window.getEventManager ? window.getEventManager() : undefined,
        errorUtils: {
          extendErrorMessages: $errUtils.extendErrorMessages,
          throwErrByPath: $errUtils.throwErrByPath,
        },
        getSourceDetailsForFirstLine: (line, projectRoot) => {
          if (Cypress.isCrossOriginSpecBridge) {
            return Cypress.specBridgeCommunicator.toPrimaryPromise({
              event: 'get:source:details:for:line',
              data: { line, projectRoot },
              timeout: Cypress.config().defaultCommandTimeout,
            })
          }

          return $stackUtils.getSourceDetailsForFirstLine(line, projectRoot)
        },
        onMoreInfoNeeded: ({ testId, logId, onSave, onCancel }: CyPromptMoreInfoNeededOptions) => {
          if (Cypress.isCrossOriginSpecBridge) {
            Cypress.specBridgeCommunicator.toPrimary('prompt:more-info-needed', { testId, logId, onSave, onCancel })
          } else {
            window.getEventManager!().localBus.emit('prompt:more-info-needed', { testId, logId, onSave, onCancel })
          }
        },
      }),
      error: undefined,
      timedOut: false,
    }
  } catch (error) {
    return {
      error,
      bundle: undefined,
      timedOut: false,
    }
  }
}

export default (Commands: Cypress.Cypress['Commands'], Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']) => {
  let initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy)

  const commands = {
    prompt (steps: string[], commandOptions: object = {}) {
      const promptCmd = cy.state('current')

      const downloadTimeout = '_downloadTimeout' in commandOptions ? commandOptions._downloadTimeout as number : 45000

      let timeoutId: NodeJS.Timeout
      const timeoutPromise = new Promise((resolve) => {
        timeoutId = setTimeout(() => {
          resolve({
            error: undefined,
            timedOut: true,
          })
        }, downloadTimeout)
      })
      const raceBundleResult = Promise.race([
        initializeCloudCyPromptPromise,
        timeoutPromise,
      ]).finally(() => {
        clearTimeout(timeoutId)
      }) as Promise<BundleResult>

      return cy.wrap(raceBundleResult, { log: false, timeout: 1e9 }).then((bundleResult: BundleResult) => {
        if (bundleResult.timedOut) {
          cy.state('current', promptCmd)

          return $errUtils.throwErrByPath('prompt.promptDownloadTimedOut', {
            args: {
              error: new Error('cy.prompt bundle download timed out'),
            },
          })
        }

        if (bundleResult.error) {
          cy.state('current', promptCmd)
          throw bundleResult.error
        }

        const cyPrompt = bundleResult.bundle

        return cyPrompt({
          steps,
          commandOptions,
          promptCmd,
        })
      })
    },
  }

  commands.prompt['__resetPrompt'] = async (delay: number = 0) => {
    initializedModule = null
    initializeCloudCyPromptPromise = new Promise((resolve) => setTimeout(resolve, delay)).then(() => initializeCloudCyPrompt(Cypress, cy))
  }

  Commands.addAll(commands)
}
