/**
 * Server-side lockfile probe that mirrors the client picker's detection.
 * Moved off the browser so SANDBOX_START can't race runtime resolution — the
 * caller used to fire SANDBOX_START before the client-side detection wrote
 * `runtime` back, baking a clone-only daemon whose dev server never
 * starts. Running here keeps it on the same request-lifetime as the sandbox
 * provision. On any failure we return null and the caller falls back to
 * clone-only (same behaviour as a repo with no lockfile).
 */

import type { Kysely } from "kysely";
import type { CredentialVault } from "../encryption/credential-vault";
import { getValidDownstreamAccessToken } from "../oauth/token-refresh";
import { DownstreamTokenStorage } from "../storage/downstream-token";
import type { Database } from "../storage/types";
import type { PackageManager } from "@decocms/shared/runtime-defaults";

/** Ordered: first match wins. Mirrors github-repo-picker.tsx's runtimeFiles. */
const LOCKFILES: Array<{ path: string; pm: PackageManager }> = [
  { path: "deno.json", pm: "deno" },
  { path: "deno.jsonc", pm: "deno" },
  { path: "bun.lock", pm: "bun" },
  { path: "bunfig.toml", pm: "bun" },
  { path: "pnpm-lock.yaml", pm: "pnpm" },
  { path: "yarn.lock", pm: "yarn" },
  { path: "package-lock.json", pm: "npm" },
  { path: "package.json", pm: "bun" },
];

const PORT_SOURCES: Partial<Record<PackageManager, string[]>> = {
  deno: ["deno.json", "deno.jsonc"],
  bun: ["package.json"],
  pnpm: ["package.json"],
  yarn: ["package.json"],
  npm: ["package.json"],
};

const PORT_RE = /(?:--port|PORT=|:)(\d{4,5})/;

export interface DetectedRuntime {
  packageManager: PackageManager;
  devPort: string | null;
}

export async function detectRepoRuntime(
  connectionId: string,
  owner: string,
  name: string,
  db: Kysely<Database>,
  vault: CredentialVault,
): Promise<DetectedRuntime | null> {
  const tokenResult = await getValidDownstreamAccessToken({
    connectionId,
    tokenStorage: new DownstreamTokenStorage(db, vault),
  });
  if (!tokenResult.accessToken) return null;
  return probeRuntime(owner, name, tokenResult.accessToken);
}

/**
 * Anonymous variant for public repos linked without a connection. Same probe,
 * no Authorization header. Hits GitHub's anon rate limit (60/h per IP) — fine
 * for one-shot detection inside SANDBOX_START, and on any failure (rate-limited,
 * network, parse) the caller falls back to clone-only just like the authed
 * path.
 */
export function detectRepoRuntimeAnonymous(
  owner: string,
  name: string,
): Promise<DetectedRuntime | null> {
  return probeRuntime(owner, name, null);
}

async function probeRuntime(
  owner: string,
  name: string,
  accessToken: string | null,
): Promise<DetectedRuntime | null> {
  // Detection runs against the repo's default branch, not the caller's sandbox
  // branch. The sandbox branch is typically a fresh name generated by SANDBOX_START
  // (e.g. `deco/keen-trail`) that hasn't been pushed yet — asking GitHub
  // for that ref 404s on every file and we'd fall back to clone-only even
  // on a valid repo. Omitting `ref` makes Contents API use HEAD.

  // Fetch only what we might need for detection + port extraction. Parallel
  // because GitHub serves these on separate CDN paths and sequential would
  // add ~1s per file inside SANDBOX_START's critical section.
  const cache = new Map<string, string | null>();
  const relevantPaths = new Set(LOCKFILES.map((f) => f.path));
  for (const sources of Object.values(PORT_SOURCES)) {
    for (const p of sources ?? []) relevantPaths.add(p);
  }
  await Promise.all(
    Array.from(relevantPaths).map(async (path) => {
      cache.set(path, await fetchGithubFile(owner, name, path, accessToken));
    }),
  );

  const hit = LOCKFILES.find(({ path }) => cache.get(path) !== null);
  if (!hit) return null;

  const portSources = PORT_SOURCES[hit.pm] ?? [];
  let devPort: string | null = null;
  for (const path of portSources) {
    devPort = extractDevPort(cache.get(path) ?? null);
    if (devPort) break;
  }
  return { packageManager: hit.pm, devPort };
}

/**
 * 404 → null (expected: most repos won't have every lockfile). Any other
 * failure also null — a half-detected runtime is worse than clone-only,
 * since the wrong PM would hang setup.
 */
async function fetchGithubFile(
  owner: string,
  repo: string,
  path: string,
  accessToken: string | null,
): Promise<string | null> {
  const url = `https://api.github.com/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`;
  try {
    const headers: Record<string, string> = {
      Accept: "application/vnd.github+json",
      "User-Agent": "studio-runtime-detect",
    };
    if (accessToken) headers.Authorization = `token ${accessToken}`;
    const res = await fetch(url, {
      headers,
      signal: AbortSignal.timeout(5_000),
    });
    if (!res.ok) return null;
    const body = (await res.json()) as {
      content?: string;
      encoding?: string;
    };
    if (!body.content || body.encoding !== "base64") return null;
    return Buffer.from(body.content, "base64").toString("utf8");
  } catch {
    return null;
  }
}

function extractDevPort(content: string | null): string | null {
  if (!content) return null;
  try {
    const parsed = JSON.parse(content) as {
      tasks?: Record<string, string>;
      scripts?: Record<string, string>;
    };
    const cmds = parsed.tasks ?? parsed.scripts ?? {};
    const cmd = cmds.dev ?? cmds.start ?? "";
    const match = cmd.match(PORT_RE);
    return match?.[1] ?? null;
  } catch {
    return null;
  }
}
