import { pathToFileURL } from "url";

import { Creator } from "../../typings/Creator";
import { SourceFinder } from "../../utils/SourceFinder";

export const load_controllers = async (
  path: string | string[] | { include: string[]; exclude?: string[] },
  isTsNode?: boolean,
): Promise<Creator<object>[]> => {
  const include: string[] = Array.isArray(path)
    ? path
    : typeof path === "object"
      ? path.include
      : [path];
  const exclude: string[] =
    typeof path === "object" && !Array.isArray(path)
      ? (path.exclude ?? [])
      : [];
  const filter = isTsNode === true ? isTypeScriptSource : isJavaScriptModule;
  const sources: string[] = await SourceFinder.find({
    include,
    exclude,
    filter,
  });
  const controllers: Creator<object>[] = await mount(sources);
  if (controllers.length !== 0 || isTsNode === true) return controllers;

  // No compiled `.js` controllers were found. Under `ttsx`, the project runs
  // straight from its TypeScript sources: `__dirname` still points at the
  // source tree (the runtime hooks serve the emitted JS under the source
  // URLs), so the controllers on disk are `.ts`, not `.js`. Detect that
  // source-run context and retry with the TypeScript filter; `import()` of each
  // `.ts` file is then served as the transformed emit by the hooks.
  if (!isTsxRuntime()) return controllers;

  const fallback: string[] = await SourceFinder.find({
    include,
    exclude,
    filter: isTypeScriptSource,
  });
  return fallback.length === 0 ? controllers : mount(fallback);
};

/** @internal */
async function mount(sources: string[]): Promise<any[]> {
  const controllers: any[] = [];
  for (const file of sources) {
    const external: any = await dynamicImport(pathToFileURL(file).href);
    for (const key in external) {
      const instance: Creator<object> = external[key];
      if (
        instance === null ||
        (typeof instance !== "function" && typeof instance !== "object")
      )
        continue;
      if (Reflect.getMetadata("path", instance) !== undefined)
        controllers.push(instance);
    }
  }
  return controllers;
}

const dynamicImport: (specifier: string) => Promise<any> = Function(
  "specifier",
  "return import(specifier);",
) as (specifier: string) => Promise<any>;

/**
 * Whether the process is running from TypeScript source under `ttsx`.
 *
 * `ttsx` runs a TypeScript entry from source: it builds the owning project to a
 * temporary directory and installs runtime module hooks that serve that emit
 * under the original source URLs, exporting the manifest path through
 * `TTSX_RUNTIME_MANIFEST`. Its presence is the reliable signal that the
 * controllers on disk are `.ts` (the `.js` glob will be empty) yet `import()`
 * of those `.ts` files resolves to transformed JavaScript.
 */
function isTsxRuntime(): boolean {
  return (
    typeof process.env.TTSX_RUNTIME_MANIFEST === "string" &&
    process.env.TTSX_RUNTIME_MANIFEST.length !== 0
  );
}

const isJavaScriptModule = (file: string): boolean =>
  /\.(?:[cm]?js)$/.test(file.toLowerCase());

const isTypeScriptSource = (file: string): boolean => {
  const lower: string = file.toLowerCase();
  return (
    /\.(?:[cm]?ts)$/.test(lower) &&
    /\.(?:d\.[cm]?ts|d\.ts)$/.test(lower) === false
  );
};
