import crypto from "crypto";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const RUNNER_MANIFEST = "runner.manifest.json";
const TESTKIT_CONFIG = "testkit.config.json";
const DEFAULT_RUNTIME_DIR = path.join("tests", "_testkit");
const METADATA_FILE = ".runtime-manifest.json";
const RUNTIME_FORMAT = 1;

export function installRuntime(options = {}) {
  const productDir = resolveProductDir(process.cwd(), options.dir);
  const runtimeDir = resolveRuntimeDir(productDir, options.path);
  const sourceFiles = readBundledRuntimeFiles();

  fs.mkdirSync(runtimeDir, { recursive: true });

  for (const file of sourceFiles) {
    const targetPath = path.join(runtimeDir, file.path);
    fs.mkdirSync(path.dirname(targetPath), { recursive: true });
    fs.writeFileSync(targetPath, file.content);
  }

  const metadata = {
    format: RUNTIME_FORMAT,
    package: "@elench/testkit",
    version: readPackageVersion(),
    files: sourceFiles.map((file) => ({
      path: file.path,
      sha256: hashContent(file.content),
    })),
  };
  fs.writeFileSync(
    path.join(runtimeDir, METADATA_FILE),
    `${JSON.stringify(metadata, null, 2)}\n`
  );

  return {
    productDir,
    runtimeDir,
    relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
    files: metadata.files,
  };
}

export function getRuntimeStatus(options = {}) {
  const productDir = resolveProductDir(process.cwd(), options.dir);
  const runtimeDir = resolveRuntimeDir(productDir, options.path);
  const sourceFiles = readBundledRuntimeFiles();
  const metadataPath = path.join(runtimeDir, METADATA_FILE);

  if (!fs.existsSync(runtimeDir) || !fs.existsSync(metadataPath)) {
    return {
      status: "missing",
      productDir,
      runtimeDir,
      relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
      missingFiles: sourceFiles.map((file) => file.path),
      driftedFiles: [],
    };
  }

  const missingFiles = [];
  const driftedFiles = [];

  for (const file of sourceFiles) {
    const targetPath = path.join(runtimeDir, file.path);
    if (!fs.existsSync(targetPath)) {
      missingFiles.push(file.path);
      continue;
    }

    const installed = fs.readFileSync(targetPath, "utf8");
    if (installed !== file.content) {
      driftedFiles.push(file.path);
    }
  }

  const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
  const versionMatches = metadata.version === readPackageVersion();

  return {
    status:
      missingFiles.length === 0 && driftedFiles.length === 0 && versionMatches
        ? "installed"
        : "drifted",
    productDir,
    runtimeDir,
    relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
    versionMatches,
    missingFiles,
    driftedFiles,
  };
}

export function formatRuntimeStatus(result) {
  if (result.status === "missing") {
    return `Runtime not installed at ${result.relativeRuntimeDir}`;
  }

  if (result.status === "installed") {
    return `Runtime at ${result.relativeRuntimeDir} is up to date`;
  }

  const problems = [];
  if (result.missingFiles.length > 0) {
    problems.push(`missing: ${result.missingFiles.join(", ")}`);
  }
  if (result.driftedFiles.length > 0) {
    problems.push(`drifted: ${result.driftedFiles.join(", ")}`);
  }
  if (result.versionMatches === false) {
    problems.push("version drift");
  }

  return `Runtime at ${result.relativeRuntimeDir} is drifted (${problems.join("; ")})`;
}

function resolveProductDir(cwd, explicitDir) {
  const dir = explicitDir ? path.resolve(cwd, explicitDir) : cwd;
  ensureProductFiles(dir);
  return dir;
}

function ensureProductFiles(dir) {
  const missing = [RUNNER_MANIFEST, TESTKIT_CONFIG].filter(
    (file) => !fs.existsSync(path.join(dir, file))
  );

  if (missing.length > 0) {
    throw new Error(
      `Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
    );
  }
}

function resolveRuntimeDir(productDir, explicitPath) {
  return path.resolve(productDir, explicitPath || DEFAULT_RUNTIME_DIR);
}

function relativeToProduct(productDir, targetPath) {
  return path.relative(productDir, targetPath) || ".";
}

function readBundledRuntimeFiles() {
  const sourceDir = path.resolve(
    path.dirname(fileURLToPath(import.meta.url)),
    "..",
    "runtime-src"
  );

  return walkRuntimeFiles(sourceDir);
}

function walkRuntimeFiles(rootDir, relativeDir = "") {
  const entries = fs.readdirSync(path.join(rootDir, relativeDir), {
    withFileTypes: true,
  });
  const files = [];

  for (const entry of entries) {
    const nextRelative = path.join(relativeDir, entry.name);
    if (entry.isDirectory()) {
      files.push(...walkRuntimeFiles(rootDir, nextRelative));
      continue;
    }

    const absolute = path.join(rootDir, nextRelative);
    files.push({
      path: nextRelative.split(path.sep).join("/"),
      content: fs.readFileSync(absolute, "utf8"),
    });
  }

  return files.sort((left, right) => left.path.localeCompare(right.path));
}

function readPackageVersion() {
  const packagePath = path.resolve(
    path.dirname(fileURLToPath(import.meta.url)),
    "..",
    "..",
    "package.json"
  );
  return JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
}

function hashContent(content) {
  return crypto.createHash("sha256").update(content).digest("hex");
}
