/**
 * The client/server split of a project (docs/backend-plan.md §4).
 *
 * `*.server.ts` files never reach the client bundle: `compileProject` swaps each one for a STUB
 * module whose exports are RPC/channel proxies (`__rpc`, `__channel`, `__serverOnly` — client SDK
 * globals, src/runtime/rpc.ts), so `import { listNotes } from "./notes.server"` in client code keeps
 * its types and calls the server. `compileServerBundle` bundles the same files with the SERVER inject
 * behind a synthetic entry that registers every module on `globalThis.__lecodesServer` (chisel keeps
 * side effects, not entry exports) — the runner reads that (src/server/runtime.ts).
 *
 * Export kinds are found by a static scan (no TS program needed): a function export is an
 * endpoint, a `channel(...)` export is a channel, anything else is server-only.
 */

import type { AppConfig } from "../server/auth/appConfig"
import { bundleProjectWithMap, SERVER_INJECT, type MemoryFiles } from "./bundler"
import type { CompileEntry } from "./compileProject"
import { deriveEndpointParams } from "./serverTypes"

export const SERVER_FILE = /\.server\.[tj]s$/
export const isServerPath = (path: string) => SERVER_FILE.test(path)

/** Project path → endpoint-id prefix: no leading `./` or `/`, forward slashes. */
export const serverModuleId = (path: string) => path.split("\\").join("/").replace(/^(\.\/|\/)+/, "")

export type ServerExportKind = "fn" | "channel" | "other"
export type ServerExport = { name: string, kind: ServerExportKind }

// ───────────────────────────── static export scan ─────────────────────────────

/** Blank out comments and string/template contents (keeps line structure) so the regexes below don't
 *  trip on `export` inside a comment or a string. */
export const blankCommentsAndStrings = (src: string): string => {
  let out = ""
  let i = 0
  const n = src.length
  while (i < n) {
    const c = src[i], d = src[i + 1]
    if (c === "/" && d === "/") { while (i < n && src[i] !== "\n") { out += " "; i++ } continue }
    if (c === "/" && d === "*") {
      out += "  "; i += 2
      while (i < n && !(src[i] === "*" && src[i + 1] === "/")) { out += src[i] === "\n" ? "\n" : " "; i++ }
      out += "  "; i += 2; continue
    }
    if (c === '"' || c === "'" || c === "`") {
      const q = c
      out += q; i++
      while (i < n && src[i] !== q) {
        if (src[i] === "\\") { out += "  "; i += 2; continue }
        out += src[i] === "\n" ? "\n" : " "; i++
      }
      out += q; i++; continue
    }
    out += c; i++
  }
  return out
}

const IDENT = "[A-Za-z_$][\\w$]*"
const classifyInit = (init: string): ServerExportKind => {
  const s = init.trimStart()
  if (/^channel\s*[<(]/.test(s)) return "channel"
  if (/^(async\s+)?function\b/.test(s)) return "fn"
  if (/^async\b/.test(s)) return "fn"                              // async (…) => / async x =>
  if (new RegExp(`^${IDENT}\\s*=>`).test(s)) return "fn"           // x => …
  if (/^\(/.test(s)) {
    // `(a, b) => …` — find the matching paren and look for `=>` (a type annotation may sit between)
    let depth = 0
    for (let i = 0; i < s.length; i++) {
      if (s[i] === "(") depth++
      else if (s[i] === ")") { depth--; if (depth === 0) return /^\s*(:[^=]*)?=>/.test(s.slice(i + 1)) ? "fn" : "other" }
    }
  }
  return "other"
}

/** Exported names of a server module with their kinds. Default exports are rejected. */
export const scanServerExports = (source: string, path = "<server module>"): ServerExport[] => {
  const src = blankCommentsAndStrings(source)
  if (/(^|\n)\s*export\s+default\b/.test(src)) {
    throw new Error(`${path}: default exports are not endpoints — use named exports`)
  }
  // (position, name, kind) — kept in source order so ids/manifests are stable and readable
  const found: { at: number, name: string, kind: ServerExportKind }[] = []
  const add = (at: number, name: string, kind: ServerExportKind) => {
    const i = found.findIndex(f => f.name === name)
    if (i >= 0) found[i] = { at: found[i].at, name, kind }
    else found.push({ at, name, kind })
  }
  // export [async] function name / export function* name
  for (const m of src.matchAll(new RegExp(`(^|\\n)\\s*export\\s+(async\\s+)?function\\s*\\*?\\s*(${IDENT})`, "g"))) add(m.index!, m[3], "fn")
  // export const|let|var name [: T] = init
  for (const m of src.matchAll(new RegExp(`(^|\\n)\\s*export\\s+(?:const|let|var)\\s+(${IDENT})\\s*(?::[^=]*?)?=\\s*([^\\n]*)`, "g"))) {
    add(m.index!, m[2], classifyInit(m[3]))
  }
  // export class name → server-only
  for (const m of src.matchAll(new RegExp(`(^|\\n)\\s*export\\s+class\\s+(${IDENT})`, "g"))) add(m.index!, m[2], "other")
  // export { a, b as c } → classify by the local declaration
  for (const m of src.matchAll(/(^|\n)\s*export\s*\{([^}]*)\}/g)) {
    for (const part of m[2].split(",")) {
      const [local, exported] = part.split(/\s+as\s+/).map(s => s.trim())
      if (!local || /^type\s/.test(local)) continue
      const name = exported ?? local
      let kind: ServerExportKind = "other"
      if (new RegExp(`(^|\\n)\\s*(async\\s+)?function\\s*\\*?\\s*${local}\\b`).test(src)) kind = "fn"
      else {
        const decl = src.match(new RegExp(`(^|\\n)\\s*(?:const|let|var)\\s+${local}\\s*(?::[^=]*?)?=\\s*([^\\n]*)`))
        if (decl) kind = classifyInit(decl[2])
      }
      add(m.index!, name, kind)
    }
  }
  return found.sort((a, b) => a.at - b.at).map(({ name, kind }) => ({ name, kind }))
}

// ───────────────────────────── client stubs ─────────────────────────────

/** The client-side replacement of a `*.server.ts` module. `serverUrl` is baked in (draft/prod are
 *  separate compiles); "" means "no server deployed" — calls fail with a clear error. */
export const serverStubSource = (path: string, exports: ServerExport[], serverUrl: string): string => {
  const mod = serverModuleId(path)
  const url = JSON.stringify(serverUrl)
  const lines = exports.map(({ name, kind }) => {
    const id = JSON.stringify(`${mod}#${name}`)
    if (kind === "fn") return `export const ${name} = __rpc(${url}, ${id})`
    if (kind === "channel") return `export const ${name} = __channel(${url}, ${id})`
    return `export const ${name} = __serverOnly(${id})`
  })
  return `// client stub of ${mod} — generated by the lecodes compiler\n${lines.join("\n")}\n`
}

// ───────────────────────────── server entry ─────────────────────────────

export const SERVER_ENTRY_PATH = "/.server-entry.ts"
/** The SDK's app-config placeholder (src/server/auth/appConfig.ts) — the compile overrides it in the file map
 *  with the project's real values (project files win over SDK files), so `auth.model.user()` and friends see
 *  app.json as an ordinary import. */
export const APP_CONFIG_PATH = "/__sdk/server/auth/appConfig.ts"

/** Compile-time manifest embedded in the server bundle (validators land here — see serverTypes.ts). */
export type ServerManifest = {
  modules: Record<string, ServerExport[]>
  /** endpoint id → JSON-schema-ish param list (filled by the TS-checker pass; absent = unvalidated). */
  params?: Record<string, unknown[]>
  /** The `app.json` subset the server runtime reads (name, auth providers); the bundle itself gets it as the
   *  compiled-in `appConfig` module (APP_CONFIG_PATH). */
  app?: AppConfig
  /** Some server file references the `auth` global: the runner then resolves a device session on every
   *  request (minting one on first contact). Projects that never touch `auth` pay nothing. */
  usesAuth?: boolean
}

/** The synthetic entry: imports every export by name (chisel has no namespace imports) and registers
 *  the module map + manifest on `globalThis.__lecodesServer` — a side effect chisel keeps. */
export const serverEntrySource = (modules: Record<string, ServerExport[]>, manifest: ServerManifest): string => {
  const imports: string[] = []
  const entries: string[] = []
  Object.entries(modules).forEach(([path, exports], i) => {
    const names = exports.map(e => `${e.name} as m${i}_${e.name}`).join(", ")
    if (names) imports.push(`import { ${names} } from ${JSON.stringify(path)}`)
    else imports.push(`import ${JSON.stringify(path)}`)
    const props = exports.map(e => `${JSON.stringify(e.name)}: m${i}_${e.name}`).join(", ")
    entries.push(`${JSON.stringify(serverModuleId(path))}: { ${props} }`)
  })
  return `${imports.join("\n")}\n;(globalThis as any).__lecodesServer = { modules: { ${entries.join(", ")} }, manifest: ${JSON.stringify(manifest)} }\n`
}

const normalizePath = (path: string) => {
  const parts: string[] = []
  for (const part of path.split("\\").join("/").split("/")) {
    if (!part || part === ".") continue
    if (part === "..") parts.pop()
    else parts.push(part)
  }
  return "/" + parts.join("/")
}

/** Scan every `*.server.ts` entry → module path → exports. Throws on a default export. */
export const scanServerModules = (entries: CompileEntry[]): Record<string, ServerExport[]> => {
  const out: Record<string, ServerExport[]> = {}
  for (const e of entries) {
    if (e.type !== "text" || !isServerPath(e.path)) continue
    out[normalizePath(e.path)] = scanServerExports(e.text ?? "", serverModuleId(e.path))
  }
  return out
}

/** Replace each `*.server.ts` entry with its client stub (returns a new entries array). */
export const stubServerEntries = (entries: CompileEntry[], serverUrl: string): CompileEntry[] =>
  entries.map(e => e.type === "text" && isServerPath(e.path)
    ? { ...e, text: serverStubSource(e.path, scanServerExports(e.text ?? "", serverModuleId(e.path)), serverUrl) }
    : e)

export type CompileServerOptions = {
  entries: CompileEntry[]
  /** Derive per-endpoint argument validators from the TS parameter types (serverTypes.ts) and embed
   *  them in the manifest. Default true; throws `EndpointTypeError` on non-JSON parameter types. */
  validate?: boolean
  /** Precomputed param schemas to embed instead of deriving them. */
  params?: Record<string, unknown[]>
  /** Server bundles are readable by default (stack traces in the server console). */
  minify?: boolean
  sourcemap?: boolean
  /** The project's `app.json` (name + auth providers) — embedded in the manifest and exposed to the bundle. */
  app?: AppConfig
}

/** Bundle the project's server side. Returns the code (a script that registers
 *  `globalThis.__lecodesServer`) and the manifest; `null` when the project has no server files. */
export const compileServerBundle = async (opts: CompileServerOptions): Promise<{ code: string, manifest: ServerManifest } | null> => {
  const modules = scanServerModules(opts.entries)
  if (Object.keys(modules).length === 0) return null
  const files: MemoryFiles = {}
  for (const e of opts.entries) {
    if (e.type !== "text") continue
    if (e.path.toLowerCase().endsWith(".svg")) continue
    files[normalizePath(e.path)] = e.text ?? ""
  }
  let params = opts.params
  if (!params && opts.validate !== false) {
    const derived = await deriveEndpointParams(opts.entries, modules)
    for (const w of derived.warnings) console.warn(`warning: ${w}`)
    params = derived.params
  }
  const usesAuth = opts.entries.some(e => e.type === "text" && isServerPath(e.path) && /\bauth\s*\./.test(blankCommentsAndStrings(e.text ?? "")))
  const manifest: ServerManifest = { modules: Object.fromEntries(Object.entries(modules).map(([p, ex]) => [serverModuleId(p), ex])), params, app: opts.app, usesAuth }
  if (opts.app) files[APP_CONFIG_PATH] = `export const app = ${JSON.stringify(opts.app)}\n`
  files[SERVER_ENTRY_PATH] = serverEntrySource(modules, manifest)
  const { code } = await bundleProjectWithMap(
    SERVER_ENTRY_PATH, files, "esm", opts.minify ?? false, false, undefined, opts.sourcemap ?? false, SERVER_INJECT,
  )
  return { code, manifest }
}
