// biome-ignore-all lint/suspicious/noConsole: Logging allowed here
import { execFileSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { bold, cyan } from "picocolors";
import { copy } from "../helpers/copy";
import { install } from "../helpers/install";
import type { GetTemplateFileArgs, InstallTemplateArgs } from "./types";

// When bundled by ncc, __dirname points to dist/, but templates are at dist/templates/
const PACKAGE_ROOT = __dirname.endsWith("dist")
  ? path.dirname(__dirname)
  : path.dirname(__dirname);
const TEMPLATES_DIR = __dirname.endsWith("dist")
  ? path.join(__dirname, "templates")
  : __dirname;
const WORKSPACE_ROOT = path.resolve(PACKAGE_ROOT, "../..");
const WINDOWS_SHELL = process.env.ComSpec ?? "cmd.exe";

const formatAppTitle = (appName: string) =>
  appName
    .split(/[-_\s]+/)
    .filter(Boolean)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join(" ");

const resolveCoreApitogoPackageName = async (): Promise<string> => {
  const createPackageJson = JSON.parse(
    await fs.readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8"),
  ) as { name: string };
  return createPackageJson.name.replace("/create-apitogo", "/apitogo");
};

/** Primary npm scope; split so secondary publish does not rewrite this guard. */
const PRIMARY_APITOGO_SCOPE = "@lukow" + "eb";

const normalizeDependencyVersion = (version: string): string =>
  version.replace(/^[\^~>=<]+/, "");

const LUCIDE_REACT_VERSION = "0.577.0";

const ensureRequiredApitogoModules = (
  dependencies: Record<string, string>,
  corePackageName: string,
  version: string,
) => {
  const scope = corePackageName.slice(0, corePackageName.indexOf("/"));
  const landing = `${scope}/apitogo-module-landing`;
  const userPanel = `${scope}/apitogo-module-user-panel`;
  const billing = `${scope}/apitogo-plugin-dev-portal-billing`;
  const moduleVersion = `^${normalizeDependencyVersion(version)}`;

  dependencies[landing] ??= moduleVersion;
  dependencies[userPanel] ??= moduleVersion;
  dependencies[billing] ??= moduleVersion;
  dependencies["lucide-react"] ??= LUCIDE_REACT_VERSION;
};

const ensureSecondaryApitogoAliases = (
  dependencies: Record<string, string>,
  corePackageName: string,
  version: string,
) => {
  if (corePackageName.startsWith(`${PRIMARY_APITOGO_SCOPE}/`)) {
    return;
  }

  const alias = (target: string) =>
    `npm:${target}@${normalizeDependencyVersion(version)}`;
  const scope = corePackageName.slice(0, corePackageName.indexOf("/"));

  dependencies[`${PRIMARY_APITOGO_SCOPE}/apitogo`] = alias(corePackageName);
  dependencies[`${PRIMARY_APITOGO_SCOPE}/apitogo-module-landing`] = alias(
    `${scope}/apitogo-module-landing`,
  );
  dependencies[`${PRIMARY_APITOGO_SCOPE}/apitogo-module-user-panel`] = alias(
    `${scope}/apitogo-module-user-panel`,
  );
  dependencies[`${PRIMARY_APITOGO_SCOPE}/apitogo-plugin-dev-portal-billing`] =
    alias(`${scope}/apitogo-plugin-dev-portal-billing`);
};

const parseWorkspaceCatalog = async (workspaceFile: string) => {
  const content = await fs.readFile(workspaceFile, "utf8");
  const catalog: Record<string, string> = {};
  let inCatalog = false;

  for (const line of content.split(/\r?\n/)) {
    if (!inCatalog) {
      inCatalog = line.trim() === "catalog:";
      continue;
    }

    if (!line.startsWith("  ")) break;

    const match = line.match(/^ {2}(?:"([^"]+)"|([^:]+)):\s+(.+)$/);
    if (!match) continue;

    const dependency = match[1] ?? match[2]?.trim();
    const version = match[3]?.trim();
    if (dependency && version) catalog[dependency] = version;
  }

  return catalog;
};

const replaceCatalogVersions = (
  value: unknown,
  catalog: Record<string, string>,
): unknown => {
  if (Array.isArray(value)) {
    return value.map((entry) => replaceCatalogVersions(entry, catalog));
  }

  if (!value || typeof value !== "object") {
    return value;
  }

  return Object.fromEntries(
    Object.entries(value).map(([key, entry]) => [
      key,
      entry === "catalog:"
        ? (catalog[key] ?? entry)
        : replaceCatalogVersions(entry, catalog),
    ]),
  );
};

const stageLocalZudokuPackage = async (
  sourceDir: string,
  stagingDir: string,
) => {
  const workspaceCatalog = await parseWorkspaceCatalog(
    path.join(WORKSPACE_ROOT, "pnpm-workspace.yaml"),
  );
  const packageJsonPath = path.join(sourceDir, "package.json");
  const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
  const stagedPackageJson = replaceCatalogVersions(
    packageJson,
    workspaceCatalog,
  ) as typeof packageJson;
  const packageFiles = new Set<string>([
    "package.json",
    ...(Array.isArray(packageJson.files) ? packageJson.files : []),
  ]);

  await fs.rm(stagingDir, { recursive: true, force: true });
  await fs.mkdir(stagingDir, { recursive: true });

  for (const entry of packageFiles) {
    if (entry === "package.json") continue;

    const sourcePath = path.join(sourceDir, entry);
    const destinationPath = path.join(stagingDir, entry);
    const stat = await fs.stat(sourcePath).catch(() => null);

    if (!stat) continue;

    if (stat.isDirectory()) {
      await fs.cp(sourcePath, destinationPath, { recursive: true });
    } else {
      await fs.mkdir(path.dirname(destinationPath), { recursive: true });
      await fs.copyFile(sourcePath, destinationPath);
    }
  }

  delete stagedPackageJson.scripts?.prepublishOnly;
  delete stagedPackageJson.scripts?.postpublish;
  if (
    stagedPackageJson.scripts &&
    Object.keys(stagedPackageJson.scripts).length === 0
  ) {
    delete stagedPackageJson.scripts;
  }

  await fs.writeFile(
    path.join(stagingDir, "package.json"),
    JSON.stringify(stagedPackageJson, null, 2) + os.EOL,
  );
};

/**
 * Get the file path for a given file in a template, e.g. "next.config.js".
 */
export const getTemplateFile = ({
  template,
  mode,
  file,
}: GetTemplateFileArgs): string => {
  return path.join(TEMPLATES_DIR, template, mode, file);
};

/**
 * Install a Zudoku internal template to a given `root` directory.
 */
export const installTemplate = async ({
  appName,
  root,
  packageManager,
  isOnline,
  template,
  mode,
  eslint,
  skipInstall,
  zudokuVersion,
}: InstallTemplateArgs) => {
  console.log(bold(`Using ${packageManager}.`));

  /**
   * Copy the template files to the target directory.
   */
  console.log("\nInitializing project with template:", template, "\n");
  const templatePath = path.join(TEMPLATES_DIR, template, mode);
  const copySource = ["**"];
  if (!eslint) copySource.push("!eslintrc.json");

  await copy(copySource, root, {
    parents: true,
    cwd: templatePath,
    rename(name) {
      switch (name) {
        case "gitignore":
        case "eslintrc.json": {
          return `.${name}`;
        }
        // README.md is ignored by webpack-asset-relocator-loader used by ncc:
        // https://github.com/vercel/webpack-asset-relocator-loader/blob/e9308683d47ff507253e37c9bcbb99474603192b/src/asset-relocator.js#L227
        case "README-template.md": {
          return "README.md";
        }
        case "zudoku.config.tsx": {
          return "apitogo.config.tsx";
        }
        case "zudoku.config.jsx": {
          return "apitogo.config.jsx";
        }
        default: {
          return name;
        }
      }
    },
  });

  const appTitle = formatAppTitle(appName);
  const substituteAppTitle = (contents: string) =>
    contents.split("__APP_TITLE__").join(appTitle);

  const configFilename =
    mode === "ts" ? "apitogo.config.tsx" : "apitogo.config.jsx";
  const configPath = path.join(root, configFilename);
  const configContents = await fs.readFile(configPath, "utf8");
  await fs.writeFile(configPath, substituteAppTitle(configContents), "utf8");

  const manifestPath = path.join(root, "apitogo.local.json");
  try {
    const manifestContents = await fs.readFile(manifestPath, "utf8");
    await fs.writeFile(
      manifestPath,
      substituteAppTitle(manifestContents),
      "utf8",
    );
  } catch {
    // Optional in customized templates.
  }

  // update import alias in any files if not using the default

  /** Copy the version from package.json or override for tests. */
  const version = process.env.ZUDOKU_PRIVATE_TEST_VERSION ?? zudokuVersion;
  const localZudokuPackageDir = path.join(WORKSPACE_ROOT, "packages", "zudoku");
  const localZudokuPackageJson = path.join(
    localZudokuPackageDir,
    "package.json",
  );
  const resolvedZudokuDependency = await fs
    .access(localZudokuPackageJson)
    .then(async () => {
      const localPackageDir = path.join(root, ".apitogo-local");
      const stagedPackageDir = path.join(localPackageDir, "zudoku-package");
      await fs.mkdir(localPackageDir, { recursive: true });
      await stageLocalZudokuPackage(localZudokuPackageDir, stagedPackageDir);

      const packDestinationArg = path.relative(
        stagedPackageDir,
        localPackageDir,
      );
      const packOutput = (
        process.platform === "win32"
          ? execFileSync(
              WINDOWS_SHELL,
              [
                "/d",
                "/c",
                "npm",
                "pack",
                "--pack-destination",
                packDestinationArg,
              ],
              { cwd: stagedPackageDir, encoding: "utf8" },
            )
          : execFileSync(
              "npm",
              ["pack", "--pack-destination", packDestinationArg],
              {
                cwd: stagedPackageDir,
                encoding: "utf8",
              },
            )
      )
        .trim()
        .split(/\r?\n/)
        .at(-1);

      if (!packOutput) {
        throw new Error("Failed to pack local zudoku package.");
      }

      return `file:.apitogo-local/${packOutput}`;
    })
    .catch(() => version);

  const coreApitogoPackageName = await resolveCoreApitogoPackageName();

  /** Create a package.json for the new project and write it to disk. */
  // biome-ignore lint/suspicious/noExplicitAny: Allow any type
  const packageJson: any = {
    name: appName,
    version: "0.1.0",
    type: "module",
    private: true,
    scripts: {
      dev: "apitogo dev",
      build: "apitogo build",
      preview: "apitogo preview",
      deploy: "apitogo deploy",
      lint: "eslint",
    },
    /**
     * Default dependencies.
     */
    dependencies: {
      react: ">=19.0.0",
      "react-dom": ">=19.0.0",
      [coreApitogoPackageName]: resolvedZudokuDependency,
    },
    devDependencies: {},
  };

  if (!resolvedZudokuDependency.startsWith("file:")) {
    ensureRequiredApitogoModules(
      packageJson.dependencies,
      coreApitogoPackageName,
      resolvedZudokuDependency,
    );
    ensureSecondaryApitogoAliases(
      packageJson.dependencies,
      coreApitogoPackageName,
      resolvedZudokuDependency,
    );
  }

  /**
   * TypeScript projects will have type definitions and other devDependencies.
   */
  if (mode === "ts") {
    packageJson.devDependencies = {
      ...packageJson.devDependencies,
      "@types/node": "^22",
      "@types/react": "^19",
      "@types/react-dom": "^19",
      typescript: "^5",
    };
  }

  /* Default ESLint dependencies. */
  if (eslint) {
    packageJson.devDependencies = {
      ...packageJson.devDependencies,
      eslint: "^8",
    };
  }

  // Typescript eslint dependencies
  if (eslint && mode === "ts") {
    packageJson.devDependencies = {
      ...packageJson.devDependencies,
      "@typescript-eslint/eslint-plugin": "^8",
      "@typescript-eslint/parser": "^8",
    };
  }

  const devDeps = Object.keys(packageJson.devDependencies).length;
  if (!devDeps) delete packageJson.devDependencies;

  await fs.writeFile(
    path.join(root, "package.json"),
    JSON.stringify(packageJson, null, 2) + os.EOL,
  );

  if (skipInstall) return;

  console.log("\nInstalling dependencies:");
  for (const dependency in packageJson.dependencies)
    console.log(`- ${cyan(dependency)}`);

  if (devDeps) {
    console.log("\nInstalling devDependencies:");
    for (const dependency in packageJson.devDependencies)
      console.log(`- ${cyan(dependency)}`);
  }

  console.log();

  await install(packageManager, isOnline);
};

export * from "./types";
