import { access, readFile } from "node:fs/promises";
import path from "node:path";

import {
  type ComputeAppManifest,
  computeAppCandidateFilePaths,
  type DetectedComputeApp,
  detectComputeApp,
} from "./detect-app.ts";
import { frameworkByKey } from "./frameworks.ts";

export interface DetectComputeAppFromDirectoryInput {
  appPath: string;
  signal?: AbortSignal;
}

export interface DetectedComputeDirectoryApp extends DetectedComputeApp {
  configFile: {
    path: string;
    standaloneOutput: boolean;
  } | null;
}

/** Detects one Compute app from a local directory. */
export async function detectComputeAppFromDirectory(
  input: DetectComputeAppFromDirectoryInput,
): Promise<DetectedComputeDirectoryApp | null> {
  const manifest = await readManifest(input.appPath, input.signal);
  const candidates = computeAppCandidateFilePaths(manifest);
  const existingCandidates = await Promise.all(
    candidates.map(async (candidate) =>
      (await fileExists(input.appPath, candidate, input.signal))
        ? candidate
        : null,
    ),
  );
  const filePaths = new Set(
    existingCandidates.filter((candidate) => candidate !== null),
  );

  const detected = detectComputeApp({ root: "", manifest, filePaths });
  if (!detected) return null;

  const descriptor = frameworkByKey(detected.framework);
  const configPath = descriptor.detectConfigFiles.find((candidate) =>
    filePaths.has(candidate),
  );
  if (!configPath) {
    return { ...detected, configFile: null };
  }

  let content: string;
  try {
    content = await readFile(path.join(input.appPath, configPath), {
      encoding: "utf8",
      signal: input.signal,
    });
  } catch (error) {
    if (input.signal?.aborted) throw error;
    if ((error as NodeJS.ErrnoException).code === "ENOENT") {
      return { ...detected, configFile: null };
    }
    throw error;
  }
  return {
    ...detected,
    configFile: {
      path: configPath,
      standaloneOutput:
        detected.framework === "nextjs" &&
        /\boutput\s*:\s*["'`]standalone["'`]/.test(content),
    },
  };
}

async function readManifest(
  appPath: string,
  signal?: AbortSignal,
): Promise<ComputeAppManifest> {
  signal?.throwIfAborted();
  try {
    const content = await readFile(path.join(appPath, "package.json"), {
      encoding: "utf8",
      signal,
    });
    const parsed = JSON.parse(content) as unknown;
    return parsed && typeof parsed === "object"
      ? (parsed as ComputeAppManifest)
      : {};
  } catch (error) {
    if (signal?.aborted) throw error;
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
    throw error;
  }
}

async function fileExists(
  appPath: string,
  relativePath: string,
  signal?: AbortSignal,
): Promise<boolean> {
  signal?.throwIfAborted();
  try {
    await access(path.join(appPath, relativePath));
    signal?.throwIfAborted();
    return true;
  } catch (error) {
    if (signal?.aborted) throw error;
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
    throw error;
  }
}
