// Entrypoint detection from the project's import graph. The entry is the file no other project
// file imports (an import-graph root): a side-effect registration module is imported by the entry,
// so it stops being a candidate, and the entry itself may freely use `export`.
//
// Pick order among roots: `/main.ts` (the documented convention) → export-free roots by size →
// any root by size. `roots.length > 1` without a `/main.ts` means the choice was ambiguous —
// CLI callers surface that as a warning suggesting `main.ts` or `--entry`.
//
// This module is intentionally dependency-free (no node imports) so the FRONTEND can run the same
// detection live in the editor. Node callers inject chisel's `scan` mode (real parser — comments,
// strings, `import type` handled correctly) via the `scanner` parameter (`scanModulesChisel` from
// bundler.ts); without one — in the browser, or with a pre-scan chisel binary — the regex fallback
// below runs over the same resolution rules.

type Files = Record<string, string>

/** Per-module facts entry detection consumes (chisel `scan` output shape). */
export type ScanModule = { imports: string[], hasExports: boolean, error?: string }

/** A pluggable module scanner: path→source in, per-module facts out (null = unavailable). */
export type ModuleScanner = (files: Files) => Record<string, ScanModule> | null

export type DetectEntryInput = {
  path: string
  type?: string
  text?: string | null
}

export type DetectedEntry = {
  /** The chosen entry path ("" when the project has no candidate module at all). */
  entry: string
  /** Import-graph roots. More than one (without a `/main.ts` among them) = ambiguous choice. */
  roots: string[]
}

/** The conventional entry filename — always wins when it is an import-graph root. */
export const ENTRY_CONVENTION = "/main.ts"

const isCode = (path: string) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts")

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

const dirOf = (path: string): string => {
  const i = path.lastIndexOf("/")
  return i <= 0 ? "/" : path.slice(0, i)
}

/** Mirror of chisel's resolver: relative/absolute specifiers only, `.ts`/`.js`/`/index.ts`
 *  fallback, exact match first. Null for bare imports (libraries) and unresolved paths. */
const resolveSpec = (files: Files, importer: string, spec: string): string | null => {
  if (!spec.startsWith("./") && !spec.startsWith("../") && !spec.startsWith("/")) return null
  const base = normalize(spec.startsWith("/") ? spec : dirOf(importer) + "/" + spec)
  for (const cand of [base, base + ".ts", base + ".js", base + "/index.ts"]) {
    if (cand in files) return cand
  }
  return null
}

// One pattern, two alternatives: `import|export … from "<spec>"` (multi-line safe — the specifier
// clause may sit on a later line) and a side-effect `import "<spec>"`.
const IMPORT_RE = /(?:^|[\n;])\s*(?:import|export)\b[^'"`]*?\bfrom\s*['"]([^'"\n]+)['"]|(?:^|[\n;])\s*import\s*['"]([^'"\n]+)['"]/g
const EXPORT_RE = /(?:^|[\n;])\s*export\b/

/** Regex fallback for a chisel binary without scan support. Same output shape; less precise
 *  (a specifier inside a comment counts, `export type` counts as an export) — both only affect
 *  which root wins a tie, never whether the true entry is a root. */
export const scanModulesFallback = (files: Files): Record<string, ScanModule> => {
  const out: Record<string, ScanModule> = {}
  for (const [path, text] of Object.entries(files)) {
    const imports: string[] = []
    for (const m of text.matchAll(IMPORT_RE)) {
      const target = resolveSpec(files, path, m[1] ?? m[2] ?? "")
      if (target && isCode(target) && !imports.includes(target)) imports.push(target)
    }
    out[path] = { imports, hasExports: EXPORT_RE.test(text) }
  }
  return out
}

/**
 * Detect the project's entrypoint. `entries` is the full project file list; only `.ts`/`.js` text
 * files participate (`.d.ts` and `.editor.ts` are never entries, and editor-only imports don't
 * remove a file from candidacy — production compiles don't bundle editor plugins).
 */
export const detectEntry = (entries: DetectEntryInput[], scanner?: ModuleScanner): DetectedEntry => {
  const files: Files = {}
  for (const e of entries) {
    if (e.type !== undefined && e.type !== "text") continue
    if (typeof e.text !== "string") continue
    const path = normalize(e.path.split("\\").join("/"))
    if (isCode(path)) files[path] = e.text
  }

  const candidates = Object.keys(files).filter((p) => !p.endsWith(".editor.ts"))
  if (candidates.length === 0) return { entry: "", roots: [] }

  const scan = scanner?.(files) ?? scanModulesFallback(files)

  const imported = new Set<string>()
  for (const [path, mod] of Object.entries(scan)) {
    if (path.endsWith(".editor.ts")) continue
    for (const target of mod.imports) imported.add(target)
  }

  let roots = candidates.filter((p) => !imported.has(p))
  // Everything imported (a cycle through every file) — degrade to all candidates.
  if (roots.length === 0) roots = candidates

  if (roots.includes(ENTRY_CONVENTION)) return { entry: ENTRY_CONVENTION, roots }

  // Export-free roots are scripts — the old heuristic's signal, kept as the tie-breaker so
  // existing projects resolve the same way; a lone exporting root is still a valid entry.
  const scripts = roots.filter((p) => !scan[p]?.hasExports)
  const pool = scripts.length > 0 ? scripts : roots
  const bySize = [...pool].sort((a, b) => files[b].length - files[a].length)
  return { entry: bySize[0], roots }
}
