import { spawn } from "node:child_process";
import { chmod, copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const ROOT_FACADE_PACKAGES = [
  "@martin/contracts",
  "@martin/core",
  "@martin/adapters",
];

const PACKAGE_FACADES = [
  {
    packageName: "@martin/contracts",
    sourceDir: ["packages", "contracts", "dist"],
    targetDir: ["dist", "vendor", "contracts"],
  },
  {
    packageName: "@martin/core",
    sourceDir: ["packages", "core", "dist"],
    targetDir: ["dist", "vendor", "core"],
  },
  {
    packageName: "@martin/policy",
    sourceDir: ["packages", "policy", "dist"],
    targetDir: ["dist", "vendor", "policy"],
  },
  {
    packageName: "@martin/headlessos-core",
    sourceDir: ["packages", "headlessos-core", "dist"],
    targetDir: ["dist", "vendor", "headlessos-core"],
  },
  {
    packageName: "@martin/audit-exporter",
    sourceDir: ["packages", "audit-exporter", "dist"],
    targetDir: ["dist", "vendor", "audit-exporter"],
  },
  {
    packageName: "@martin/adapters",
    sourceDir: ["packages", "adapters", "dist"],
    targetDir: ["dist", "vendor", "adapters"],
  },
];

const PACKAGE_ASSETS = [
  {
    packageName: "@martin/policy",
    sourceDir: ["packages", "policy", "policies"],
    targetDir: ["dist", "vendor", "policies"],
  },
];

const REWRITABLE_PACKAGES = {
  "@martin/contracts": "contracts",
  "@martin/core": "core",
  "@martin/policy": "policy",
  "@martin/headlessos-core": "headlessos-core",
  "@martin/audit-exporter": "audit-exporter",
  "@martin/adapters": "adapters",
};

export async function buildStandaloneMcpPackage(options = {}) {
  const packageDir = path.resolve(options.packageDir ?? fileURLToPath(new URL("..", import.meta.url)));
  const rootDir = path.resolve(options.rootDir ?? path.join(packageDir, "..", ".."));
  const distDir = path.join(packageDir, "dist");

  await ensureWorkspaceArtifacts(rootDir);
  await rm(distDir, { force: true, recursive: true, maxRetries: 10, retryDelay: 100 });
  await runCommand(pnpmCommand(), ["exec", "tsc", "-p", "tsconfig.build.json"], { cwd: packageDir });

  await rewriteDirectory({
    currentDir: distDir,
    distDir,
    skipDirs: new Set(["vendor"]),
  });

  await vendorDependencyGraph({
    rootDir,
    packageDir,
    distDir,
  });

  await chmod(path.join(distDir, "server.js"), 0o755);

  return {
    packageDir,
    distDir,
    vendorDir: path.join(distDir, "vendor"),
  };
}

async function ensureWorkspaceArtifacts(rootDir) {
  for (const facade of PACKAGE_FACADES.filter((candidate) => ROOT_FACADE_PACKAGES.includes(candidate.packageName))) {
    await runCommand(
      pnpmCommand(),
      workspaceBuildCommandArgs(facade.packageName),
      { cwd: rootDir },
    );
  }
}

async function vendorDependencyGraph(input) {
  const vendored = new Set();
  const pending = [...ROOT_FACADE_PACKAGES];

  while (pending.length > 0) {
    const packageName = pending.shift();
    if (!packageName || vendored.has(packageName)) {
      continue;
    }

    const facade = resolveFacade(packageName);
    const discoveredDependencies = await copyFacadeDirectory({
      sourceDir: path.join(input.rootDir, ...facade.sourceDir),
      targetDir: path.join(input.packageDir, ...facade.targetDir),
      distDir: input.distDir,
      packageName,
    });

    for (const dependency of discoveredDependencies) {
      if (!vendored.has(dependency)) {
        pending.push(dependency);
      }
    }

    for (const asset of PACKAGE_ASSETS.filter((candidate) => candidate.packageName === packageName)) {
      await copyRawDirectory({
        sourceDir: path.join(input.rootDir, ...asset.sourceDir),
        targetDir: path.join(input.packageDir, ...asset.targetDir),
      });
    }

    vendored.add(packageName);
  }
}

function resolveFacade(packageName) {
  const facade = PACKAGE_FACADES.find((candidate) => candidate.packageName === packageName);
  if (!facade) {
    throw new Error(`No vendored facade is configured for ${packageName}.`);
  }
  return facade;
}

export function workspaceBuildCommandArgs(packageName) {
  return ["--filter", packageName, "build"];
}

async function copyFacadeDirectory(input) {
  const sourceStats = await stat(input.sourceDir).catch(() => null);
  if (!sourceStats?.isDirectory()) {
    throw new Error(`Missing vendored facade source directory for ${input.packageName}: ${input.sourceDir}`);
  }

  return copyDirectory({
    sourceDir: input.sourceDir,
    targetDir: input.targetDir,
    distDir: input.distDir,
    relativeDir: "",
  });
}

async function copyRawDirectory(input) {
  const sourceStats = await stat(input.sourceDir).catch(() => null);
  if (!sourceStats?.isDirectory()) {
    return;
  }

  await mkdir(input.targetDir, { recursive: true });

  const entries = await readdir(input.sourceDir, { withFileTypes: true });
  for (const entry of entries) {
    const sourcePath = path.join(input.sourceDir, entry.name);
    const targetPath = path.join(input.targetDir, entry.name);

    if (entry.isDirectory()) {
      await copyRawDirectory({
        sourceDir: sourcePath,
        targetDir: targetPath,
      });
      continue;
    }

    await copyFile(sourcePath, targetPath);
  }
}

async function copyDirectory(input) {
  await mkdir(input.targetDir, { recursive: true });
  const dependencies = new Set();

  const entries = await readdir(input.sourceDir, { withFileTypes: true });

  for (const entry of entries) {
    const relativePath = input.relativeDir
      ? path.join(input.relativeDir, entry.name)
      : entry.name;

    if (entry.isDirectory()) {
      if (shouldSkipDirectory(entry.name, relativePath)) {
        continue;
      }

      const nestedDependencies = await copyDirectory({
        sourceDir: path.join(input.sourceDir, entry.name),
        targetDir: path.join(input.targetDir, entry.name),
        distDir: input.distDir,
        relativeDir: relativePath,
      });
      mergeDependencySets(dependencies, nestedDependencies);
      continue;
    }

    if (shouldSkipFile(entry.name)) {
      continue;
    }

    const sourcePath = path.join(input.sourceDir, entry.name);
    const targetPath = path.join(input.targetDir, entry.name);

    if (entry.name.endsWith(".js") || entry.name.endsWith(".d.ts")) {
      const contents = await readFile(sourcePath, "utf8");
      mergeDependencySets(dependencies, collectRewritablePackages(contents));
      const rewritten = rewriteBuiltFileContents(entry.name, contents, {
        targetPath,
        distDir: input.distDir,
      });
      await writeFile(targetPath, rewritten, "utf8");
      continue;
    }

    await copyFile(sourcePath, targetPath);
  }

  return dependencies;
}

async function rewriteDirectory(input) {
  const entries = await readdir(input.currentDir, { withFileTypes: true });

  for (const entry of entries) {
    const entryPath = path.join(input.currentDir, entry.name);

    if (entry.isDirectory()) {
      if (input.skipDirs.has(entry.name)) {
        continue;
      }
      await rewriteDirectory({
        ...input,
        currentDir: entryPath,
      });
      continue;
    }

    if (entry.name.endsWith(".map")) {
      await rm(entryPath, { force: true });
      continue;
    }

    if (!entry.name.endsWith(".js") && !entry.name.endsWith(".d.ts")) {
      continue;
    }

    const contents = await readFile(entryPath, "utf8");
    const rewritten = rewriteBuiltFileContents(entry.name, contents, {
      targetPath: entryPath,
      distDir: input.distDir,
    });

    if (rewritten !== contents) {
      await writeFile(entryPath, rewritten, "utf8");
    }
  }
}

function shouldSkipDirectory(name, relativePath) {
  return name === "tests" || relativePath === "src";
}

function shouldSkipFile(name) {
  return name.endsWith(".map");
}

export function collectRewritablePackages(contents) {
  const matches = contents.match(/@martin\/(?:contracts|core|policy|headlessos-core|audit-exporter|adapters)(?:\/[^'"]+)?/g) ?? [];
  return new Set(matches.map((match) => match.split("/").slice(0, 2).join("/")));
}

function mergeDependencySets(target, source) {
  for (const entry of source) {
    target.add(entry);
  }
}

export function rewritePackageSpecifiers(contents, input) {
  return contents.replace(
    /(['"])(@martin\/(?:contracts|core|policy|headlessos-core|audit-exporter|adapters)(?:\/[^'"]+)?)\1/g,
    (_match, quote, packageName) => {
      const parts = packageName.split("/");
      const basePackageName = parts.slice(0, 2).join("/");
      const subpath = parts.slice(2).join("/");
      const mapped = REWRITABLE_PACKAGES[basePackageName];
      if (!mapped) {
        return `${quote}${packageName}${quote}`;
      }

      const normalizedSubpath = subpath
        ? (subpath.endsWith(".js") ? subpath : `${subpath}.js`)
        : "index.js";
      const targetFile = subpath
        ? path.join(input.distDir, "vendor", mapped, normalizedSubpath)
        : path.join(input.distDir, "vendor", mapped, "index.js");
      const specifier = toImportSpecifier(path.dirname(input.targetPath), targetFile);

      return `${quote}${specifier}${quote}`;
    },
  );
}

function rewriteBuiltFileContents(fileName, contents, input) {
  const rewritten = rewritePackageSpecifiers(contents, input);
  return fileName.endsWith(".js") || fileName.endsWith(".d.ts")
    ? stripSourceMapDirectives(rewritten)
    : rewritten;
}

function stripSourceMapDirectives(contents) {
  return contents
    .replace(/^[ \t]*\/\/[#@]\s*sourceMappingURL=.*(?:\r?\n)?/gmu, "")
    .replace(/^[ \t]*\/\*#\s*sourceMappingURL=.*?\*\/(?:\r?\n)?/gmsu, "")
    .replace(/^[ \t]*\/\/[#@]\s*declarationMappingURL=.*(?:\r?\n)?/gmu, "");
}

function toImportSpecifier(fromDir, toFile) {
  const relativePath = path.relative(fromDir, toFile).split(path.sep).join("/");
  return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
}

function pnpmCommand() {
  return process.platform === "win32" ? "pnpm.cmd" : "pnpm";
}

async function runCommand(command, args, options) {
  await new Promise((resolve, reject) => {
    const launch = createCommandLaunch(command, args);
    const child = spawn(launch.command, launch.args, {
      cwd: options.cwd,
      env: process.env,
      stdio: "inherit",
      shell: false,
      windowsHide: true,
    });

    child.on("error", reject);
    child.on("close", (code) => {
      if (code !== 0) {
        reject(new Error(`Command failed (${code ?? "unknown"}): ${command} ${args.join(" ")}`));
        return;
      }
      resolve();
    });
  });
}

export function createCommandLaunch(command, args, platform = process.platform) {
  if (platform !== "win32") {
    return { command, args };
  }

  return {
    command: process.env.ComSpec ?? "cmd.exe",
    args: ["/d", "/s", "/c", toCmdCommand(command, args)],
  };
}

function toCmdCommand(command, args) {
  return [quoteForCmdArgument(command), ...args.map((arg) => quoteForCmdArgument(arg))].join(" ");
}

function quoteForCmdArgument(value) {
  return /[\s"]/u.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
}
