const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");

function readJson(filePath, fallback = null) {
  try {
    return JSON.parse(fs.readFileSync(filePath, "utf8"));
  } catch {
    return fallback;
  }
}

function dependencyFingerprint(rootPath) {
  const packageJson = fs.readFileSync(path.join(rootPath, "package.json"));
  const lockPath = path.join(rootPath, "package-lock.json");
  const lock = fs.existsSync(lockPath) ? fs.readFileSync(lockPath) : Buffer.from("");
  return crypto.createHash("sha256").update(packageJson).update(lock).digest("hex");
}

function dependencyIssues(rootPath) {
  const manifest = readJson(path.join(rootPath, "package.json"), {});
  const lock = readJson(path.join(rootPath, "package-lock.json"), {});
  const issues = [];
  const dependencies = new Map([
    ...Object.keys(manifest.dependencies || {}).map((name) => [name, false]),
    ...Object.keys(manifest.optionalDependencies || {}).map((name) => [name, true])
  ]);
  for (const [name, optional] of [...dependencies.entries()].sort(([left], [right]) => left.localeCompare(right))) {
    const installedPath = path.join(rootPath, "node_modules", ...name.split("/"), "package.json");
    const installed = readJson(installedPath);
    const expected = lock.packages?.[`node_modules/${name}`]?.version || null;
    if (!installed?.version) {
      issues.push({ dependency: name, reason: "missing", expected, optional });
    } else if (expected && installed.version !== expected) {
      issues.push({ dependency: name, reason: "version_mismatch", expected, installed: installed.version, optional });
    }
  }
  return issues;
}

function writeState(rootPath, values) {
  const statePath = path.join(rootPath, "data", "update-dependencies.json");
  fs.mkdirSync(path.dirname(statePath), { recursive: true });
  const temporary = `${statePath}.${process.pid}.tmp`;
  fs.writeFileSync(temporary, `${JSON.stringify(values, null, 2)}\n`);
  fs.renameSync(temporary, statePath);
  return statePath;
}

function npmInvocation(options = {}) {
  const environment = options.env || process.env;
  const execPath = path.resolve(options.execPath || process.execPath);
  const configuredCli = String(environment.npm_execpath || "").trim();
  if (configuredCli && fs.existsSync(configuredCli)) {
    return { command: execPath, args: [configuredCli] };
  }

  const platform = options.platform || process.platform;
  if (platform === "win32") {
    const candidates = [
      path.join(path.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js"),
      environment.ProgramFiles
        ? path.join(environment.ProgramFiles, "nodejs", "node_modules", "npm", "bin", "npm-cli.js")
        : null
    ].filter(Boolean);
    const npmCli = candidates.find((candidate) => fs.existsSync(candidate));
    if (npmCli) return { command: execPath, args: [npmCli] };

    return {
      command: String(environment.ComSpec || environment.COMSPEC || "cmd.exe"),
      args: ["/d", "/s", "/c", "npm.cmd"]
    };
  }
  return { command: "npm", args: [] };
}

function ensureRuntimeDependencies(options = {}) {
  const rootPath = path.resolve(options.rootPath || path.join(__dirname, "..", ".."));
  const before = dependencyIssues(rootPath);
  const fingerprint = dependencyFingerprint(rootPath);
  if (!before.length) {
    writeState(rootPath, {
      schema_version: 1,
      fingerprint,
      status: "ready",
      checked_at: new Date().toISOString()
    });
    return { ready: true, installed: false, issues: [] };
  }
  if (options.install === false) return { ready: false, installed: false, issues: before };

  const invocation = npmInvocation(options);
  const installArgs = fs.existsSync(path.join(rootPath, "package-lock.json"))
    ? ["ci", "--omit=dev", "--no-audit", "--no-fund"]
    : ["install", "--omit=dev", "--no-audit", "--no-fund"];
  const runner = options.runner || spawnSync;
  const result = runner(invocation.command, [...invocation.args, ...installArgs], {
    cwd: rootPath,
    encoding: "utf8",
    timeout: Math.max(60000, Number(options.timeoutMs) || 600000),
    windowsHide: true
  });
  if (result?.error || result?.status !== 0) {
    const detail = String(result?.stderr || result?.stdout || result?.error?.message || "npm failed").trim().slice(-4000);
    if (before.every((issue) => issue.optional)) {
      writeState(rootPath, {
        schema_version: 1,
        fingerprint,
        status: "ready_without_optional_dependencies",
        checked_at: new Date().toISOString(),
        unresolved: before,
        warning: detail
      });
      return { ready: true, installed: false, optional_failed: true, issues: before, warning: detail };
    }
    throw new Error(`Dependency synchronization failed. ${detail}`);
  }
  const after = dependencyIssues(rootPath);
  const requiredAfter = after.filter((issue) => !issue.optional);
  if (requiredAfter.length) {
    throw new Error(`Dependency synchronization finished with unresolved packages: ${requiredAfter.map((item) => item.dependency).join(", ")}.`);
  }
  writeState(rootPath, {
    schema_version: 1,
    fingerprint,
    status: after.length ? "ready_without_optional_dependencies" : "ready",
    installed_at: new Date().toISOString(),
    resolved: before.filter((issue) => !after.some((remaining) => remaining.dependency === issue.dependency)),
    unresolved: after
  });
  return { ready: true, installed: true, issues: before, unresolved: after };
}

module.exports = {
  dependencyFingerprint,
  dependencyIssues,
  ensureRuntimeDependencies,
  npmInvocation
};
