// sniffs the types and params of a file's exports, including const values

import ts from "typescript"

export type Signature = {
  type: string,
  returnType: string | null,
  params: Param[]
}

export type Param = {
  name: string,
  type: string,
  optional: boolean,
  rest: boolean,
  default: string | null
}

export class SniffError extends Error {
  constructor(message: string) {
    super(message)
    this.name = "TypeError"
    Object.setPrototypeOf(this, SniffError.prototype)
  }
}

export type ExportInfo =
  | { kind: "function", name: string, signatures: Signature[] }
  | { kind: "value", name: string, type: string, value: string | null }

let prevProgram: ts.Program | undefined

export async function moduleExports(file: string): Promise<Record<string, ExportInfo>> {
  const program = ts.createProgram([file], {
    target: ts.ScriptTarget.ESNext,
    module: ts.ModuleKind.ESNext,
    moduleResolution: ts.ModuleResolutionKind.NodeNext,
    noLib: true,
    types: [],
    skipDefaultLibCheck: true,
    skipLibCheck: true
  }, undefined, prevProgram)
  prevProgram = program

  const checker = program.getTypeChecker()
  const sf = program.getSourceFile(file)
  if (!sf) throw new SniffError(`File not found: ${file}`)

  const moduleSym = (sf as any).symbol as ts.Symbol | undefined
  if (!moduleSym) return {}

  const exportSymbols = checker.getExportsOfModule(moduleSym)
  const result: ExportInfo[] = []

  for (const sym of exportSymbols) {
    const decl = sym.valueDeclaration ?? sym.declarations?.[0] ?? sf
    const type = checker.getTypeOfSymbolAtLocation(sym, decl)
    const sigs = checker.getSignaturesOfType(type, ts.SignatureKind.Call)

    if (sigs.length > 0) {
      result.push({
        kind: "function",
        name: sym.getName(),
        signatures: sigs.map(sig => ({
          type: checker.typeToString(type, decl),
          returnType: checker.typeToString(checker.getReturnTypeOfSignature(sig), decl),
          params: sig.getParameters().map(p => {
            const pd: any = p.getDeclarations()?.[0] ?? decl
            const pt = checker.getTypeOfSymbolAtLocation(p, pd)
            return {
              name: p.getName(),
              type: checker.typeToString(pt, pd),
              optional: !!pd.questionToken || !!pd.initializer,
              rest: !!pd.dotDotDotToken,
              default: pd.initializer ? pd.initializer.getText() : null
            }
          })
        }))
      })
    } else {
      let value: string | null = null
      if (ts.isVariableDeclaration(decl) && decl.initializer) {
        value = decl.initializer.getText()
      }
      result.push({
        kind: "value",
        name: sym.getName(),
        type: checker.typeToString(type, decl),
        value
      })
    }
  }

  return Object.fromEntries(
    result.map(item => [item.name, item])
  )
}

if (import.meta.main) {
  const path = Bun.argv[2]
  if (!path) {
    console.error("usage: sniff <path>")
    process.exit(1)
  }
  console.log(await moduleExports(path))
}