// Turns a project's files into the esbuild input map + entrypoint + metadata header, then bundles.
// The one canonical "user project → deployable bundle" path: the backend and CLI both call this
// (the backend resolves asset URLs from the DB, the CLI from local metadata — same function, the
// URLs are passed in as `publicUrl` + per-entry `fileSrc`/`shaders`).

import { bundleProjectWithMap, scanModulesChisel, type MemoryFiles } from "./bundler"
import { detectEntry } from "./detectEntry"
import { buildHeader } from "./header"
import { inlineSourceMapComment, offsetSourceMap } from "./sourcemap"
import { stubServerEntries } from "./serverSplit"

export type CompileFileType = "text" | "resource" | "shader"

export type CompileEntry = {
  path: string
  type: CompileFileType
  /** Source text for code / .svg files. */
  text?: string
  /** Server src for a binary resource (e.g. "/uploads/x.png"); prefixed with publicUrl. */
  fileSrc?: string
  /** Under `localAssets`: keep THIS resource in the URL form (publicUrl + fileSrc) anyway — for
   *  hosts that bundle most assets locally but stream heavy ones (e.g. videos) from the server. */
  remote?: boolean
  /** Compiled-shader artifacts (platform + src); the first drives the per-backend URL. */
  shaders?: { platform: string, src: string }[]
  /** `shaders` are LOCAL files staged next to the bundle (bare artifact names, not server srcs):
   *  the module compiles to a fetchLocal lookup instead of a URL. Set by the CLI's local shader
   *  compile for hosts with no asset server (the desktop host). */
  shadersLocal?: boolean
}

export type CompileOptions = {
  entries: CompileEntry[]
  name: string
  publicUrl: string
  entryOverride?: string
  /** Prepend the metadata header (default true). */
  header?: boolean
  /** Generated-icon URL for the header (backend only). */
  icon?: string
  /** `// fonts:` header payload (from the font() macro pass the caller ran over the entries). */
  fontsHeader?: string
  /** `// icons:` header payload (from the assetIcon() macro pass the caller ran over the entries). */
  iconsHeader?: string
  /** "esm" (default, shipped) or "iife" (self-running, for the local renderer). */
  format?: "esm" | "iife"
  /** Resolve `asset()` resources to a LOCAL host asset at runtime instead of a `publicUrl` URL: each
   *  resource compiles to `"id:" + _creatorUtils.fetchLocal(<name>)` (the host reads the file by name
   *  and returns its systemId). For hosts with no asset server (the desktop native host), where a URL
   *  can't be fetched. Off by default — the shipped/web build keeps the URL form. */
  localAssets?: boolean
  /** Override minification (default: on, `LECODES_CHISEL_MINIFY` env aside). Scene-editor bundles
   *  compile with `false`: mangling erases class names, which the editor matches aspect schemas by. */
  minify?: boolean
  /** Generate a source map (default true) and inline it into the bundle. `false` skips the map
   *  entirely — a smaller bundle, but runtime errors report generated positions only. */
  sourcemap?: boolean
  /** Editor compile: the `EDITOR` global compiles to `true` (scene-editor bundles). Default false —
   *  shipped bundles get `EDITOR` = `false`, const-folded so `if (EDITOR)` branches are DCE'd. */
  editor?: boolean
  /** Component-library dependencies: slug → the library's files (root-relative paths). Merged under
   *  `/__lib/<slug>/…` and reachable from project code via `import … from "<slug>"`. See
   *  docs/library-system-plan.md §4. */
  libraries?: Record<string, MemoryFiles>
  /** Base URL of the project's deployed server (draft or prod), baked into the RPC/channel stubs
   *  that replace every `*.server.ts` module in the client bundle (docs/backend-plan.md §4). Server
   *  files never reach the client bundle either way; with no URL, calling an endpoint fails with
   *  "server not deployed". */
  serverUrl?: string
}

export const compileProject = async (opts: CompileOptions): Promise<string> => {
  // Client bundle: every *.server.ts becomes its RPC/channel stub (server code stays server-side).
  const entries = stubServerEntries(opts.entries, opts.serverUrl ?? "")
  const entry = opts.entryOverride ?? detectEntry(entries, scanModulesChisel).entry
  if (!entry) {
    throw new Error("Couldn't find an entrypoint (no .ts/.js project file — name yours main.ts). Pass entryOverride.")
  }

  const files: Record<string, string> = {}
  for (const e of entries) {
    if (e.type === "resource") {
      if (opts.localAssets && !e.remote) {
        // The host resolves the asset by name at runtime (reads the local file → systemId); the
        // "id:" prefix is the host's local-resource URL scheme (parsed by the UI + 2D texture paths).
        const name = e.path.split(/[\\/]/).pop() ?? e.path
        files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`
      } else {
        files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`
      }
    } else if (e.type === "shader") {
      const shaders = e.shaders ?? []
      if (e.shadersLocal && shaders.length > 0) {
        // Artifacts compiled locally and staged NEXT TO the bundle (no asset server): the host
        // reads them by name through the same fetchLocal door the built-in materials use
        // (`Material.lit` → fetchLocal("lit.filamat")), and Material.load parses the "id:" scheme.
        // The name is built at RUNTIME because one bundle serves every native backend — the
        // desktop host reports "desktop" on Windows and "opengl" on Linux, iOS "metal".
        // Only entries the CLI actually compiled+staged carry `shadersLocal`; server artifacts
        // (URLs) and un-staged ones keep the URL forms below.
        const name = shaders[0].src.split(/[\\/]/).pop() ?? ""
        const [prefix, suffix = ".filamat"] = name.split("_opengl")
        files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(prefix + "_")} + _creator.backend + ${JSON.stringify(suffix)}))`
      } else if (shaders.length > 2) {
        files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``
      } else if (shaders.length > 0) {
        files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``
      } else {
        files[e.path] = `export default ""` // local-only shader with no compiled artifact yet
      }
    } else if (e.path.toLowerCase().endsWith(".svg")) {
      files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`
    } else {
      files[e.path] = e.text ?? ""
    }
  }

  const { code, map } = await bundleProjectWithMap(
    entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true,
  )
  if (opts.header === false) {
    return map ? `${code}\n${inlineSourceMapComment(map)}` : code
  }
  const header = buildHeader(opts.name, code, { icon: opts.icon, fonts: opts.fontsHeader, icons: opts.iconsHeader })
  if (!map) return header + code
  // Shift the map down past the header (prepended before the code); the inline comment lands last.
  const headerLines = (header.match(/\n/g) ?? []).length
  return `${header}${code}\n${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`
}
