import { createHash, randomBytes } from "node:crypto";
import { execFile } from "node:child_process";
import {
  existsSync,
  lstatSync,
  promises as fs,
  readFileSync,
  readlinkSync,
  readdirSync,
} from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

export class LocalTransportError extends Error {
  constructor(message) {
    super(message);
    this.name = "LocalTransportError";
  }
}

/**
 * @param {string} path
 * @param {string} home
 */
function resolvePath(path, home) {
  if (path === "~") return home;
  if (path.startsWith("~/")) return join(home, path.slice(2));
  return path;
}

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

/**
 * @param {import("node:fs").Stats} st
 */
function fileOwnership(st) {
  return st.uid === 0 ? "root" : "user";
}

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(`${(st.mode & 0o777).toString(8)} ${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 treeDigest(root) {
  const lines = [];
  walkTree(root, "", lines);
  return sha256Hex(lines.join("\n"));
}

/**
 * @param {string} absPath
 * @param {import("node:fs").Stats} st
 */
function pathDigest(absPath, st) {
  if (st.isSymbolicLink()) {
    return sha256Hex(`L:${readlinkSync(absPath)}`);
  }
  if (st.isFile()) {
    return sha256Hex(readFileSync(absPath));
  }
  if (st.isDirectory()) {
    return treeDigest(absPath);
  }
  return null;
}

/**
 * @param {string[]} argv
 * @returns {Promise<{ stdout: string, stderr: string, code: number }>}
 */
async function defaultExec(argv) {
  try {
    const { stdout, stderr } = await execFileAsync(argv[0], argv.slice(1), {
      encoding: "utf8",
    });
    return { stdout: stdout ?? "", stderr: stderr ?? "", code: 0 };
  } catch (err) {
    const execErr = /** @type {NodeJS.ErrnoException & { stdout?: string; stderr?: string }} */ (
      err
    );
    return {
      stdout: execErr.stdout ?? "",
      stderr: execErr.stderr ?? "",
      code: typeof execErr.code === "number" ? execErr.code : 1,
    };
  }
}

/**
 * @param {object} [options]
 * @param {string} [options.home]
 * @param {(argv: string[]) => Promise<{ stdout: string, stderr: string, code: number }>} [options.exec]
 * @param {typeof fs.rename} [options.rename]
 * @returns {import("./types.mjs").Transport}
 */
export function createLocalTransport(options = {}) {
  const home = options.home ?? homedir();
  const exec = options.exec ?? defaultExec;
  const rename = options.rename ?? fs.rename.bind(fs);

  return {
    name: "local",

    async read(path) {
      const abs = resolvePath(path, home);
      if (!existsSync(abs)) {
        return {
          exists: false,
          digest: null,
          ownership: "user",
          mode: null,
          linkTarget: null,
        };
      }
      const st = lstatSync(abs);
      return {
        exists: true,
        digest: pathDigest(abs, st),
        ownership: fileOwnership(st),
        mode: String(st.mode & 0o777),
        linkTarget: st.isSymbolicLink() ? readlinkSync(abs) : null,
      };
    },

    async write(item, stagedBytes) {
      if (item.ownership === "root") {
        throw new LocalTransportError("local transport refuses root-owned items");
      }

      const dest = resolvePath(item.destination, home);
      const bytesDigest = sha256Hex(stagedBytes);
      if (bytesDigest !== item.content_digest) {
        throw new LocalTransportError(
          `content digest mismatch for ${item.id}: expected ${item.content_digest}, got ${bytesDigest}`,
        );
      }

      if (existsSync(dest)) {
        const st = lstatSync(dest);
        if (st.isFile()) {
          const current = sha256Hex(readFileSync(dest));
          if (current === item.content_digest) {
            return;
          }
        }
      }

      await fs.mkdir(dirname(dest), { recursive: true });

      const stagingPath = join(
        dirname(dest),
        `.${basename(dest)}.staging-${process.pid}-${randomBytes(8).toString("hex")}`,
      );

      try {
        await fs.writeFile(stagingPath, stagedBytes);
        const writtenDigest = sha256Hex(readFileSync(stagingPath));
        if (writtenDigest !== item.content_digest) {
          throw new LocalTransportError(
            `staged content digest mismatch for ${item.id}: expected ${item.content_digest}, got ${writtenDigest}`,
          );
        }
        await rename(stagingPath, dest);
      } catch (err) {
        await fs.unlink(stagingPath).catch(() => {});
        throw err;
      }
    },

    async unitState(unitName) {
      const enabledResult = await exec(["systemctl", "--user", "is-enabled", unitName]);
      const activeResult = await exec(["systemctl", "--user", "is-active", unitName]);
      return {
        enabled: enabledResult.code === 0,
        active: activeResult.code === 0 && activeResult.stdout.trim() === "active",
      };
    },

    async gitRef(path) {
      const abs = resolvePath(path, home);
      const base = existsSync(abs)
        ? lstatSync(abs).isDirectory()
          ? abs
          : dirname(abs)
        : dirname(abs);
      const result = await exec(["git", "-C", base, "rev-parse", "HEAD"]);
      if (result.code !== 0) {
        return null;
      }
      const ref = result.stdout.trim();
      return ref || null;
    },
  };
}
