import {
  existsSync,
  readFileSync,
  writeFileSync,
  readdirSync,
  statSync,
} from "node:fs";
import { join, relative } from "node:path";
import type { PlopTypes } from "@turbo/gen";

/**
 * Throws if a core package directory already exists. Used as the first action
 * in every per-package generator so re-running is safe.
 */
export function assertOptionalPackageNotPresent(
  name: string,
  cwd: string = process.cwd(),
): void {
  const pkgRoot = join(cwd, "packages", name);
  if (existsSync(pkgRoot)) {
    throw new Error(
      `packages/${name}/ already exists — refusing to scaffold (delete it first if intentional)`,
    );
  }
}

/**
 * Inserts a package name into the transpilePackages array of a Next.js config
 * file, preserving alphabetical order. Idempotent.
 */
export function addToTranspilePackages(
  nextConfigPath: string,
  pkgName: string,
): void {
  const source = readFileSync(nextConfigPath, "utf8");
  if (source.includes(`"${pkgName}"`)) return;
  const updated = source.replace(
    /(transpilePackages:\s*\[\s*)([\s\S]*?)(\s*\])/,
    (_match, open: string, body: string, close: string) => {
      const entries = body
        .split(",")
        .map((e) => e.trim())
        .filter(Boolean);
      entries.push(`"${pkgName}"`);
      entries.sort();
      const formatted = entries.map((e) => `    ${e}`).join(",\n");
      return `${open}\n${formatted},${close}`;
    },
  );
  writeFileSync(nextConfigPath, updated);
}

/**
 * Inserts a code block immediately after a `// <gen:NAME>` anchor in a file.
 * Idempotent: refuses to insert if the exact block already follows the anchor.
 */
export function splicePluginRulesAt(
  filePath: string,
  anchorName: string,
  block: string,
): void {
  const source = readFileSync(filePath, "utf8");
  const anchor = `// <gen:${anchorName}>`;
  const idx = source.indexOf(anchor);
  if (idx === -1) {
    throw new Error(`Anchor ${anchor} not found in ${filePath}`);
  }
  const after = source.slice(idx + anchor.length);
  if (after.trimStart().startsWith(block.trim())) return; // idempotent
  const updated = source.slice(0, idx + anchor.length) + "\n" + block + after;
  writeFileSync(filePath, updated);
}

/**
 * Inserts an import line immediately after the matching anchor. Idempotent.
 */
export function splicePluginImportsAt(
  filePath: string,
  anchorName: string,
  importLine: string,
): void {
  splicePluginRulesAt(filePath, anchorName, importLine);
}

/**
 * Inserts a `{ type, pattern, ...opts }` entry into the boundaries/elements
 * array of `core-eslint/base.js`, placed immediately BEFORE the
 * `packages/core-*` wildcard so its more-specific match wins. Idempotent.
 */
export function addBoundariesEntry(
  baseJsPath: string,
  packagePath: string,
  opts: { mode?: "folder" } = {},
): void {
  const source = readFileSync(baseJsPath, "utf8");
  if (source.includes(`pattern: "${packagePath}"`)) return; // idempotent
  const wildcardLine = source.match(
    /(\s*\{\s*type:\s*"core",\s*pattern:\s*"packages\/core-\*"[^}]*\},)/,
  );
  if (!wildcardLine) {
    throw new Error(`packages/core-* wildcard not found in ${baseJsPath}`);
  }
  const modeFragment = opts.mode ? `, mode: "${opts.mode}"` : "";
  const newEntry = `        { type: "core", pattern: "${packagePath}"${modeFragment} },\n`;
  const updated = source.replace(
    wildcardLine[0],
    `\n${newEntry}${wildcardLine[1]}`,
  );
  writeFileSync(baseJsPath, updated);
}

/**
 * Walks turbo/generators/templates/<srcPrefix>/ recursively. For each .hbs
 * file, returns a plop `add` action that emits the file (without .hbs
 * extension) at <destPrefix>/<relative-path>. The actions are sorted so
 * directory creation is deterministic.
 *
 * Set `opts.force` to overwrite existing files (idempotent re-runs).
 */
export function emitTemplateTree(
  srcPrefix: string,
  destPrefix: string,
  opts: { templatesRoot?: string; force?: boolean } = {},
): PlopTypes.AddActionConfig[] {
  // The templates directory is resolved in priority order:
  // 1. opts.templatesRoot — test injection (temp directory)
  // 2. cwd/turbo/generators/templates — turbo gen context (cwd = repo root)
  // 3. cwd/templates — vitest context (cwd = turbo/generators)
  let root: string;
  if (opts.templatesRoot) {
    root = opts.templatesRoot;
  } else {
    const fromRepoRoot = join(
      process.cwd(),
      "turbo",
      "generators",
      "templates",
    );
    const fromGeneratorsDir = join(process.cwd(), "templates");
    root = existsSync(fromRepoRoot) ? fromRepoRoot : fromGeneratorsDir;
  }
  const srcRoot = join(root, srcPrefix);
  const out: PlopTypes.AddActionConfig[] = [];
  walkHbs(srcRoot, srcRoot, srcPrefix, destPrefix, out, opts.force ?? false);
  out.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
  return out;
}

function walkHbs(
  topRoot: string,
  dir: string,
  srcPrefix: string,
  destPrefix: string,
  out: PlopTypes.AddActionConfig[],
  force: boolean,
): void {
  for (const name of readdirSync(dir)) {
    const full = join(dir, name);
    if (statSync(full).isDirectory()) {
      walkHbs(topRoot, full, srcPrefix, destPrefix, out, force);
      continue;
    }
    if (!name.endsWith(".hbs")) continue;
    const rel = relative(topRoot, full).replace(/\.hbs$/, "");
    out.push({
      type: "add",
      path: join(destPrefix, rel).replace(/\\/g, "/"),
      templateFile: join(
        "templates",
        srcPrefix,
        relative(topRoot, full),
      ).replace(/\\/g, "/"),
      force,
    } as PlopTypes.AddActionConfig);
  }
}
