import { workAsyncStorage } from '../app-render/work-async-storage.external'
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external'
import { abortOnSynchronousPlatformIOAccess } from '../app-render/dynamic-rendering'
import { InvariantError } from '../../shared/lib/invariant-error'
import { RenderStage } from '../app-render/staged-rendering'

import { getServerReact, getClientReact } from '../runtime-reacts.external'

type ApiType = 'time' | 'random' | 'crypto'

export function io(expression: string, type: ApiType) {
  const workUnitStore = workUnitAsyncStorage.getStore()
  const workStore = workAsyncStorage.getStore()

  if (!workUnitStore || !workStore) {
    return
  }

  switch (workUnitStore.type) {
    case 'prerender':
    case 'prerender-runtime': {
      const prerenderSignal = workUnitStore.controller.signal

      if (prerenderSignal.aborted === false) {
        // If the prerender signal is already aborted we don't need to construct
        // any stacks because something else actually terminated the prerender.
        let message: string
        switch (type) {
          case 'time':
            message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing the current time in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time`
            break
          case 'random':
            message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-random`
            break
          case 'crypto':
            message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random cryptographic values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-crypto`
            break
          default:
            throw new InvariantError(
              'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
            )
        }

        abortOnSynchronousPlatformIOAccess(
          workStore.route,
          expression,
          applyOwnerStack(new Error(message)),
          workUnitStore
        )
      }
      break
    }
    case 'prerender-client': {
      const prerenderSignal = workUnitStore.controller.signal

      if (prerenderSignal.aborted === false) {
        // If the prerender signal is already aborted we don't need to construct
        // any stacks because something else actually terminated the prerender.
        let message: string
        switch (type) {
          case 'time':
            message = `Route "${workStore.route}" used ${expression} inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time-client`
            break
          case 'random':
            message = `Route "${workStore.route}" used ${expression} inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-random-client`
            break
          case 'crypto':
            message = `Route "${workStore.route}" used ${expression} inside a Client Component without a Suspense boundary above it. See more info here: https://nextjs.org/docs/messages/next-prerender-crypto-client`
            break
          default:
            throw new InvariantError(
              'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
            )
        }

        abortOnSynchronousPlatformIOAccess(
          workStore.route,
          expression,
          applyOwnerStack(new Error(message)),
          workUnitStore
        )
      }
      break
    }
    case 'request':
      if (process.env.NODE_ENV === 'development') {
        const stageController = workUnitStore.stagedRendering
        if (stageController && stageController.canSyncInterrupt()) {
          let message: string
          if (stageController.currentStage === RenderStage.Static) {
            switch (type) {
              case 'time':
                message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing the current time in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time`
                break
              case 'random':
                message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-random`
                break
              case 'crypto':
                message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random cryptographic values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-crypto`
                break
              default:
                throw new InvariantError(
                  'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
                )
            }
          } else {
            // We're in the Runtime stage.
            // We only error for Sync IO in the Runtime stage if the route has a runtime prefetch config.
            // This check is implemented in `stageController.canSyncInterrupt()` --
            // if runtime prefetching isn't enabled, then we won't get here.

            let accessStatement: string
            let additionalInfoLink: string

            switch (type) {
              case 'time':
                accessStatement = 'the current time'
                additionalInfoLink =
                  'https://nextjs.org/docs/messages/next-prerender-runtime-current-time'
                break
              case 'random':
                accessStatement = 'random values synchronously'
                additionalInfoLink =
                  'https://nextjs.org/docs/messages/next-prerender-runtime-random'
                break
              case 'crypto':
                accessStatement = 'random cryptographic values synchronously'
                additionalInfoLink =
                  'https://nextjs.org/docs/messages/next-prerender-runtime-crypto'
                break
              default:
                throw new InvariantError(
                  'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
                )
            }

            message = `Route "${workStore.route}" used ${expression} before accessing either uncached data (e.g. \`fetch()\`) or awaiting \`connection()\`. When configured for Runtime prefetching, accessing ${accessStatement} in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: ${additionalInfoLink}`
          }

          const syncIOError = applyOwnerStack(new Error(message))
          stageController.syncInterruptCurrentStageWithReason(syncIOError)
        }
      }
      break
    case 'prerender-ppr':
    case 'prerender-legacy':
    case 'cache':
    case 'private-cache':
    case 'unstable-cache':
      break
    default:
      workUnitStore satisfies never
  }
}

function applyOwnerStack(error: Error) {
  // TODO: Instead of stitching the stacks here, we should log the original
  // error as-is when it occurs, and let `patchErrorInspect` handle adding the
  // owner stack, instead of logging it deferred in the `LogSafely` component
  // via `throwIfDisallowedDynamic`.
  if (process.env.NODE_ENV !== 'production') {
    const ownerStack =
      getClientReact()?.captureOwnerStack?.() ??
      getServerReact()?.captureOwnerStack?.()

    if (ownerStack) {
      let stack = ownerStack

      if (error.stack) {
        const frames: string[] = []

        for (const frame of error.stack.split('\n').slice(1)) {
          if (frame.includes('react_stack_bottom_frame')) {
            break
          }

          frames.push(frame)
        }

        stack = '\n' + frames.join('\n') + stack
      }

      error.stack = error.name + ': ' + error.message + stack
    }
  }

  return error
}
