import { createHash } from "node:crypto";
import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync } from "node:fs";
import { basename, dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { FleetError } from "./loader.mjs";
import { canonicalSourceMode } from "./mode.mjs";

const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "../..");
const MODULES = join(REPO_ROOT, "modules");
const WORKSTATION = join(MODULES, "workstation");
const BUILDBOX = join(MODULES, "buildbox");
const MANIFEST_DIR = join(WORKSTATION, "manifest");
const DEVTOOLS_PATH = join(BUILDBOX, "devtools.json");
const WORKSTATION_USER_UNITS = join(WORKSTATION, "systemd/user");
const USER_CONFIG = join(BUILDBOX, "user-config");
const HOST_CONFIG = join(BUILDBOX, "host-config");
const SLICE_CEILINGS = join(WORKSTATION, "claude/systemd/user-root");

const PROFILE_EXPANDERS = new Map([
  ["shared-agent-tools", expandSharedAgentTools],
  ["workstation", expandWorkstation],
  ["buildbox", expandBuildbox],
  ["buildbox-root", expandBuildboxRoot],
]);

const HOST_CONFIG_INSTALLS = [
  { subdir: "sysctl.d", destination: "/etc/sysctl.d" },
  { subdir: "system-conf.d", destination: "/etc/systemd/system.conf.d" },
  { subdir: "journald.conf.d", destination: "/etc/systemd/journald.conf.d" },
  { subdir: "systemd-system", destination: "/etc/systemd/system" },
];

function fail(message) {
  throw new FleetError(message);
}

function expandTilde(path) {
  if (path === "~") return "~";
  if (path.startsWith("~/")) return path;
  if (path.startsWith("/")) return path;
  return `~/${path}`;
}

function repoRel(absPath) {
  return relative(REPO_ROOT, absPath).split("\\").join("/");
}

function readJson(path) {
  return JSON.parse(readFileSync(path, "utf8"));
}

function sha256Hex(value) {
  return createHash("sha256").update(value).digest("hex");
}

function fileDigest(absPath) {
  if (!existsSync(absPath)) {
    return sha256Hex(`ABSENT:${repoRel(absPath)}`);
  }
  const st = lstatSync(absPath);
  if (st.isSymbolicLink()) {
    return sha256Hex(`L:${readlinkSync(absPath)}`);
  }
  if (st.isFile()) {
    return sha256Hex(readFileSync(absPath));
  }
  return treeDigest(absPath);
}

function treeDigest(root) {
  const lines = [];
  walkTree(root, "", lines);
  return sha256Hex(lines.join("\n"));
}

function walkTree(root, prefix, lines) {
  if (!existsSync(root)) {
    lines.push(`ABSENT ${prefix || "."}`);
    return;
  }
  const st = lstatSync(root);
  if (st.isSymbolicLink()) {
    lines.push(`L ${prefix || "."} -> ${readlinkSync(root)}`);
    return;
  }
  if (st.isFile()) {
    lines.push(`${canonicalSourceMode(st)} ${prefix || "."} ${sha256Hex(readFileSync(root))}`);
    return;
  }
  if (!st.isDirectory()) return;
  for (const name of readdirSync(root).sort()) {
    if (name === "." || name === "..") continue;
    const rel = prefix ? `${prefix}/${name}` : name;
    walkTree(join(root, name), rel, lines);
  }
}

function listFilesRecursive(root) {
  const out = [];
  if (!existsSync(root)) return out;
  const stack = [root];
  while (stack.length) {
    const dir = stack.pop();
    for (const name of readdirSync(dir).sort()) {
      const abs = join(dir, name);
      const st = lstatSync(abs);
      if (st.isDirectory()) {
        stack.push(abs);
      } else if (st.isFile() || st.isSymbolicLink()) {
        out.push(abs);
      }
    }
  }
  return out.sort();
}

function repoSource(path, kind = "repo") {
  return { kind, path: repoRel(path), ref: null };
}

function makeItem({
  id,
  source,
  destination,
  ownership,
  strategy,
  mode,
  content_digest,
  owner,
}) {
  return Object.freeze({
    id,
    source: Object.freeze({ ...source }),
    destination,
    ownership,
    strategy,
    mode,
    content_digest,
    owner,
  });
}

function loadDevtools() {
  return readJson(DEVTOOLS_PATH);
}

function expandAgentHomes() {
  const items = [];
  const manifestPaths = listFilesRecursive(MANIFEST_DIR)
    .filter((p) => p.endsWith(".json"))
    .sort((a, b) => a.localeCompare(b));
  for (const manifestPath of manifestPaths) {
    const agent = basename(manifestPath, ".json");
    const manifest = readJson(manifestPath);
    const home = expandTilde(manifest.home);
    for (const entry of manifest.entries ?? []) {
      const sourceKind = entry.source === "deploy" ? "deploy" : "repo";
      const sourcePath = join(WORKSTATION, agent, entry.path);
      const destination = `${home}/${entry.path}`;
      items.push(
        makeItem({
          id: `agent-home:${agent}:${entry.path}`,
          source: repoSource(sourcePath, sourceKind),
          destination,
          ownership: "user",
          strategy: entry.strategy,
          mode: "converge",
          content_digest: fileDigest(sourcePath),
          owner: "remote-seat-provision",
        }),
      );
    }
  }
  return items;
}

function vendorDestination(vendor, root) {
  if (vendor.dest) {
    return expandTilde(vendor.dest.replaceAll("{version}", vendor.version));
  }
  const bins = Object.values(vendor.bins ?? {});
  if (bins.length > 0) {
    return expandTilde(bins[0].replaceAll("{dest}", "").replaceAll("{version}", vendor.version));
  }
  return expandTilde(`${root}/${vendor.name}`);
}

function expandDevtools() {
  const spec = loadDevtools();
  const root = expandTilde(spec.root ?? "~/.dev-tools");
  const items = [];

  for (const tool of spec.tools ?? []) {
    const dest = expandTilde(tool.dest ?? `${root}/${tool.name}`);
    const declaration = JSON.stringify({
      name: tool.name,
      repo: tool.repo,
      ref: tool.ref,
      dest,
      install: tool.install ?? "",
      bins: tool.bins ?? {},
    });
    items.push(
      makeItem({
        id: `devtools:tool:${tool.name}`,
        source: { kind: "ref", path: tool.repo, ref: tool.ref },
        destination: dest,
        ownership: "user",
        strategy: "git-checkout",
        mode: "converge",
        content_digest: sha256Hex(declaration),
        owner: "fleet",
      }),
    );
    for (const [binName, binPath] of Object.entries(tool.bins ?? {})) {
      const target = expandTilde(binPath.startsWith("/") ? binPath : `${dest}/${binPath}`);
      items.push(
        makeItem({
          id: `devtools:tool:${tool.name}:bin:${binName}`,
          source: { kind: "ref", path: tool.repo, ref: tool.ref },
          destination: expandTilde(`~/.local/bin/${binName}`),
          ownership: "user",
          strategy: "symlink",
          mode: "converge",
          content_digest: sha256Hex(`${tool.ref}:${binName}:${target}`),
          owner: "fleet",
        }),
      );
    }
  }

  for (const vendor of spec.vendor ?? []) {
    const dest = vendorDestination(vendor, root);
    const sourcePath = vendor.kind === "mise" ? vendor.tool : vendor.url?.replaceAll("{version}", vendor.version);
    const declaration = JSON.stringify(vendor);
    items.push(
      makeItem({
        id: `devtools:vendor:${vendor.name}`,
        source: { kind: "ref", path: sourcePath, ref: vendor.version },
        destination: dest,
        ownership: "user",
        strategy: vendor.kind === "mise" ? "git-checkout" : "copy",
        mode: "converge",
        content_digest: sha256Hex(declaration),
        owner: "fleet",
      }),
    );
    for (const [binName, binPath] of Object.entries(vendor.bins ?? {})) {
      const target = expandTilde(binPath.replaceAll("{dest}", dest));
      items.push(
        makeItem({
          id: `devtools:vendor:${vendor.name}:bin:${binName}`,
          source: { kind: "ref", path: sourcePath, ref: vendor.version },
          destination: expandTilde(`~/.local/bin/${binName}`),
          ownership: "user",
          strategy: "symlink",
          mode: "converge",
          content_digest: sha256Hex(`${vendor.version}:${binName}:${target}`),
          owner: "fleet",
        }),
      );
    }
  }

  return items;
}

function expandSharedAgentTools() {
  return expandDevtools();
}

function expandWorkstationUnits() {
  const items = [];
  for (const abs of listFilesRecursive(WORKSTATION_USER_UNITS)) {
    const rel = relative(WORKSTATION_USER_UNITS, abs).split("\\").join("/");
    const destination = expandTilde(`~/.config/systemd/user/${rel}`);
    items.push(
      makeItem({
        id: `workstation:unit:${rel}`,
        source: repoSource(abs),
        destination,
        ownership: "user",
        strategy: "unit",
        mode: "converge",
        content_digest: fileDigest(abs),
        owner: "fleet",
      }),
    );
  }
  return items;
}

function expandWorkstation() {
  return [...expandAgentHomes(), ...expandWorkstationUnits()];
}

function expandBuildboxUserConfig() {
  const items = [];
  for (const abs of listFilesRecursive(USER_CONFIG)) {
    const rel = relative(USER_CONFIG, abs).split("\\").join("/");
    let destination;
    let strategy;
    if (rel.startsWith("bin/")) {
      destination = expandTilde(`~/.local/bin/${basename(rel)}`);
      strategy = "copy";
    } else if (rel.startsWith("systemd/")) {
      destination = expandTilde(`~/.config/systemd/user/${basename(rel)}`);
      strategy = "unit";
    } else {
      destination = expandTilde(`~/${rel}`);
      strategy = "copy";
    }
    items.push(
      makeItem({
        id: `buildbox:user-config:${rel}`,
        source: repoSource(abs),
        destination,
        ownership: "user",
        strategy,
        mode: "converge",
        content_digest: fileDigest(abs),
        owner: "fleet",
      }),
    );
  }
  return items;
}

function expandBuildbox() {
  return expandBuildboxUserConfig();
}

function expandHostConfigTree(sourceRoot, idPrefix, destinationRoot, mode) {
  const items = [];
  for (const abs of listFilesRecursive(sourceRoot)) {
    const rel = relative(sourceRoot, abs).split("\\").join("/");
    const destination = `${destinationRoot}/${rel}`;
    items.push(
      makeItem({
        id: `${idPrefix}:${rel}`,
        source: repoSource(abs),
        destination,
        ownership: "root",
        strategy: "copy",
        mode,
        content_digest: fileDigest(abs),
        owner: "fleet",
      }),
    );
  }
  return items;
}

function expandBuildboxRoot() {
  const items = [];
  for (const { subdir, destination } of HOST_CONFIG_INSTALLS) {
    const sourceRoot = join(HOST_CONFIG, subdir);
    items.push(...expandHostConfigTree(sourceRoot, `buildbox-root:host-config:${subdir}`, destination, "harden"));
  }
  for (const binName of ["buildbox-rescue-door", "buildbox-sshd-access"]) {
    const abs = join(HOST_CONFIG, "bin", binName);
    if (!existsSync(abs)) continue;
    items.push(
      makeItem({
        id: `buildbox-root:host-config:bin:${binName}`,
        source: repoSource(abs),
        destination: `/usr/local/sbin/${binName}`,
        ownership: "root",
        strategy: "copy",
        mode: "harden",
        content_digest: fileDigest(abs),
        owner: "fleet",
      }),
    );
  }
  items.push(
    ...expandHostConfigTree(SLICE_CEILINGS, "buildbox-root:slice-ceilings", "/etc/systemd/user", "harden"),
  );
  return items;
}

/**
 * @param {import("./types.mjs").DesiredItem[]} items
 */
export function rejectDuplicateDestinations(items) {
  const seen = new Map();
  for (const item of items) {
    const prior = seen.get(item.destination);
    if (prior) {
      fail(`duplicate destination ${JSON.stringify(item.destination)} (${prior.id} and ${item.id})`);
    }
    seen.set(item.destination, item);
  }
}

/**
 * @param {import("./loader.mjs").ValidatedFleet} fleet
 * @param {string} nodeName
 * @returns {import("./types.mjs").DesiredItem[]}
 */
export function expandNode(fleet, nodeName) {
  const node = fleet.node(nodeName);
  const items = [];
  for (const profile of node.profiles) {
    const expander = PROFILE_EXPANDERS.get(profile);
    if (!expander) {
      fail(`profile: unknown profile ${JSON.stringify(profile)}`);
    }
    items.push(...expander());
  }
  rejectDuplicateDestinations(items);
  return Object.freeze(items.map((item) => Object.freeze({ ...item })));
}
