import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { configuration } from '@vxrn/compiler'
import micromatch from 'micromatch'
import type { Plugin } from 'vite'
import { isNativeEnvironment } from 'vxrn'
import {
  API_ROUTE_GLOB_PATTERN,
  ROUTE_GLOB_PATTERN,
  ROUTE_NATIVE_EXCLUSION_GLOB_PATTERNS,
  ROUTE_WEB_EXCLUSION_GLOB_PATTERNS,
} from '../../router/glob-patterns'
import { matchDirectoryRenderMode, matchFileRenderMode } from '../../router/matchers'
import type { RouteIndex } from '../../utils/routeIndex'
import type { One } from '../types'
import {
  resolvedVirtualEntryId,
  resolvedVirtualEntryIdNative,
  virtualEntryId,
  virtualEntryIdNative,
} from './virtualEntryConstants'

// rust-side hook filters, derived from the ids so they can't drift
const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const VIRTUAL_ENTRY_SOURCE_RE = new RegExp(
  `^(?:${escapeRe(virtualEntryId)}|${escapeRe(virtualEntryIdNative)})$`
)
const VIRTUAL_ENTRY_RESOLVED_RE = new RegExp(
  `^(?:${escapeRe(resolvedVirtualEntryId)}|${escapeRe(resolvedVirtualEntryIdNative)})$`
)

type NormalizedSetupFiles = {
  client?: string
  server?: string
  ios?: string
  android?: string
}

function normalizeSetupFile(
  setupFile: One.PluginOptions['setupFile']
): NormalizedSetupFiles {
  if (!setupFile) return {}
  if (typeof setupFile === 'string') {
    return {
      client: setupFile,
      server: setupFile,
      ios: setupFile,
      android: setupFile,
    }
  }
  if ('native' in setupFile) {
    return {
      client: setupFile.client,
      server: setupFile.server,
      ios: setupFile.native,
      android: setupFile.native,
    }
  }
  const sf = setupFile as {
    client?: string
    server?: string
    ios?: string
    android?: string
  }
  return {
    client: sf.client,
    server: sf.server,
    ios: sf.ios,
    android: sf.android,
  }
}

type SetupImportResult = {
  /** Import statement to prepend (for native static imports) */
  importStatement: string
  /** Variable declaration for setup promise (for web dynamic imports) */
  promiseDeclaration: string
  /** The variable name to pass to createApp, or empty if no setup */
  promiseVarName: string
}

function getSetupFileImport(
  environmentName: string,
  setupFiles: NormalizedSetupFiles,
  useStaticImport: boolean,
  root?: string
): SetupImportResult {
  const envMap: Record<string, keyof NormalizedSetupFiles> = {
    client: 'client',
    ssr: 'server',
    ios: 'ios',
    android: 'android',
  }

  const key = envMap[environmentName]
  if (!key) return { importStatement: '', promiseDeclaration: '', promiseVarName: '' }

  const setupFile = setupFiles[key]
  if (!setupFile)
    return { importStatement: '', promiseDeclaration: '', promiseVarName: '' }

  // file:// URL is canonical; bare Windows absolute path has backslashes (mirrors createNativeDevEngine.ts)
  const resolvedSetupFile = root
    ? pathToFileURL(resolve(root, setupFile)).href
    : setupFile

  // For native, use static import since dynamic import doesn't work
  if (useStaticImport) {
    return {
      importStatement: `import ${JSON.stringify(resolvedSetupFile)}`,
      promiseDeclaration: '',
      promiseVarName: '',
    }
  }

  // For web, use a lazy function that returns the dynamic import promise.
  // This ensures the import only executes at runtime when createApp calls it,
  // not during build when the module is evaluated.
  return {
    importStatement: '',
    promiseDeclaration: `const __oneGetSetupPromise = () => import(${JSON.stringify(resolvedSetupFile)})`,
    promiseVarName: '__oneGetSetupPromise',
  }
}

export function createVirtualEntry(options: {
  root: string
  router?: One.PluginOptions['router']
  flags: One.Flags
  setupFile?: One.PluginOptions['setupFile']
  routeIndex: RouteIndex
}): Plugin {
  const routeGlobs = [
    `/${options.root}/${ROUTE_GLOB_PATTERN}`,
    ...(options.router?.ignoredRouteFiles?.map(
      (pattern) => `!/${options.root}/${pattern}`
    ) || []),
  ]
  const apiRouteGlobs = `/${options.root}/${API_ROUTE_GLOB_PATTERN}`

  const setupFiles = normalizeSetupFile(options.setupFile)
  let viteRoot = ''
  let isDevMode = false

  return {
    name: 'one-virtual-entry',
    enforce: 'pre',

    configResolved(config) {
      viteRoot = config.root
      isDevMode = config.command === 'serve'
    },

    // only the two virtual entry ids reach js; every other import in the graph
    // is rejected rust-side
    resolveId: {
      filter: { id: VIRTUAL_ENTRY_SOURCE_RE },
      handler(id) {
        if (id === virtualEntryId) {
          return resolvedVirtualEntryId
        }
        if (id === virtualEntryIdNative) {
          return resolvedVirtualEntryIdNative
        }
      },
    },

    load: {
      filter: { id: VIRTUAL_ENTRY_RESOLVED_RE },
      handler(id) {
        if (id === resolvedVirtualEntryId) {
          const isNative = isNativeEnvironment(this.environment)
          const isSSR = this.environment.name === 'ssr'
          const serverSpaRouteFiles = isSSR
            ? options.routeIndex
                .getPaths()
                .map((file) => file.replace(/^\.\//, ''))
                .filter((file) => /\.tsx?$/.test(file))
                .filter(
                  (file) => !micromatch.isMatch(file, ROUTE_WEB_EXCLUSION_GLOB_PATTERNS)
                )
                .filter((file) => {
                  const parts = file.split('/')
                  const filename = parts.at(-1) || ''
                  if (
                    filename.startsWith('_layout') ||
                    filename.startsWith('_middleware')
                  ) {
                    return false
                  }

                  const fileMode = matchFileRenderMode(filename)
                  if (fileMode) return fileMode === 'spa'

                  let parentMode: One.RouteRenderMode | 'api' | undefined
                  for (const directory of parts.slice(0, -1)) {
                    parentMode =
                      matchDirectoryRenderMode(directory)?.renderMode ?? parentMode
                  }
                  return parentMode === 'spa'
                })
            : []
          const serverSpaRouteStubs = serverSpaRouteFiles
            .map(
              (file) =>
                `${JSON.stringify(`/${options.root}/${file}`)}: () => Promise.resolve({})`
            )
            .join(',\n    ')
          const serverSpaBuildGlobs = serverSpaRouteFiles.map(
            (file) => `/${options.root}/${file}`
          )
          const serverSpaBuildRoutes = serverSpaBuildGlobs.length
            ? `{
  loader: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'loader', query: '?one-spa-build' }),
  generateStaticParams: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'generateStaticParams', query: '?one-spa-build' }),
  sitemap: import.meta.glob(${JSON.stringify(serverSpaBuildGlobs)}, { exhaustive: true, import: 'sitemap', query: '?one-spa-build' })
}`
            : '{}'
          const webRouteGlobs = [
            ...routeGlobs,
            ...ROUTE_WEB_EXCLUSION_GLOB_PATTERNS.map((pattern) => `!${pattern}`),
            ...serverSpaRouteFiles.map((file) => `!/${options.root}/${file}`),
          ]
          // native always needs static import. SSR in dev mode uses static import
          // so Vite crawls the setupFile's dep tree (including react) and pre-bundles
          // them before the first request. SSR in build mode uses lazy import so the
          // setupFile doesn't execute during the build phase.
          const useStaticImport = isNative || (isSSR && isDevMode)
          const setupResult = getSetupFileImport(
            this.environment.name,
            setupFiles,
            useStaticImport,
            viteRoot
          )
          // When nativewind is enabled, import the components module to register Text, View, etc. with cssInterop
          const nativewindImport = configuration.enableNativewind
            ? `import 'react-native-css-interop/dist/runtime/components'`
            : ''
          // For web/SSR, pass getSetupPromise to createApp so it can call it at runtime
          const setupPromiseArg = setupResult.promiseVarName
            ? `getSetupPromise: ${setupResult.promiseVarName},`
            : ''
          const linkingArg = options.router?.linking
            ? `linking: ${JSON.stringify(options.router.linking)},`
            : ''

          // the react-refresh preamble has to be installed before the first
          // compiler-wrapped module evaluates, or that module throws "React refresh
          // preamble was not loaded" and client init aborts. /@one/dev.js installs
          // it, but it cannot be relied on to win: it is a DEFERRED module script,
          // so it waits for the document to finish parsing, while the client entry
          // is async and runs the moment it arrives. that race is decided by
          // document size. a SPA shell is a few hundred bytes and dev.js wins; a
          // prerendered +ssg/ssr page is hundreds of KB and the entry regularly
          // wins, throws on the first route module, and leaves the page unhydrated
          // and unthemed on the very same server. bundled dev has no working
          // /@one/dev.js at all, since it imports /@vite/client, which does not
          // exist there.
          // so install it from the entry, whose body is the one point guaranteed to
          // run before the lazily-globbed route modules in every dev client. the
          // !$RefreshReg$ guard makes this a no-op when dev.js already won, and
          // dev.js keeps its own install because injectIntoGlobalHook wants to run
          // before react evaluates. ('one' and /@react-refresh are node_modules /
          // the runtime, so neither is refresh-wrapped, and on the client the setup
          // file is imported lazily rather than statically.)
          // this must stay gated to a dev CLIENT. `isBundled` is true for EVERY
          // environment during build, so it cannot gate this on its own, and a prod
          // build emitting an import of /@react-refresh would reference a module
          // that only exists while serving.
          const isDevClient =
            this.environment.mode === 'dev' && this.environment.name === 'client'
          const refreshPreambleImport = isDevClient
            ? `import { injectIntoGlobalHook as __oneInjectRefresh } from '/@react-refresh'`
            : ''
          const refreshPreambleSetup = isDevClient
            ? `if (typeof window !== 'undefined' && !window.$RefreshReg$) {
  __oneInjectRefresh(window)
  window.$RefreshReg$ = () => {}
  window.$RefreshSig$ = () => (type) => type
}`
            : ''
          return `
${setupResult.promiseDeclaration}
${nativewindImport}
${refreshPreambleImport}

import { createApp, registerPreloadedRoute as _registerPreloadedRoute } from 'one'
${setupResult.importStatement}
${refreshPreambleSetup}

// Export registerPreloadedRoute so preload files can import it from this bundle
// Named export that wraps the original function
export function registerPreloadedRoute(key, module) {
  return _registerPreloadedRoute(key, module)
}

// Also expose on window for debugging and to prevent tree-shaking
if (typeof window !== 'undefined') {
  window.__oneRegisterPreloadedRoute = registerPreloadedRoute
}

// build-only spa exports generate route artifacts without exposing the page
// component to the runtime server route map.
export const oneBuildOnlySpaRoutes = ${serverSpaBuildRoutes}

// globbing ${JSON.stringify(webRouteGlobs)}
export default createApp({
  ${setupPromiseArg}
  routes: {
    ...import.meta.glob(${JSON.stringify(webRouteGlobs)}, { exhaustive: true }),
    ${serverSpaRouteStubs}
  },
  routerRoot: ${JSON.stringify(options.root)},
  flags: ${JSON.stringify(options.flags)},
  ${linkingArg}
})
        `
        }

        if (id === resolvedVirtualEntryIdNative) {
          const isNative = isNativeEnvironment(this.environment)
          const setupResult = getSetupFileImport(
            this.environment.name,
            setupFiles,
            isNative,
            viteRoot
          )
          const linkingArg = options.router?.linking
            ? `linking: ${JSON.stringify(options.router.linking)},`
            : ''
          return `
import { createApp } from 'one'
${setupResult.importStatement}

// globbing ${JSON.stringify(routeGlobs)}
export default createApp({
  routes: import.meta.glob(${JSON.stringify([...routeGlobs, ...ROUTE_NATIVE_EXCLUSION_GLOB_PATTERNS.map((p) => `!${p}`), `!${apiRouteGlobs}`])}, { exhaustive: true }),
  routerRoot: ${JSON.stringify(options.root)},
  flags: ${JSON.stringify(options.flags)},
  ${linkingArg}
})
        `
        }
      },
    },
  }
}
