/**
 * Endpoint parameter schemas from TypeScript types (docs/backend-plan.md §2.3 / §4.3).
 *
 * Endpoints are plain functions; their argument types are ordinary TS annotations. The client is not
 * trusted, so the server validates every call — and rather than making developers write a schema
 * twice, this pass runs the TypeScript checker over the project's `*.server.ts` files (in memory) and
 * lowers each exported function's parameter types to the compact JSON schema `src/server/validate.ts`
 * interprets at runtime. The result is embedded in the server bundle's manifest.
 *
 * Supported: primitives, literals, unions, enums, arrays, tuples, object types / interfaces / type
 * aliases (from any project file), optional properties, index signatures (`Record<string, T>`),
 * optional / defaulted / rest parameters, `any`/`unknown` (= accept anything). Rejected with a
 * compile error: functions, classes with methods, `Date`, `Map`/`Set`, symbols, recursive types —
 * "endpoint args must be JSON-serializable". Return types are not validated.
 *
 * `typescript` is loaded lazily (it's a devDependency of the SDK and a dependency of the platform
 * compile step); the browser-safe parts of the compiler never import this module's runtime.
 */

import type { CompileEntry } from "./compileProject"
import type { ParamSchema, Schema } from "../server/validate"
import { serverModuleId, type ServerExport } from "./serverSplit"

type TS = typeof import("typescript")

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("/")
}

export class EndpointTypeError extends Error {
  readonly problems: string[]
  constructor(problems: string[]) {
    super(`endpoint args must be JSON-serializable:\n  ${problems.join("\n  ")}`)
    this.name = "EndpointTypeError"
    this.problems = problems
  }
}

export type DeriveResult = {
  /** endpoint id → parameter schemas (only endpoints the checker could see). */
  params: Record<string, ParamSchema[]>
  warnings: string[]
}

/**
 * Derive `{ "<module>#<fn>": ParamSchema[] }` for every function export of every `*.server.ts` entry.
 * Throws `EndpointTypeError` when a parameter type can't be validated at runtime.
 */
export const deriveEndpointParams = async (entries: CompileEntry[], modules: Record<string, ServerExport[]>): Promise<DeriveResult> => {
  let ts: TS
  try {
    ts = (await import("typescript")).default ?? (await import("typescript")) as unknown as TS
  } catch {
    throw new Error("serverTypes: the `typescript` package is required to compile server endpoints")
  }

  const files = new Map<string, string>()
  for (const e of entries) {
    if (e.type !== "text" || typeof e.text !== "string") continue
    if (!/\.[tj]sx?$/.test(e.path)) continue
    files.set(normalizePath(e.path), e.text)
  }
  const roots = Object.keys(modules).map(normalizePath).filter(p => files.has(p))

  const options: import("typescript").CompilerOptions = {
    target: ts.ScriptTarget.ESNext,
    module: ts.ModuleKind.ESNext,
    moduleResolution: ts.ModuleResolutionKind.Bundler,
    allowImportingTsExtensions: true,
    strict: true,
    noEmit: true,
    skipLibCheck: true,
    types: [],
    lib: ["lib.es2022.d.ts"],
  }
  const defaultLibPath = ts.getDefaultLibFilePath(options).split("\\").join("/")
  const defaultLibDir = defaultLibPath.slice(0, defaultLibPath.lastIndexOf("/"))
  const sys = ts.sys
  const host: import("typescript").CompilerHost = {
    fileExists: p => files.has(normalizePath(p)) || (p.split("\\").join("/").startsWith(defaultLibDir) && sys.fileExists(p)),
    readFile: p => files.get(normalizePath(p)) ?? (p.split("\\").join("/").startsWith(defaultLibDir) ? sys.readFile(p) : undefined),
    getSourceFile: (p, lang) => {
      const text = files.get(normalizePath(p)) ?? (p.split("\\").join("/").startsWith(defaultLibDir) ? sys.readFile(p) : undefined)
      return text === undefined ? undefined : ts.createSourceFile(p, text, lang, true)
    },
    getDefaultLibFileName: o => ts.getDefaultLibFilePath(o),
    writeFile: () => {},
    getCurrentDirectory: () => "/",
    getCanonicalFileName: p => p,
    useCaseSensitiveFileNames: () => true,
    getNewLine: () => "\n",
    directoryExists: () => true,
    getDirectories: () => [],
  }
  const program = ts.createProgram(roots, options, host)
  const checker = program.getTypeChecker()

  const params: Record<string, ParamSchema[]> = {}
  const warnings: string[] = []
  const problems: string[] = []

  for (const [path, exports] of Object.entries(modules)) {
    const sf = program.getSourceFile(normalizePath(path))
    if (!sf) { warnings.push(`${path}: not in the TS program`); continue }
    const modSym = checker.getSymbolAtLocation(sf)
    if (!modSym) continue
    const exported = new Map(checker.getExportsOfModule(modSym).map(s => [s.name, s]))
    for (const ex of exports) {
      if (ex.kind !== "fn") continue
      const id = `${serverModuleId(path)}#${ex.name}`
      let sym = exported.get(ex.name)
      if (!sym) { warnings.push(`${id}: export not found by the checker — unvalidated`); continue }
      if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym)
      const decl = sym.valueDeclaration ?? sym.declarations?.[0]
      if (!decl) { warnings.push(`${id}: no declaration — unvalidated`); continue }
      const type = checker.getTypeOfSymbolAtLocation(sym, decl)
      const sig = type.getCallSignatures()[0]
      if (!sig) { warnings.push(`${id}: not callable — unvalidated`); continue }
      const list: ParamSchema[] = []
      for (const p of sig.getParameters()) {
        const pd = p.valueDeclaration as import("typescript").ParameterDeclaration | undefined
        const ptype = checker.getTypeOfSymbolAtLocation(p, pd ?? decl)
        const rest = !!pd?.dotDotDotToken
        const optional = !!pd?.questionToken || !!pd?.initializer || rest
        try {
          // `x?: T` carries `undefined` in its type — the optional flag covers it, strip it from the schema
          const members = ptype.flags & ts.TypeFlags.Union ? (ptype as import("typescript").UnionType).types : [ptype]
          const schema = lowerMembers(ts, checker, pd?.questionToken ? members.filter(t => !(t.flags & ts.TypeFlags.Undefined)) : members, `${id}(${p.name})`, new Set())
          list.push({ name: p.name, schema, optional, rest })
        } catch (e) {
          problems.push((e as Error).message)
        }
      }
      params[id] = list
    }
  }
  if (problems.length) throw new EndpointTypeError(problems)
  return { params, warnings }
}

// ───────────────────────────── type → schema ─────────────────────────────

const lower = (ts: TS, checker: import("typescript").TypeChecker, type: import("typescript").Type, where: string, stack: Set<import("typescript").Type>): Schema => {
  const f = type.flags
  const T = ts.TypeFlags
  if (f & (T.Any | T.Unknown)) return { t: "any" }
  if (f & T.Never) throw new Error(`${where}: never`)
  if (f & T.Null) return { t: "null" }
  if (f & (T.Undefined | T.Void)) return { t: "undefined" }
  if (f & T.BooleanLiteral) return { t: "lit", v: (type as any).intrinsicName === "true" }
  if (f & T.Boolean) return { t: "boolean" }
  if (f & T.StringLiteral) return { t: "lit", v: (type as import("typescript").StringLiteralType).value }
  if (f & T.NumberLiteral) return { t: "lit", v: (type as import("typescript").NumberLiteralType).value }
  if (f & T.String) return { t: "string" }
  if (f & T.Number) return { t: "number" }
  if (f & (T.BigInt | T.BigIntLiteral | T.ESSymbol | T.UniqueESSymbol)) throw new Error(`${where}: ${checker.typeToString(type)} is not JSON`)
  if (f & T.EnumLiteral && f & T.Union) {
    return { t: "union", of: (type as import("typescript").UnionType).types.map(t => lower(ts, checker, t, where, stack)) }
  }
  if (f & T.Union) return lowerMembers(ts, checker, (type as import("typescript").UnionType).types, where, stack)
  if (f & T.Intersection || f & T.Object) {
    if (stack.has(type)) throw new Error(`${where}: recursive type ${checker.typeToString(type)}`)
    if (type.getCallSignatures().length || type.getConstructSignatures().length) throw new Error(`${where}: functions are not JSON`)
    stack.add(type)
    try {
      if (checker.isArrayType(type)) {
        const [el] = checker.getTypeArguments(type as import("typescript").TypeReference)
        return { t: "array", of: el ? lower(ts, checker, el, where, stack) : { t: "any" } }
      }
      if (checker.isTupleType(type)) {
        const ref = type as import("typescript").TypeReference
        const target = ref.target as import("typescript").TupleType
        const args = checker.getTypeArguments(ref)
        const items: Schema[] = []
        let rest: Schema | undefined
        target.elementFlags.forEach((flag, i) => {
          const s = lower(ts, checker, args[i], where, stack)
          if (flag & ts.ElementFlags.Rest || flag & ts.ElementFlags.Variadic) rest = (s.t === "array" ? s.of : s)
          else items.push(flag & ts.ElementFlags.Optional ? { t: "union", of: [s, { t: "undefined" }] } : s)
        })
        return { t: "tuple", items, rest }
      }
      const name = checker.typeToString(type)
      if (/^(Date|Map<|Set<|WeakMap<|WeakSet<|Promise<|RegExp$|ArrayBuffer$|Uint8Array$)/.test(name)) throw new Error(`${where}: ${name} is not JSON`)
      const props: Record<string, Schema> = {}
      const optional: string[] = []
      for (const p of checker.getPropertiesOfType(type)) {
        const pt = checker.getTypeOfSymbolAtLocation(p, p.valueDeclaration ?? p.declarations?.[0]!)
        if (pt.getCallSignatures().length) throw new Error(`${where}.${p.name}: functions are not JSON`)
        const isOpt = !!(p.flags & ts.SymbolFlags.Optional)
        // an optional property's type carries `undefined` — strip it, the optional list covers it
        const members = pt.flags & T.Union ? (pt as import("typescript").UnionType).types : [pt]
        props[p.name] = lowerMembers(ts, checker, isOpt ? members.filter(t => !(t.flags & T.Undefined)) : members, `${where}.${p.name}`, stack)
        if (isOpt) optional.push(p.name)
      }
      let index: Schema | undefined
      for (const info of checker.getIndexInfosOfType(type)) {
        if (info.keyType.flags & T.String) index = lower(ts, checker, info.type, `${where}[string]`, stack)
      }
      return { t: "object", props, ...(optional.length ? { optional } : {}), ...(index ? { index } : {}) }
    } finally {
      stack.delete(type)
    }
  }
  if (f & T.TypeParameter) throw new Error(`${where}: generic parameter ${checker.typeToString(type)} can't be validated`)
  throw new Error(`${where}: unsupported type ${checker.typeToString(type)}`)
}

/** Lower a list of union members: `boolean` arrives as `true | false` — fold it back; a single member is itself. */
const lowerMembers = (ts: TS, checker: import("typescript").TypeChecker, members: readonly import("typescript").Type[], where: string, stack: Set<import("typescript").Type>): Schema => {
  const T = ts.TypeFlags
  if (members.length === 1) return lower(ts, checker, members[0], where, stack)
  const isLit = (m: import("typescript").Type, v: string) => !!(m.flags & T.BooleanLiteral) && (m as any).intrinsicName === v
  const foldBool = members.some(m => isLit(m, "true")) && members.some(m => isLit(m, "false"))
  const of: Schema[] = foldBool ? [{ t: "boolean" }] : []
  for (const m of members) {
    if (foldBool && m.flags & T.BooleanLiteral) continue
    of.push(lower(ts, checker, m, where, stack))
  }
  return of.length === 1 ? of[0] : { t: "union", of }
}
