import { createHash, randomBytes } from "node:crypto";
import { execFile } from "node:child_process";
import { mkdtempSync, unlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join } from "node:path";
import { promisify } from "node:util";
import {
  ACCESS_PATHS,
  BuildboxRegistryError,
  requireReachable,
  resolveAccess,
} from "../../modules/workstation/claude/lib/buildbox-registry.mjs";
import { shq, sshOpts, sshTarget } from "../../modules/workstation/claude/lib/remote-ssh.mjs";

const execFileAsync = promisify(execFile);

export class SshTransportError extends Error {
  /**
   * @param {string} nodeName
   * @param {string} detail
   */
  constructor(nodeName, detail) {
    super(`ssh transport: ${nodeName}: ${detail}`);
    this.name = "SshTransportError";
    this.nodeName = nodeName;
  }
}

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

/**
 * @param {import("../../modules/workstation/claude/lib/buildbox-registry.mjs").ReturnType<typeof resolveAccess>} access
 * @param {number} [connectTimeoutSec]
 */
function accessToSshCfg(access, connectTimeoutSec = 6) {
  return {
    host: access.host,
    port: access.port,
    ssh_user: access.user,
    identity_file: access.identity_file,
    connect_timeout_sec: connectTimeoutSec,
  };
}

/**
 * @param {import("../../modules/workstation/claude/lib/buildbox-registry.mjs").ReturnType<typeof import("../../modules/workstation/claude/lib/buildbox-registry.mjs").parseRegistry>} registry
 * @param {string} hostRef
 */
function resolveDialAccess(registry, hostRef) {
  requireReachable(registry, [hostRef]);
  const host = registry.hosts.find((entry) => entry.name === hostRef || entry.ssh_alias === hostRef);
  if (!host) {
    throw new BuildboxRegistryError(`no such buildbox host "${hostRef}"`);
  }
  for (const path of ACCESS_PATHS) {
    if (host.access[path] !== null) {
      return { nodeName: host.name, access: resolveAccess(registry, host.name, path) };
    }
  }
  throw new SshTransportError(host.name, "has no dialable access path");
}

/**
 * @param {string} message
 */
function isConnectionFailure(message) {
  return /ECONNREFUSED|Connection refused|Connection timed out|No route to host|Network is unreachable|Could not resolve hostname|Operation timed out|Connection reset/i.test(
    message,
  );
}

/**
 * @param {string} script
 * @param {string[]} args
 */
function remoteBash(script, args = []) {
  const argList = args.map(shq).join(" ");
  const encoded = Buffer.from(script).toString("base64");
  return `printf %s ${shq(encoded)} | base64 -d | bash -s ${argList}`;
}

const REMOTE_READ_SH = [
  "set -euo pipefail",
  "path=$1",
  "sha256_hex() { printf '%s' \"$1\" | sha256sum | awk '{print $1}'; }",
  'if [ ! -e "$path" ]; then',
  "  printf '%s\\n' '__FLEET__:absent'",
  "  exit 0",
  "fi",
  'if [ -L "$path" ]; then',
  '  target=$(readlink "$path")',
  '  digest=$(sha256_hex "L:$target")',
  "  printf 'exists=1\\ndigest=%s\\nownership=user\\nmode=\\nlink=%s\\n' \"$digest\" \"$target\"",
  "  exit 0",
  "fi",
  'if [ -f "$path" ]; then',
  "  digest=$(sha256sum \"$path\" | awk '{print $1}')",
  "  mode=$(stat -c '%a' \"$path\")",
  "  uid=$(stat -c '%u' \"$path\")",
  "  ownership=user",
  '  [ "$uid" = 0 ] && ownership=root',
  "  printf 'exists=1\\ndigest=%s\\nownership=%s\\nmode=%s\\nlink=\\n' \"$digest\" \"$ownership\" \"$mode\"",
  "  exit 0",
  "fi",
  'if [ -d "$path" ]; then',
  "  lines_file=$(mktemp)",
  "  walk() {",
  '    local root="$1"',
  '    local prefix="$2"',
  '    if [ ! -e "$root" ]; then',
  '      printf \'ABSENT %s\\n\' "${prefix:-.}" >>"$lines_file"',
  "      return",
  "    fi",
  '    if [ -L "$root" ]; then',
  '      printf \'L %s -> %s\\n\' "${prefix:-.}" "$(readlink "$root")" >>"$lines_file"',
  "      return",
  "    fi",
  '    if [ -f "$root" ]; then',
  "      local mode digest",
  "      mode=$(stat -c '%a' \"$root\")",
  "      digest=$(sha256sum \"$root\" | awk '{print $1}')",
  '      printf \'%s %s %s\\n\' "$mode" "${prefix:-.}" "$digest" >>"$lines_file"',
  "      return",
  "    fi",
  '    if [ ! -d "$root" ]; then',
  "      return",
  "    fi",
  "    local entry rel",
  "    while IFS= read -r entry; do",
  '      [ "$entry" = . ] || [ "$entry" = .. ] && continue',
  '      if [ -n "$prefix" ]; then rel="${prefix}/${entry}"; else rel="$entry"; fi',
  '      walk "$root/$entry" "$rel"',
  "    done < <(find \"$root\" -mindepth 1 -maxdepth 1 -printf '%f\\n' | sort)",
  "  }",
  '  walk "$path" ""',
  "  joined=$(paste -sd '\\n' \"$lines_file\")",
  "  digest=$(printf '%s' \"$joined\" | sha256sum | awk '{print $1}')",
  '  rm -f "$lines_file"',
  "  mode=$(stat -c '%a' \"$path\")",
  "  uid=$(stat -c '%u' \"$path\")",
  "  ownership=user",
  '  [ "$uid" = 0 ] && ownership=root',
  "  printf 'exists=1\\ndigest=%s\\nownership=%s\\nmode=%s\\nlink=\\n' \"$digest\" \"$ownership\" \"$mode\"",
  "  exit 0",
  "fi",
  "printf '%s\\n' '__FLEET__:absent'",
  "",
].join("\n");

const REMOTE_WRITE_SH = String.raw`set -euo pipefail
dest=$1
staging=$2
expected=$3
if [ -f "$dest" ]; then
  current=$(sha256sum "$dest" | awk '{print $1}')
  if [ "$current" = "$expected" ]; then
    rm -f "$staging"
    printf 'noop\n'
    exit 0
  fi
fi
actual=$(sha256sum "$staging" | awk '{print $1}')
if [ "$actual" != "$expected" ]; then
  rm -f "$staging"
  exit 42
fi
mv -f "$staging" "$dest"
printf 'promoted\n'
`;

const REMOTE_CLEANUP_SH = String.raw`set -euo pipefail
rm -f "$1"
`;

/**
 * @param {string} stdout
 */
function parseReadOutput(stdout) {
  const text = stdout.trim();
  if (!text || text === "__FLEET__:absent") {
    return {
      exists: false,
      digest: null,
      ownership: /** @type {"user"} */ ("user"),
      mode: null,
      linkTarget: null,
    };
  }
  const fields = Object.fromEntries(
    text
      .split("\n")
      .map((line) => line.split("=", 2))
      .filter((parts) => parts.length === 2),
  );
  return {
    exists: fields.exists === "1",
    digest: fields.digest || null,
    ownership: fields.ownership === "root" ? "root" : "user",
    mode: fields.mode || null,
    linkTarget: fields.link || 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",
      maxBuffer: 16 * 1024 * 1024,
    });
    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.hostRef
 * @param {ReturnType<typeof import("../../modules/workstation/claude/lib/buildbox-registry.mjs").parseRegistry>} options.registry
 * @param {(argv: string[]) => Promise<{ stdout: string, stderr: string, code: number }>} [options.exec]
 * @param {number} [options.connectTimeoutSec]
 * @returns {import("./types.mjs").Transport}
 */
export function createSshTransport(options) {
  const { hostRef, registry } = options;
  if (!hostRef || !registry) {
    throw new Error("createSshTransport requires hostRef and registry");
  }

  let nodeName;
  let sshCfg;
  try {
    const dial = resolveDialAccess(registry, hostRef);
    nodeName = dial.nodeName;
    sshCfg = accessToSshCfg(dial.access, options.connectTimeoutSec);
  } catch (err) {
    if (err instanceof BuildboxRegistryError) {
      const match = /host "([^"]+)"/.exec(err.message);
      const named = match?.[1] ?? hostRef;
      throw new SshTransportError(named, err.message.replace(/^buildbox registry[^:]*:\s*/, ""));
    }
    throw err;
  }

  const exec = options.exec ?? defaultExec;

  /**
   * @param {string} remoteCommand
   */
  async function runRemote(remoteCommand) {
    const argv = ["ssh", ...sshOpts(sshCfg, { mux: false }), sshTarget(sshCfg), remoteCommand];
    const result = await exec(argv);
    const detail = `${result.stderr}\n${result.stdout}`.trim();
    if (result.code !== 0 && isConnectionFailure(detail)) {
      throw new SshTransportError(nodeName, detail || "connection failed");
    }
    return result;
  }

  /**
   * @param {string} localPath
   * @param {string} remotePath
   */
  async function upload(localPath, remotePath) {
    const scpOpts = sshOpts(sshCfg, { mux: false }).map((value) => (value === "-p" ? "-P" : value));
    const argv = ["scp", "-q", ...scpOpts, localPath, `${sshTarget(sshCfg)}:${remotePath}`];
    const result = await exec(argv);
    const detail = `${result.stderr}\n${result.stdout}`.trim();
    if (result.code !== 0) {
      if (isConnectionFailure(detail)) {
        throw new SshTransportError(nodeName, detail || "connection failed");
      }
      throw new SshTransportError(nodeName, `upload failed: ${detail || `exit ${result.code}`}`);
    }
  }

  return {
    name: "ssh",

    async read(path) {
      const result = await runRemote(remoteBash(REMOTE_READ_SH, [path]));
      if (result.code !== 0) {
        throw new SshTransportError(nodeName, result.stderr.trim() || `read failed with exit ${result.code}`);
      }
      return parseReadOutput(result.stdout);
    },

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

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

      const current = await this.read(item.destination);
      if (current.exists && current.digest === item.content_digest) {
        return;
      }

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

      const localStaging = join(mkdtempSync(join(tmpdir(), "fleet-ssh-upload-")), "payload");
      writeFileSync(localStaging, stagedBytes);

      try {
        const mkdir = await runRemote(`mkdir -p ${shq(dirname(dest))}`);
        if (mkdir.code !== 0) {
          throw new SshTransportError(nodeName, mkdir.stderr.trim() || "mkdir failed");
        }

        await upload(localStaging, stagingPath);

        const promote = await runRemote(remoteBash(REMOTE_WRITE_SH, [dest, stagingPath, item.content_digest]));
        if (promote.code === 42) {
          throw new SshTransportError(
            nodeName,
            `staged content digest mismatch for ${item.id}: expected ${item.content_digest}`,
          );
        }
        if (promote.code !== 0) {
          await runRemote(remoteBash(REMOTE_CLEANUP_SH, [stagingPath])).catch(() => {});
          throw new SshTransportError(nodeName, promote.stderr.trim() || `promote failed with exit ${promote.code}`);
        }
      } catch (err) {
        await runRemote(remoteBash(REMOTE_CLEANUP_SH, [stagingPath])).catch(() => {});
        throw err;
      } finally {
        unlinkSync(localStaging);
      }
    },

    async unitState(unitName) {
      const enabled = await runRemote(`systemctl --user is-enabled ${shq(unitName)}`);
      const active = await runRemote(`systemctl --user is-active ${shq(unitName)}`);
      return {
        enabled: enabled.code === 0,
        active: active.code === 0 && active.stdout.trim() === "active",
      };
    },

    async gitRef(path) {
      const cmd = [
        `p=${shq(path)}`,
        'if [ -e "$p" ]; then if [ -d "$p" ]; then base="$p"; else base=$(dirname "$p"); fi; else base=$(dirname "$p"); fi',
        'git -C "$base" rev-parse HEAD 2>/dev/null || true',
      ].join("; ");
      const result = await runRemote(cmd);
      const ref = result.stdout.trim();
      return ref || null;
    },
  };
}
