import { devtoolsEventClient } from '@tanstack/devtools-client'
import { ServerEventBus } from '@tanstack/devtools-event-bus/server'
import chalk from 'chalk'
import {
  DEFAULT_EDITOR_CONFIG,
  TANSTACK_DEVTOOLS_PACKAGES,
  addPluginToDevtools,
  addSourceToJsx,
  detectDevtoolsFile,
  emitOutdatedDeps,
  enhanceConsoleLog,
  generateConsolePipeCode,
  handleDevToolsRequest,
  handleOpenSource,
  injectPluginIntoFile,
  injectRuntimeBridge,
  installPackage,
  normalizePath,
  readPackageJson,
  removeDevtools,
  stripEnhancedLogPrefix,
  wireRuntimeBridgeChannels,
} from '@tanstack/devtools-bundler-core'
import type {
  ConsoleLevel,
  EditorConfig,
  TanStackDevtoolsConfig,
} from '@tanstack/devtools-bundler-core'
import type { ServerResponse } from 'node:http'
import type { Plugin } from 'vite'
import type { HttpServerLike } from '@tanstack/devtools-event-bus/server'

export type { ConsoleLevel } from '@tanstack/devtools-bundler-core'
export type TanStackDevtoolsViteConfig = TanStackDevtoolsConfig

export const defineDevtoolsConfig = (config: TanStackDevtoolsViteConfig) =>
  config

export const devtools = (args?: TanStackDevtoolsViteConfig): Array<Plugin> => {
  let port = 5173
  const logging = args?.logging ?? true
  const enhancedLogsConfig = args?.enhancedLogs ?? { enabled: true }
  const injectSourceConfig = args?.injectSource ?? { enabled: true }
  const removeDevtoolsOnBuild = args?.removeDevtoolsOnBuild ?? true
  const serverBusEnabled = args?.eventBusConfig?.enabled ?? true
  const consolePipingConfig = args?.consolePiping ?? { enabled: true }
  const consolePipingLevels: Array<ConsoleLevel> =
    consolePipingConfig.levels ?? ['log', 'warn', 'error', 'info', 'debug']

  let devtoolsFileId: string | null = null
  let devtoolsPort: number | null = null
  let devtoolsHost: string | null = null
  let devtoolsProtocol: 'http' | 'https' | null = null

  return [
    {
      enforce: 'pre',
      name: '@tanstack/devtools:inject-source',
      apply(config) {
        return config.mode === 'development' && injectSourceConfig.enabled
      },
      transform: {
        filter: {
          id: {
            exclude: [/node_modules/, /\?raw/, /\/dist\//, /\/build\//],
          },
        },
        handler(code, id) {
          return addSourceToJsx(code, id, args?.injectSource?.ignore)
        },
      },
    },
    {
      name: '@tanstack/devtools:config',
      enforce: 'pre',
      config(_, { command }) {
        // we do not apply any config changes for build
        if (command !== 'serve') {
          return
        }

        /*  const solidDedupeDeps = [
          'solid-js',
          'solid-js/web',
          'solid-js/store',
          'solid-js/html',
          'solid-js/h',
        ]

        return {
          resolve: {
            dedupe: solidDedupeDeps,
          },
          optimizeDeps: {
            include: solidDedupeDeps,
          },
        } */
      },
    },
    {
      enforce: 'pre',
      name: '@tanstack/devtools:custom-server',
      apply(config) {
        // Custom server is only needed in development for piping events to the client
        return config.mode === 'development'
      },
      async configureServer(server) {
        if (serverBusEnabled) {
          const preferredPort = args?.eventBusConfig?.port ?? 4206
          const isHttps = !!server.config.server.https
          const serverHost =
            typeof server.config.server.host === 'string'
              ? server.config.server.host
              : 'localhost'

          devtoolsProtocol = isHttps ? 'https' : 'http'
          devtoolsHost = serverHost

          const bus = new ServerEventBus({
            ...args?.eventBusConfig,
            port: preferredPort,
            host: serverHost,
            // When HTTPS is enabled, piggyback on Vite's server
            // so WebSocket/SSE connections share the same TLS certificate
            ...(isHttps && server.httpServer
              ? { httpServer: server.httpServer as HttpServerLike }
              : {}),
          })
          // start() now handles EADDRINUSE and returns the actual port
          devtoolsPort = await bus.start()
          if ((server as any).environments) {
            const teardownBridge = wireRuntimeBridgeChannels(
              server as unknown as Parameters<
                typeof wireRuntimeBridgeChannels
              >[0],
              () => globalThis.__TANSTACK_EVENT_TARGET__,
            )
            server.httpServer?.on('close', teardownBridge)
          }
        }

        server.middlewares.use((req, _res, next) => {
          if (req.socket.localPort && req.socket.localPort !== port) {
            port = req.socket.localPort
          }
          next()
        })
        if (server.config.server.port) {
          port = server.config.server.port
        }

        server.httpServer?.on('listening', () => {
          port = server.config.server.port
        })

        const editor = args?.editor ?? DEFAULT_EDITOR_CONFIG
        const openInEditor: EditorConfig['open'] = async (
          path,
          lineNum,
          columnNum,
        ) => {
          if (!path) {
            return
          }
          await editor.open(path, lineNum, columnNum)
        }

        const originalConsole = Object.fromEntries(
          consolePipingLevels.map((l) => [l, console[l].bind(console)]),
        ) as Partial<Record<ConsoleLevel, typeof console.log>>

        // SSE clients for broadcasting server logs to browser
        const sseClients: Array<{
          res: ServerResponse
          id: number
        }> = []
        let sseClientId = 0
        const consolePipingEnabled = consolePipingConfig.enabled ?? true

        server.middlewares.use((req, res, next) =>
          handleDevToolsRequest(req, res, next, {
            onOpenSource: (parsedData) => {
              const { data, routine } = parsedData
              if (routine === 'open-source') {
                return handleOpenSource({
                  data: { type: data.type, data },
                  openInEditor,
                })
              }
              return
            },
            ...(consolePipingEnabled
              ? {
                  onConsolePipe: (entries) => {
                    for (const entry of entries) {
                      const prefix = chalk.cyan('[Client]')
                      const logMethod =
                        originalConsole[entry.level as ConsoleLevel] ??
                        // log exists in consolePipingLevels
                        originalConsole.log!

                      const cleanedArgs = stripEnhancedLogPrefix(
                        entry.args,
                        (loc) => chalk.gray(loc),
                      )
                      logMethod(prefix, ...cleanedArgs)
                    }
                  },
                  onConsolePipeSSE: (res, req) => {
                    res.setHeader('Content-Type', 'text/event-stream')
                    res.setHeader('Cache-Control', 'no-cache')
                    res.setHeader('Connection', 'keep-alive')
                    res.setHeader('Access-Control-Allow-Origin', '*')
                    res.flushHeaders()

                    const clientId = ++sseClientId
                    sseClients.push({ res, id: clientId })

                    req.on('close', () => {
                      const index = sseClients.findIndex(
                        (c) => c.id === clientId,
                      )
                      if (index !== -1) {
                        sseClients.splice(index, 1)
                      }
                    })
                  },
                  onServerConsolePipe: (entries) => {
                    try {
                      const data = JSON.stringify({
                        entries: entries.map((e) => ({
                          level: e.level,
                          args: e.args,
                          source: 'server',
                          timestamp: e.timestamp || Date.now(),
                        })),
                      })

                      for (const client of sseClients) {
                        client.res.write(`data: ${data}\n\n`)
                      }
                    } catch {}
                  },
                }
              : {}),
          }),
        )
      },
    },
    {
      name: '@tanstack/devtools:remove-devtools-on-build',
      apply(config, { command }) {
        // Check both command and mode to support various hosting providers
        // Some providers (Cloudflare, Netlify, Heroku) might not use 'build' command
        // but will always set mode to 'production' for production builds
        return (
          (command !== 'serve' || config.mode === 'production') &&
          removeDevtoolsOnBuild
        )
      },
      enforce: 'pre',
      transform(code, id) {
        if (
          id.includes('node_modules') ||
          id.includes('?raw') ||
          !TANSTACK_DEVTOOLS_PACKAGES.some((pkg) => code.includes(pkg))
        )
          return
        const transform = removeDevtools(code, id)
        if (!transform) return
        if (logging) {
          console.log(
            `\n${chalk.greenBright(`[@tanstack/devtools-vite]`)} Removed devtools code from: ${id.replace(normalizePath(process.cwd()), '')}\n`,
          )
        }
        return transform
      },
    },
    {
      name: '@tanstack/devtools:event-client-setup',
      apply(config, { command }) {
        if (
          process.env.CI ||
          process.env.NODE_ENV !== 'development' ||
          command !== 'serve'
        )
          return false
        return config.mode === 'development'
      },
      async configureServer() {
        const packageJson = await readPackageJson()
        const outdatedDeps = emitOutdatedDeps().then((deps) => deps)

        // Listen for package installation requests
        devtoolsEventClient.on('install-devtools', async (event) => {
          const result = await installPackage(event.payload.packageName)
          devtoolsEventClient.emit('devtools-installed', {
            packageName: event.payload.packageName,
            success: result.success,
            error: result.error,
          })

          // If installation was successful, automatically add the plugin to devtools
          if (result.success) {
            const { packageName, pluginName, pluginImport } = event.payload

            console.log(
              chalk.blueBright(
                `[@tanstack/devtools-vite] Auto-adding ${packageName} to devtools...`,
              ),
            )

            const injectResult = addPluginToDevtools(
              devtoolsFileId,
              packageName,
              pluginName,
              pluginImport,
            )

            if (injectResult.success) {
              // Emit plugin-added event so the UI updates
              devtoolsEventClient.emit('plugin-added', {
                packageName,
                success: true,
              })

              // Also re-read package.json to update the UI with the newly installed package
              const updatedPackageJson = await readPackageJson()
              devtoolsEventClient.emit('package-json-read', {
                packageJson: updatedPackageJson,
              })
            }
          }
        })

        // Listen for add plugin to devtools requests
        devtoolsEventClient.on('add-plugin-to-devtools', (event) => {
          const { packageName, pluginName, pluginImport } = event.payload

          console.log(
            chalk.blueBright(
              `[@tanstack/devtools-vite] Adding ${packageName} to devtools...`,
            ),
          )

          const result = addPluginToDevtools(
            devtoolsFileId,
            packageName,
            pluginName,
            pluginImport,
          )

          devtoolsEventClient.emit('plugin-added', {
            packageName,
            success: result.success,
            error: result.error,
          })
        })

        // Handle bump-package-version event
        devtoolsEventClient.on('bump-package-version', async (event) => {
          const {
            packageName,
            devtoolsPackage,
            pluginName,
            minVersion,
            pluginImport,
          } = event.payload

          console.log(
            chalk.blueBright(
              `[@tanstack/devtools-vite] Bumping ${packageName} to version ${minVersion}...`,
            ),
          )

          // Install the package with the minimum version
          const packageWithVersion = minVersion
            ? `${packageName}@^${minVersion}`
            : packageName

          const result = await installPackage(packageWithVersion)

          if (!result.success) {
            console.log(
              chalk.redBright(
                `[@tanstack/devtools-vite] Failed to bump ${packageName}: ${result.error}`,
              ),
            )
            devtoolsEventClient.emit('devtools-installed', {
              packageName: devtoolsPackage,
              success: false,
              error: result.error,
            })
            return
          }

          console.log(
            chalk.greenBright(
              `[@tanstack/devtools-vite] Successfully bumped ${packageName} to ${minVersion}!`,
            ),
          )

          // Check if we found the devtools file
          if (!devtoolsFileId) {
            console.log(
              chalk.yellowBright(
                `[@tanstack/devtools-vite] Devtools file not found. Skipping auto-injection.`,
              ),
            )
            devtoolsEventClient.emit('devtools-installed', {
              packageName: devtoolsPackage,
              success: true,
            })
            return
          }

          // Now inject the devtools plugin
          console.log(
            chalk.blueBright(
              `[@tanstack/devtools-vite] Adding ${devtoolsPackage} to devtools...`,
            ),
          )

          const injectResult = injectPluginIntoFile(devtoolsFileId, {
            packageName: devtoolsPackage,
            pluginName,
            pluginImport,
          })

          if (injectResult.success) {
            console.log(
              chalk.greenBright(
                `[@tanstack/devtools-vite] Successfully added ${devtoolsPackage} to devtools!`,
              ),
            )

            devtoolsEventClient.emit('plugin-added', {
              packageName: devtoolsPackage,
              success: true,
            })

            // Re-read package.json to update the UI
            const updatedPackageJson = await readPackageJson()
            devtoolsEventClient.emit('package-json-read', {
              packageJson: updatedPackageJson,
            })
          } else {
            console.log(
              chalk.redBright(
                `[@tanstack/devtools-vite] Failed to add ${devtoolsPackage} to devtools: ${injectResult.error}`,
              ),
            )

            devtoolsEventClient.emit('plugin-added', {
              packageName: devtoolsPackage,
              success: false,
              error: injectResult.error,
            })
          }
        })

        // whenever a client mounts we send all the current info to the subscribers
        devtoolsEventClient.on('mounted', async () => {
          devtoolsEventClient.emit('outdated-deps-read', {
            outdatedDeps: await outdatedDeps,
          })
          devtoolsEventClient.emit('package-json-read', {
            packageJson,
          })
        })

        // Console piping is now handled via HTTP endpoints in the custom-server plugin
      },
      async handleHotUpdate({ file }) {
        if (file.endsWith('package.json')) {
          const newPackageJson = await readPackageJson()
          devtoolsEventClient.emit('package-json-read', {
            packageJson: newPackageJson,
          })
          emitOutdatedDeps()
        }
      },
    },
    // Enhanced logs must run BEFORE console-pipe-transform: the latter
    // prepends a multi-line IIFE to root entry files, which would shift
    // every line number computed by the oxc-parser AST and produce
    // "Go to Source" links pointing past the end of the user's file.
    {
      name: '@tanstack/devtools:better-console-logs',
      enforce: 'pre',
      apply(config) {
        return config.mode === 'development' && enhancedLogsConfig.enabled
      },
      transform: {
        filter: {
          id: {
            exclude: [/node_modules/, /\?raw/, /\/dist\//, /\/build\//],
          },
          code: {
            include: 'console.',
          },
        },
        handler(code, id) {
          return enhanceConsoleLog(code, id, port)
        },
      },
    },
    // Inject console piping code into entry files (both client and server)
    {
      name: '@tanstack/devtools:console-pipe-transform',
      enforce: 'pre',
      apply(config, { command }) {
        return (
          config.mode === 'development' &&
          command === 'serve' &&
          (consolePipingConfig.enabled ?? true)
        )
      },
      transform: {
        filter: {
          id: {
            include: /\.(tsx?|jsx?)$/,
            exclude: [/node_modules/, /\/dist\//, /\?/],
          },
          code: {
            exclude: /__tsdConsolePipe/, // avoid transforming files that already contain the console pipe code
          },
        },
        handler(code) {
          // Check if this is a root entry file (with <html> JSX or client entry points)
          // In SSR frameworks, this file runs on BOTH server (SSR) and client (hydration)
          // so our runtime check (typeof window === 'undefined') handles both environments
          const isRootEntry =
            /<html[\s>]/i.test(code) ||
            code.includes('StartClient') ||
            code.includes('hydrateRoot') ||
            code.includes('createRoot') ||
            (code.includes('solid-js/web') && code.includes('render('))

          if (isRootEntry) {
            const viteServerUrl = `http://localhost:${port}`
            const inlineCode = generateConsolePipeCode(
              consolePipingLevels,
              viteServerUrl,
            )

            return `${inlineCode}\n${code}`
          }

          return undefined
        },
      },
    },
    {
      name: '@tanstack/devtools:inject-plugin',
      apply(config, { command }) {
        return config.mode === 'development' && command === 'serve'
      },
      transform(code, id) {
        // First pass: find where TanStackDevtools is imported
        if (!devtoolsFileId && detectDevtoolsFile(code)) {
          // Extract actual file path (remove query params)
          const [filePath] = id.split('?')
          if (filePath) {
            devtoolsFileId = filePath
          }
        }

        return undefined
      },
    },
    {
      name: '@tanstack/devtools:connection-injection',
      apply(config, { command }) {
        return config.mode === 'development' && command === 'serve'
      },
      transform(code, id) {
        // Only transform @tanstack packages that contain the connection placeholders
        const hasPlaceholder =
          code.includes('__TANSTACK_DEVTOOLS_PORT__') ||
          code.includes('__TANSTACK_DEVTOOLS_HOST__') ||
          code.includes('__TANSTACK_DEVTOOLS_PROTOCOL__')
        if (!hasPlaceholder) return
        if (
          !id.includes('@tanstack/devtools') &&
          !id.includes('@tanstack/event-bus')
        )
          return

        // Replace placeholders with actual values (or fallback defaults)
        const portValue = devtoolsPort ?? 4206
        const hostValue = devtoolsHost ?? 'localhost'
        const protocolValue = devtoolsProtocol ?? 'http'

        let result = code
        result = result.replace(
          /__TANSTACK_DEVTOOLS_PORT__/g,
          String(portValue),
        )
        result = result.replace(
          /__TANSTACK_DEVTOOLS_HOST__/g,
          JSON.stringify(hostValue),
        )
        result = result.replace(
          /__TANSTACK_DEVTOOLS_PROTOCOL__/g,
          JSON.stringify(protocolValue),
        )
        return result
      },
    },
    {
      name: '@tanstack/devtools:runtime-bridge',
      apply(config, { command }) {
        return config.mode === 'development' && command === 'serve'
      },
      transform(code, id) {
        if (id.includes('?')) return
        return injectRuntimeBridge(
          code,
          id,
          (this as any).environment?.name as string | undefined,
        )
      },
    },
  ]
}
