import { AstroBuild } from "./astro-build.ts";
import type { BuildSettings } from "./build-settings.ts";
import type { BuildStrategy } from "./build-strategy.ts";
import { BunBuild } from "./bun-build.ts";
import { FRAMEWORKS, type FrameworkBuildType } from "./config/frameworks.ts";
import { CustomBuild } from "./custom-build.ts";
import { NestjsBuild } from "./nestjs-build.ts";
import { NextjsBuild } from "./nextjs-build.ts";
import { NuxtBuild } from "./nuxt-build.ts";
import { TanstackStartBuild } from "./tanstack-start-build.ts";
import type { BuildCommandIo } from "./workspace.ts";

/** A concrete framework build type, or `"auto"` to detect it. */
export type BuildType = FrameworkBuildType | "auto";

/**
 * Consumer hooks shared across `createBuildStrategy` / `resolveBuildStrategy`.
 * All optional: the CLI passes none, while a headless consumer injects build
 * `env`, taps output, and requires standalone Next output.
 */
export interface BuildStrategyHooks {
  /** Build `env` and per-line output tap, forwarded to the build command. */
  io?: BuildCommandIo;
  /** Fail instead of falling back to the full-tree Next `next start` artifact. */
  requireStandalone?: boolean;
}

/**
 * Detection order, derived from the framework registry so a new framework is
 * picked up automatically. `bun` is forced last — it is the universal fallback
 * (its `canBuild` always returns true).
 */
const DETECTION_ORDER: FrameworkBuildType[] = (() => {
  const buildTypes = [
    ...new Set(FRAMEWORKS.map((framework) => framework.buildType)),
  ];
  return [
    ...buildTypes.filter((type) => type !== "bun" && type !== "custom"),
    "bun",
  ];
})();

/**
 * Constructs the build strategy for an explicit framework build type, wiring
 * through optional committed build settings and entrypoints for strategies
 * that need one.
 */
export function createBuildStrategy(
  options: {
    appPath: string;
    buildType: FrameworkBuildType;
    entrypoint?: string;
    buildSettings?: BuildSettings;
  } & BuildStrategyHooks,
): BuildStrategy {
  const { appPath, buildSettings, io } = options;
  switch (options.buildType) {
    case "nextjs":
      return new NextjsBuild({
        appPath,
        buildSettings,
        io,
        requireStandalone: options.requireStandalone,
      });
    case "nuxt":
      return new NuxtBuild({ appPath, buildSettings, io });
    case "astro":
      return new AstroBuild({ appPath, buildSettings, io });
    case "nestjs":
      return new NestjsBuild({ appPath, buildSettings, io });
    case "tanstack-start":
      return new TanstackStartBuild({ appPath, buildSettings, io });
    case "custom":
      return new CustomBuild({
        appPath,
        entrypoint: options.entrypoint,
        buildSettings,
        io,
      });
    case "bun":
      return new BunBuild({
        appPath,
        entrypoint: options.entrypoint,
        buildSettings,
        io,
      });
  }
}

/**
 * Resolves the build strategy and the concrete build type for an app. With an
 * explicit `buildType` the matching strategy is returned directly; with
 * `"auto"` (or omitted) each framework is tried in detection order and the
 * first match wins, with bun as the fallback. Returning the resolved build
 * type lets a consumer report what it detected.
 */
export async function resolveBuildStrategy(
  options: {
    appPath: string;
    buildType?: BuildType;
    entrypoint?: string;
    buildSettings?: BuildSettings;
    signal?: AbortSignal;
  } & BuildStrategyHooks,
): Promise<{ strategy: BuildStrategy; buildType: FrameworkBuildType }> {
  const shared = {
    appPath: options.appPath,
    entrypoint: options.entrypoint,
    buildSettings: options.buildSettings,
    io: options.io,
    requireStandalone: options.requireStandalone,
  };

  if (options.buildType && options.buildType !== "auto") {
    return {
      buildType: options.buildType,
      strategy: createBuildStrategy({
        ...shared,
        buildType: options.buildType,
      }),
    };
  }

  for (const buildType of DETECTION_ORDER) {
    if (buildType === "bun") {
      break;
    }
    const strategy = createBuildStrategy({ ...shared, buildType });
    options.signal?.throwIfAborted();
    if (await strategy.canBuild(options.signal)) {
      return { buildType, strategy };
    }
  }

  return {
    buildType: "bun",
    strategy: createBuildStrategy({ ...shared, buildType: "bun" }),
  };
}
