import { readFileSync, writeFileSync } from "fs";
import { resolve, dirname, relative } from "path";
import ts from "typescript";

const ENTRY = resolve("src/index.ts");
const OUT = resolve("dist/index.d.ts");

const configFile = ts.findConfigFile("./", ts.sys.fileExists, "tsconfig.json");
const configHost: ts.ParseConfigFileHost = {
  ...ts.sys,
  onUnRecoverableConfigFileDiagnostic: (d) => {
    throw new Error(ts.flattenDiagnosticMessageText(d.messageText, "\n"));
  },
};

const parsed = ts.getParsedCommandLineOfConfigFile(
  configFile!,
  { emitDeclarationOnly: true, declaration: true, noEmit: false },
  configHost
)!;

const program = ts.createProgram([ENTRY], parsed.options);
const checker = program.getTypeChecker();

const emitted = new Set<string>(); // dedupe across files
const lines: string[] = ["// Auto-generated by build-dts.ts", ""];

function relativePath(fullfilename: string): string {
  return relative(process.cwd(), fullfilename);
}

function fileText(sf: ts.SourceFile, node: ts.Node): string {
  return sf.text.slice(node.getFullStart(), node.getEnd()).trim();
}

function tryResolveType(sf: ts.SourceFile, node: ts.TypeAliasDeclaration): string | null {
  try {
    const type = checker.getTypeAtLocation(node.name);
    const resolved = checker.typeToString(
      type,
      undefined,
      ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseFullyQualifiedType
    );
    if (resolved === node.name.text) return null;
    return `export type ${node.name.text} = ${resolved};`;
  } catch {
    return null;
  }
}

function visitFile(sf: ts.SourceFile) {
  function visit(node: ts.Node) {
    const isExported = (n: ts.Node) =>
      (n as any).modifiers?.some((m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword);

    // export * from "./x" or export type * from "./x" — follow and inline
    if (ts.isExportDeclaration(node)) {
      const modSpec = node.moduleSpecifier;
      if (modSpec && ts.isStringLiteral(modSpec)) {
        const resolved = ts.resolveModuleName(
          modSpec.text,
          sf.fileName,
          parsed.options,
          ts.sys
        ).resolvedModule;

        if (resolved && !resolved.isExternalLibraryImport) {
          const targetSf = program.getSourceFile(resolved.resolvedFileName);
          if (targetSf) {
            visitFile(targetSf); // recurse into the re-exported file
            return;
          }
        }
      }
      // external or unresolved — copy verbatim
      const text = fileText(sf, node);
      if (!emitted.has(text)) {
        emitted.add(text);
        lines.push(`// ${relativePath(sf.fileName)}\n`);
        lines.push(text);
      }
      return;
    }

    // export type Foo = ...
    if (ts.isTypeAliasDeclaration(node) && isExported(node)) {
      const resolved = tryResolveType(sf, node);
      const text = resolved ?? fileText(sf, node);
      if (!emitted.has(node.name.text)) {
        emitted.add(node.name.text);
        lines.push(`// ${relativePath(sf.fileName)}\n`);
        lines.push(text);
      }
      return;
    }

    // export interface, enum, const enum
    if (
      (ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node)) &&
      isExported(node)
    ) {
      const text = fileText(sf, node);
      if (!emitted.has((node as any).name.text)) {
        emitted.add((node as any).name.text);
        lines.push(`// ${relativePath(sf.fileName)}\n`);
        lines.push(text);
      }
      return;
    }

    // export class
    if (ts.isClassDeclaration(node) && isExported(node) && node.name) {
      const text = fileText(sf, node);
      if (!emitted.has(node.name.text)) {
        emitted.add(node.name.text);
        lines.push(`// ${relativePath(sf.fileName)}\n`);
        lines.push(text);
      }
      return;
    }

    // export function / export const
    if (
      (ts.isFunctionDeclaration(node) || ts.isVariableStatement(node)) &&
      isExported(node)
    ) {
      try {
        if (ts.isFunctionDeclaration(node) && node.name) {
          const sym = checker.getSymbolAtLocation(node.name);
          if (sym && !emitted.has(sym.name)) {
            emitted.add(sym.name);
            const type = checker.getTypeOfSymbolAtLocation(sym, node);
            for (const sig of type.getCallSignatures()) {
              lines.push(`export declare function ${sym.name}${checker.signatureToString(sig)};`);
            }
            return;
          }
        }
      } catch { }
      const text = fileText(sf, node);
      if (!emitted.has(text)) { emitted.add(text); lines.push(text); }
      return;
    }

    ts.forEachChild(node, visit);
  }

  ts.forEachChild(sf, visit);
}

const entryFile = program.getSourceFile(ENTRY)!;
visitFile(entryFile);
lines.push("");

writeFileSync(OUT, lines.join("\n"));
// console.log(`✓ wrote ${OUT}`);