function assertSupportedNode() {
  const version = process.versions && process.versions.node ? process.versions.node : "unknown";
  const major = Number.parseInt(String(version).split(".")[0], 10);
  if (Number.isNaN(major) || major < 18) {
    process.stderr.write(
      "codewhale: Node.js 18 or newer is required for npm installation. " +
      `Current Node.js version is ${version}. ` +
      "Please upgrade Node.js and rerun `npm install -g codewhale`.\n",
    );
    process.exit(1);
  }
}

assertSupportedNode();

const fs = require("fs");
const https = require("https");
const http = require("http");
const net = require("net");
const tls = require("tls");
const crypto = require("crypto");
const { URL } = require("url");
const { mkdir, chmod, stat, rename, readFile, unlink, writeFile } = fs.promises;
const { createWriteStream } = fs;
const path = require("path");

const {
  checksumManifestUrl,
  detectBinaryNames,
  releaseAssetUrl,
  releaseBinaryDirectory,
} = require("./artifacts");
const { preflightGlibc } = require("./preflight-glibc");
const pkg = require("../package.json");

const DEFAULT_TIMEOUT_MS = 300_000; // 5 minutes per attempt
const DEFAULT_STALL_MS = 30_000; // abort if no bytes for 30s
const OPTIONAL_TIMEOUT_MS = 15_000; // fail fast during optional npm postinstall
const OPTIONAL_STALL_MS = 5_000; // avoid long hangs when install can recover on first run
const MAX_ATTEMPTS = 5;
const OPTIONAL_MAX_ATTEMPTS = 1; // runtime keeps the full retry budget on first launch
const BASE_BACKOFF_MS = 1_000;

const RETRYABLE_NET_CODES = new Set([
  "ECONNRESET",
  "ECONNREFUSED",
  "EDOWNLOADTIMEOUT",
  "ETIMEDOUT",
  "EAI_AGAIN",
  "ENOTFOUND",
  "ENETUNREACH",
  "EHOSTUNREACH",
  "EPIPE",
  "ECONNABORTED",
]);

class NonRetryableError extends Error {
  constructor(message) {
    super(message);
    this.name = "NonRetryableError";
    this.nonRetryable = true;
  }
}

class HttpStatusError extends Error {
  constructor(status, url) {
    super(`Request failed with status ${status}: ${url}`);
    this.name = "HttpStatusError";
    this.status = status;
  }
}

class DownloadTimeoutError extends Error {
  constructor(message) {
    super(message);
    this.name = "DownloadTimeoutError";
    this.code = "EDOWNLOADTIMEOUT";
  }
}

function resolvePackageVersion() {
  const configuredVersion =
    process.env.DEEPSEEK_TUI_VERSION ||
    process.env.DEEPSEEK_VERSION ||
    pkg.deepseekBinaryVersion ||
    pkg.version;
  return String(configuredVersion).trim();
}

function resolveRepo() {
  return process.env.DEEPSEEK_TUI_GITHUB_REPO || process.env.DEEPSEEK_GITHUB_REPO || "Hmbown/CodeWhale";
}

function isOptionalInstall(argv = process.argv.slice(2), env = process.env) {
  return (
    argv.includes("--optional") ||
    env.DEEPSEEK_TUI_OPTIONAL_INSTALL === "1" ||
    env.DEEPSEEK_OPTIONAL_INSTALL === "1"
  );
}

function isInstallContext(context) {
  return context === "install";
}

function isPnpmUserAgent(env = process.env) {
  return String(env.npm_config_user_agent || "").toLowerCase().includes("pnpm/");
}

function shouldSkipOptionalPostinstall(
  context,
  argv = process.argv.slice(2),
  env = process.env,
) {
  return isInstallContext(context) && isOptionalInstall(argv, env) && isPnpmUserAgent(env);
}

// Optional install only relaxes npm postinstall behavior. Runtime downloads
// keep the normal retry/timeout budget so first-run recovery stays resilient.
function defaultTimeoutMs(context = "runtime", env = process.env) {
  return isInstallContext(context) && isOptionalInstall(undefined, env)
    ? OPTIONAL_TIMEOUT_MS
    : DEFAULT_TIMEOUT_MS;
}

function defaultStallMs(context = "runtime", env = process.env) {
  return isInstallContext(context) && isOptionalInstall(undefined, env)
    ? OPTIONAL_STALL_MS
    : DEFAULT_STALL_MS;
}

function maxAttempts(context = "runtime", env = process.env) {
  return isInstallContext(context) && isOptionalInstall(undefined, env)
    ? OPTIONAL_MAX_ATTEMPTS
    : MAX_ATTEMPTS;
}

function binaryPaths() {
  const { codewhale, tui } = detectBinaryNames();
  const releaseDir = releaseBinaryDirectory();
  return {
    codewhale: {
      asset: codewhale,
      target: path.join(releaseDir, process.platform === "win32" ? "codewhale.exe" : "codewhale"),
    },
    tui: {
      asset: tui,
      target: path.join(releaseDir, process.platform === "win32" ? "codewhale-tui.exe" : "codewhale-tui"),
    },
  };
}

// ────────────────────────────────────────────────────────────────────────────
// Logging / progress
// ────────────────────────────────────────────────────────────────────────────

function isQuietInstall() {
  if (process.env.DEEPSEEK_TUI_QUIET_INSTALL === "1") {
    return true;
  }
  const level = (process.env.npm_config_loglevel || "").toLowerCase();
  return level === "silent" || level === "error";
}

function logInfo(message) {
  if (isQuietInstall()) {
    return;
  }
  process.stderr.write(`codewhale: ${message}\n`);
}

function installFailureHint(error) {
  const message = error && error.message ? String(error.message) : "";
  const code = error && error.code ? String(error.code) : "";
  const releaseBase =
    process.env.DEEPSEEK_TUI_RELEASE_BASE_URL ||
    process.env.DEEPSEEK_RELEASE_BASE_URL;
  const networkMarkers = [
    "github.com",
    "ENOTFOUND",
    "EAI_AGAIN",
    "ETIMEDOUT",
    "ECONNRESET",
    "ENETUNREACH",
    "EHOSTUNREACH",
    "EDOWNLOADTIMEOUT",
  ];
  const looksLikeNetworkDownloadFailure = networkMarkers.some(
    (marker) => message.includes(marker) || code === marker,
  );
  if (!looksLikeNetworkDownloadFailure) {
    return "";
  }

  if (releaseBase) {
    return [
      "codewhale install hint:",
      `  DEEPSEEK_TUI_RELEASE_BASE_URL is set to ${releaseBase}`,
      "  Verify that this directory contains codewhale-artifacts-sha256.txt",
      "  plus the codewhale/codewhale-tui binary assets for your platform.",
    ].join("\n");
  }

  return [
    "codewhale install hint:",
    "  The npm package downloads prebuilt binaries from GitHub Releases.",
    "  If GitHub is unavailable on this network, mirror the release assets and set:",
    "    DEEPSEEK_TUI_RELEASE_BASE_URL=https://<mirror>/<release-asset-directory>/",
    "  The directory must contain codewhale-artifacts-sha256.txt and the platform binaries.",
    "  See docs/INSTALL.md#npm-binary-download-times-out.",
  ].join("\n");
}

function envInt(name, fallback) {
  const raw = process.env[name];
  if (!raw) {
    return fallback;
  }
  const parsed = Number.parseInt(String(raw).trim(), 10);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return fallback;
  }
  return parsed;
}

function downloadTimeoutMs(context = "runtime") {
  return envInt(
    "DEEPSEEK_TUI_DOWNLOAD_TIMEOUT_MS",
    envInt("DEEPSEEK_DOWNLOAD_TIMEOUT_MS", defaultTimeoutMs(context)),
  );
}

function downloadStallMs(context = "runtime") {
  return envInt(
    "DEEPSEEK_TUI_DOWNLOAD_STALL_MS",
    envInt("DEEPSEEK_DOWNLOAD_STALL_MS", defaultStallMs(context)),
  );
}

function formatMb(bytes) {
  return (bytes / (1024 * 1024)).toFixed(0);
}

function createProgressReporter(assetName, totalBytes) {
  if (isQuietInstall()) {
    return { onChunk: () => {}, finish: () => {} };
  }
  const isTty = !!process.stderr.isTTY;
  const interactive = isTty;
  const tickBytes = interactive ? 1 * 1024 * 1024 : 5 * 1024 * 1024;
  const tickMs = 2_000;

  let received = 0;
  let lastBytesPrinted = 0;
  let lastTimePrinted = 0;
  let everPrinted = false;

  const render = (final) => {
    if (totalBytes && totalBytes > 0) {
      const pct = Math.min(100, Math.round((received / totalBytes) * 100));
      const line = `codewhale: downloading ${assetName}: ${formatMb(received)} / ${formatMb(totalBytes)} MB (${pct}%)`;
      if (interactive) {
        process.stderr.write(`${line}\r`);
      } else {
        process.stderr.write(`${line}\n`);
      }
    } else {
      const line = `codewhale: downloading ${assetName}: ${formatMb(received)} MB downloaded`;
      if (interactive) {
        process.stderr.write(`${line}\r`);
      } else {
        process.stderr.write(`${line}\n`);
      }
    }
    everPrinted = true;
    lastBytesPrinted = received;
    lastTimePrinted = Date.now();
  };

  return {
    onChunk(chunkLen) {
      received += chunkLen;
      const now = Date.now();
      if (
        received - lastBytesPrinted >= tickBytes ||
        (interactive && now - lastTimePrinted >= tickMs)
      ) {
        render(false);
      }
    },
    finish() {
      // Final line — always render once.
      render(true);
      if (interactive && everPrinted) {
        // Move past the carriage-return line and emit a "done" footer.
        process.stderr.write("\n");
      }
      process.stderr.write(`codewhale: ${assetName} ... done.\n`);
    },
  };
}

// ────────────────────────────────────────────────────────────────────────────
// Proxy support (HTTPS_PROXY / HTTP_PROXY / NO_PROXY) — pure Node, CONNECT
// tunnel + TLS upgrade for HTTPS targets.
// ────────────────────────────────────────────────────────────────────────────

function getProxyUrl(targetUrl) {
  const isHttps = targetUrl.protocol === "https:";
  const candidates = isHttps
    ? ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"]
    : ["HTTP_PROXY", "http_proxy"];
  for (const name of candidates) {
    const raw = process.env[name];
    if (raw && String(raw).trim() !== "") {
      return String(raw).trim();
    }
  }
  return null;
}

function shouldBypassProxy(host) {
  const raw = process.env.NO_PROXY || process.env.no_proxy;
  if (!raw) {
    return false;
  }
  const lower = String(host).toLowerCase();
  for (const part of String(raw).split(",")) {
    const entry = part.trim().toLowerCase();
    if (!entry) {
      continue;
    }
    if (entry === "*") {
      return true;
    }
    // Strip leading dot and any explicit port.
    const stripped = entry.replace(/^\./, "").replace(/:.*$/, "");
    if (!stripped) {
      continue;
    }
    if (lower === stripped || lower.endsWith(`.${stripped}`)) {
      return true;
    }
  }
  return false;
}

function parseProxy(proxyStr) {
  // Accept "http://user:pass@host:port" and bare "host:port".
  const normalized = /^[a-z][a-z0-9+\-.]*:\/\//i.test(proxyStr)
    ? proxyStr
    : `http://${proxyStr}`;
  const u = new URL(normalized);
  const port = u.port
    ? Number.parseInt(u.port, 10)
    : u.protocol === "https:"
      ? 443
      : 80;
  let auth = null;
  if (u.username) {
    const user = decodeURIComponent(u.username);
    const pass = u.password ? decodeURIComponent(u.password) : "";
    auth = Buffer.from(`${user}:${pass}`).toString("base64");
  }
  return {
    protocol: u.protocol,
    host: u.hostname,
    port,
    auth,
    raw: proxyStr,
  };
}

function connectThroughProxy(proxy, targetHost, targetPort, timeoutMs) {
  return new Promise((resolve, reject) => {
    const socket = net.connect({ host: proxy.host, port: proxy.port });
    let settled = false;
    const fail = (err) => {
      if (settled) return;
      settled = true;
      try {
        socket.destroy();
      } catch {
        // ignore
      }
      reject(err);
    };

    const timer = timeoutMs > 0
      ? setTimeout(() => fail(new DownloadTimeoutError(
          `proxy CONNECT to ${proxy.host}:${proxy.port} timed out after ${timeoutMs} ms`,
        )), timeoutMs)
      : null;

    socket.once("error", (err) => {
      if (timer) clearTimeout(timer);
      // Surface proxy host so the user can fix it.
      const wrapped = new Error(
        `proxy connection failed (${proxy.host}:${proxy.port}): ${err.message}`,
      );
      wrapped.code = err.code;
      fail(wrapped);
    });

    socket.once("connect", () => {
      const lines = [
        `CONNECT ${targetHost}:${targetPort} HTTP/1.1`,
        `Host: ${targetHost}:${targetPort}`,
        "User-Agent: codewhale-installer",
        "Proxy-Connection: keep-alive",
      ];
      if (proxy.auth) {
        lines.push(`Proxy-Authorization: Basic ${proxy.auth}`);
      }
      const req = `${lines.join("\r\n")}\r\n\r\n`;

      let buf = Buffer.alloc(0);
      const onData = (chunk) => {
        buf = Buffer.concat([buf, chunk]);
        const idx = buf.indexOf("\r\n\r\n");
        if (idx === -1) {
          if (buf.length > 16 * 1024) {
            socket.removeListener("data", onData);
            fail(new Error(
              `proxy ${proxy.host}:${proxy.port} returned an oversized response header`,
            ));
          }
          return;
        }
        socket.removeListener("data", onData);
        const head = buf.slice(0, idx).toString("utf8");
        const firstLine = head.split(/\r?\n/, 1)[0] || "";
        const m = firstLine.match(/^HTTP\/\d\.\d\s+(\d{3})/);
        if (!m) {
          fail(new Error(`proxy ${proxy.host}:${proxy.port} returned invalid CONNECT reply: ${firstLine}`));
          return;
        }
        const code = Number.parseInt(m[1], 10);
        if (code !== 200) {
          fail(new Error(
            `proxy ${proxy.host}:${proxy.port} refused CONNECT to ${targetHost}:${targetPort}: HTTP ${code}`,
          ));
          return;
        }
        if (timer) clearTimeout(timer);
        if (settled) return;
        settled = true;
        // Any bytes past the header belong to the tunneled stream — but in
        // practice CONNECT 200 has no body; if it did, we'd lose those bytes
        // here. Keep it simple: trust well-behaved proxies.
        resolve(socket);
      };
      socket.on("data", onData);
      socket.write(req, "utf8");
    });
  });
}

// ────────────────────────────────────────────────────────────────────────────
// HTTP request with timeout, stall detection, and proxy support.
// ────────────────────────────────────────────────────────────────────────────

function httpRequest(rawUrl, opts = {}) {
  const context =
    opts.context === undefined || opts.context === null ? "runtime" : opts.context;
  const totalTimeoutMs =
    opts.totalTimeoutMs === undefined || opts.totalTimeoutMs === null
      ? downloadTimeoutMs(context)
      : opts.totalTimeoutMs;
  const stallMs =
    opts.stallMs === undefined || opts.stallMs === null
      ? downloadStallMs(context)
      : opts.stallMs;

  return new Promise((resolve, reject) => {
    let url;
    try {
      url = new URL(rawUrl);
    } catch (err) {
      reject(new NonRetryableError(`Invalid URL: ${rawUrl} (${err.message})`));
      return;
    }
    if (url.protocol !== "https:" && url.protocol !== "http:") {
      reject(new NonRetryableError(`Unsupported protocol: ${url.protocol}`));
      return;
    }

    const proxyStr = !shouldBypassProxy(url.hostname) ? getProxyUrl(url) : null;
    const isHttps = url.protocol === "https:";
    const port = url.port
      ? Number.parseInt(url.port, 10)
      : isHttps
        ? 443
        : 80;

    let totalTimer = null;
    let stallTimer = null;
    let settled = false;
    let req = null;
    let res = null;

    const cleanup = () => {
      if (totalTimer) {
        clearTimeout(totalTimer);
        totalTimer = null;
      }
      if (stallTimer) {
        clearTimeout(stallTimer);
        stallTimer = null;
      }
    };

    const fail = (err) => {
      if (settled) return;
      settled = true;
      cleanup();
      try {
        if (req && !req.destroyed) req.destroy();
      } catch {
        // ignore
      }
      try {
        if (res && !res.destroyed) res.destroy();
      } catch {
        // ignore
      }
      reject(err);
    };

    if (totalTimeoutMs > 0) {
      totalTimer = setTimeout(() => {
        fail(new DownloadTimeoutError(
          `download exceeded total timeout of ${totalTimeoutMs} ms ` +
          `(set DEEPSEEK_TUI_DOWNLOAD_TIMEOUT_MS to raise it; current stall budget is ${stallMs} ms)`,
        ));
      }, totalTimeoutMs);
    }

    const armStallTimer = () => {
      if (stallMs <= 0) return;
      if (stallTimer) clearTimeout(stallTimer);
      stallTimer = setTimeout(() => {
        fail(new DownloadTimeoutError(
          `download stalled — no bytes received for ${stallMs} ms ` +
          `(set DEEPSEEK_TUI_DOWNLOAD_STALL_MS to raise it; total budget is ${totalTimeoutMs} ms)`,
        ));
      }, stallMs);
    };

    const launch = (socket) => {
      const reqOptions = {
        method: "GET",
        host: url.hostname,
        port,
        path: `${url.pathname}${url.search || ""}`,
        headers: {
          Host: url.host,
          "User-Agent": "codewhale-installer",
          Accept: "*/*",
          Connection: "close",
        },
      };
      if (socket) {
        reqOptions.createConnection = () => socket;
        if (isHttps) {
          // Wrap raw TCP socket from CONNECT in TLS.
          const tlsSocket = tls.connect({
            socket,
            servername: url.hostname,
            ALPNProtocols: ["http/1.1"],
          });
          tlsSocket.once("error", (err) => fail(err));
          reqOptions.createConnection = () => tlsSocket;
        }
      }
      const client = isHttps ? https : http;
      try {
        req = client.request(reqOptions, (response) => {
          res = response;
          armStallTimer();
          response.on("data", () => {
            armStallTimer();
          });
          response.on("end", () => {
            cleanup();
          });
          response.on("error", (err) => fail(err));

          const status = response.statusCode || 0;
          if (status >= 300 && status < 400 && response.headers.location) {
            cleanup();
            settled = true;
            response.resume();
            resolve({ redirect: response.headers.location, response: null });
            return;
          }
          if (status < 200 || status >= 300) {
            const err = new HttpStatusError(status, rawUrl);
            // 4xx: non-retryable; 5xx: retryable.
            if (status >= 400 && status < 500) {
              err.nonRetryable = true;
            }
            fail(err);
            return;
          }
          if (settled) return;
          settled = true;
          // Hand the live response stream to the caller.
          resolve({ redirect: null, response });
        });
        req.once("error", (err) => fail(err));
        req.once("socket", (s) => {
          // Belt-and-suspenders: surface socket-level errors quickly.
          s.once("error", (err) => fail(err));
        });
        req.end();
      } catch (err) {
        fail(err);
      }
    };

    if (proxyStr) {
      let proxy;
      try {
        proxy = parseProxy(proxyStr);
      } catch (err) {
        fail(new NonRetryableError(
          `Invalid proxy URL "${proxyStr}": ${err.message}`,
        ));
        return;
      }
      if (!isHttps) {
        // Plain HTTP through proxy — send absolute URI, no CONNECT.
        const client = http;
        try {
          req = client.request(
            {
              host: proxy.host,
              port: proxy.port,
              method: "GET",
              path: rawUrl,
              headers: {
                Host: url.host,
                "User-Agent": "codewhale-installer",
                Accept: "*/*",
                Connection: "close",
                ...(proxy.auth ? { "Proxy-Authorization": `Basic ${proxy.auth}` } : {}),
              },
            },
            (response) => {
              res = response;
              armStallTimer();
              response.on("data", () => armStallTimer());
              response.on("end", () => cleanup());
              response.on("error", (err) => fail(err));
              const status = response.statusCode || 0;
              if (status >= 300 && status < 400 && response.headers.location) {
                cleanup();
                settled = true;
                response.resume();
                resolve({ redirect: response.headers.location, response: null });
                return;
              }
              if (status < 200 || status >= 300) {
                const err = new HttpStatusError(status, rawUrl);
                if (status >= 400 && status < 500) err.nonRetryable = true;
                fail(err);
                return;
              }
              if (settled) return;
              settled = true;
              resolve({ redirect: null, response });
            },
          );
          req.once("error", (err) => fail(err));
          req.end();
        } catch (err) {
          fail(err);
        }
        return;
      }

      // HTTPS through proxy: CONNECT tunnel + TLS upgrade.
      connectThroughProxy(proxy, url.hostname, port, Math.max(stallMs, 5_000))
        .then((tcpSocket) => {
          if (settled) {
            try { tcpSocket.destroy(); } catch { /* ignore */ }
            return;
          }
          const tlsSocket = tls.connect({
            socket: tcpSocket,
            servername: url.hostname,
            ALPNProtocols: ["http/1.1"],
          });
          tlsSocket.once("error", (err) => fail(err));
          tlsSocket.once("secureConnect", () => {
            if (settled) {
              try { tlsSocket.destroy(); } catch { /* ignore */ }
              return;
            }
            const reqOptions = {
              method: "GET",
              createConnection: () => tlsSocket,
              path: `${url.pathname}${url.search || ""}`,
              headers: {
                Host: url.host,
                "User-Agent": "codewhale-installer",
                Accept: "*/*",
                Connection: "close",
              },
            };
            try {
              req = https.request(reqOptions, (response) => {
                res = response;
                armStallTimer();
                response.on("data", () => armStallTimer());
                response.on("end", () => cleanup());
                response.on("error", (err) => fail(err));
                const status = response.statusCode || 0;
                if (status >= 300 && status < 400 && response.headers.location) {
                  cleanup();
                  settled = true;
                  response.resume();
                  resolve({ redirect: response.headers.location, response: null });
                  return;
                }
                if (status < 200 || status >= 300) {
                  const err = new HttpStatusError(status, rawUrl);
                  if (status >= 400 && status < 500) err.nonRetryable = true;
                  fail(err);
                  return;
                }
                if (settled) return;
                settled = true;
                resolve({ redirect: null, response });
              });
              req.once("error", (err) => fail(err));
              req.end();
            } catch (err) {
              fail(err);
            }
          });
        })
        .catch((err) => fail(err));
      return;
    }

    // No proxy — direct connection.
    launch(null);
  });
}

// ────────────────────────────────────────────────────────────────────────────
// Retry wrapper
// ────────────────────────────────────────────────────────────────────────────

function isRetryable(err) {
  if (!err) return false;
  if (err.nonRetryable) return false;
  if (err instanceof NonRetryableError) return false;
  if (err instanceof DownloadTimeoutError) return true;
  // withRetry() rethrows a plain Error while preserving name/status, so wrapped
  // HTTP 5xx failures still classify as retryable during optional postinstall.
  if (
    (err instanceof HttpStatusError || err.name === "HttpStatusError") &&
    typeof err.status === "number"
  ) {
    return err.status >= 500;
  }
  if (err.code && RETRYABLE_NET_CODES.has(err.code)) return true;
  // Network-flavored messages we may see without a code.
  const msg = String(err.message || "").toLowerCase();
  if (msg.includes("network") && msg.includes("unreachable")) return true;
  if (msg.includes("socket hang up")) return true;
  if (msg.includes("aborted")) return true;
  return false;
}

function backoffDelay(attempt) {
  // attempt is 1-indexed; first retry waits ~1s.
  const base = BASE_BACKOFF_MS * 2 ** (attempt - 1);
  const jitter = (Math.random() * 0.4 - 0.2) * base; // ±20%
  return Math.max(0, Math.round(base + jitter));
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function withRetry(label, fn, context = "runtime") {
  let lastErr;
  const attemptLimit = maxAttempts(context);
  for (let attempt = 1; attempt <= attemptLimit; attempt++) {
    try {
      return await fn(attempt);
    } catch (err) {
      lastErr = err;
      if (!isRetryable(err) || attempt === attemptLimit) {
        break;
      }
      const wait = backoffDelay(attempt);
      logInfo(
        `${label} failed (attempt ${attempt}/${attemptLimit}): ${err.message}; retrying in ${wait} ms`,
      );
      if (attempt === 1) {
        const hint = installFailureHint(err);
        if (hint) {
          process.stderr.write(`${hint}\n`);
        }
      }
      await sleep(wait);
    }
  }
  const msg = lastErr && lastErr.message ? lastErr.message : String(lastErr);
  const wrapped = new Error(
    `${label} failed after ${attemptLimit} attempt(s): ${msg}`,
  );
  // Preserve retry classification metadata because the install entrypoint uses
  // the wrapped error to decide whether optional postinstall may ignore it.
  if (lastErr && lastErr.code) {
    wrapped.code = lastErr.code;
  }
  if (lastErr && lastErr.name) {
    wrapped.name = lastErr.name;
  }
  if (lastErr && typeof lastErr.status === "number") {
    wrapped.status = lastErr.status;
  }
  if (lastErr && lastErr.nonRetryable) {
    wrapped.nonRetryable = true;
  }
  if (lastErr && lastErr.stack) {
    wrapped.cause = lastErr;
  }
  throw wrapped;
}

// ────────────────────────────────────────────────────────────────────────────
// Public download primitives (now retry + progress aware)
// ────────────────────────────────────────────────────────────────────────────

async function followRedirects(url, opts = {}) {
  const maxRedirects = 10;
  let current = url;
  for (let hop = 0; hop < maxRedirects; hop++) {
    const result = await httpRequest(current, opts);
    if (result.redirect) {
      try {
        current = new URL(result.redirect, current).toString();
      } catch {
        current = result.redirect;
      }
      continue;
    }
    return result;
  }
  throw new NonRetryableError(`too many redirects starting at ${url}`);
}

function streamToFile(response, destination, progress) {
  return new Promise((resolve, reject) => {
    const sink = createWriteStream(destination);
    let done = false;
    const finish = (err) => {
      if (done) return;
      done = true;
      if (err) {
        sink.destroy();
        reject(err);
      } else {
        resolve();
      }
    };
    response.on("data", (chunk) => {
      if (progress) progress.onChunk(chunk.length);
    });
    response.on("error", (err) => finish(err));
    sink.on("error", (err) => finish(err));
    sink.on("finish", () => finish(null));
    response.pipe(sink);
  });
}

async function download(url, destination, options = {}) {
  await mkdir(path.dirname(destination), { recursive: true });
  const assetName = options.assetName || path.basename(destination);
  const context =
    options.context === undefined || options.context === null ? "runtime" : options.context;
  const attemptLimit = maxAttempts(context);
  await withRetry(`download ${assetName}`, async (attempt) => {
    const result = await followRedirects(url, {
      context,
      totalTimeoutMs: downloadTimeoutMs(context),
      stallMs: downloadStallMs(context),
    });
    const response = result.response;
    const lenHeader = response.headers["content-length"];
    const total = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
    const progress = createProgressReporter(assetName, Number.isFinite(total) ? total : 0);
    if (attempt > 1) {
      logInfo(`retry attempt ${attempt}/${attemptLimit} for ${assetName}`);
    }
    try {
      await streamToFile(response, destination, progress);
    } catch (err) {
      // Ensure we don't leave a partial file confusing future attempts.
      try {
        await unlink(destination);
      } catch {
        // ignore
      }
      throw err;
    }
    progress.finish();
  }, context);
}

async function downloadText(url, options = {}) {
  const context =
    options.context === undefined || options.context === null ? "runtime" : options.context;
  return withRetry(`fetch ${url}`, async () => {
    const result = await followRedirects(url, {
      context,
      totalTimeoutMs: downloadTimeoutMs(context),
      stallMs: downloadStallMs(context),
    });
    const response = result.response;
    response.setEncoding("utf8");
    // NOTE: do NOT use `for await (const chunk of response)` here.
    // `httpRequest` attaches a `data` listener on the response to re-arm
    // the stall timer, which puts the stream in flowing mode. The async
    // iterator expects paused mode and will silently miss every chunk —
    // this manifested as an empty checksum manifest in the npm wrapper
    // smoke test ("Checksum manifest is missing <asset>"). Subscribing
    // to `data` events directly stacks alongside the stall listener and
    // both fire per chunk, so we collect the body correctly without
    // disturbing the stall detection.
    return new Promise((resolve, reject) => {
      const chunks = [];
      response.on("data", (chunk) => {
        chunks.push(chunk);
      });
      response.on("end", () => {
        resolve(chunks.join(""));
      });
      response.on("error", reject);
    });
  }, context);
}

async function readLocalVersion(file) {
  return readFile(file, "utf8").catch(() => "");
}

async function fileExists(file) {
  try {
    const result = await stat(file);
    return result.isFile();
  } catch {
    return false;
  }
}

function parseChecksumManifest(text) {
  const checksums = new Map();
  for (const line of text.split(/\r?\n/)) {
    const trimmed = line.trim();
    if (!trimmed) {
      continue;
    }
    const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
    if (!match) {
      throw new Error(`Invalid checksum manifest line: ${trimmed}`);
    }
    checksums.set(match[2], match[1].toLowerCase());
  }
  return checksums;
}

async function sha256File(filePath) {
  const content = await readFile(filePath);
  return crypto.createHash("sha256").update(content).digest("hex");
}

async function verifyChecksum(filePath, assetName, checksums) {
  const expected = checksums.get(assetName);
  if (!expected) {
    throw new NonRetryableError(`Checksum manifest is missing ${assetName}`);
  }
  const actual = await sha256File(filePath);
  if (actual !== expected) {
    // Bytes are corrupted; another fetch is unlikely to help without a fix
    // upstream. Mark non-retryable.
    throw new NonRetryableError(
      `Checksum mismatch for ${assetName}: expected ${expected}, got ${actual}`,
    );
  }
}

async function checksumMatches(filePath, assetName, checksums) {
  const expected = checksums.get(assetName);
  if (!expected) {
    throw new NonRetryableError(`Checksum manifest is missing ${assetName}`);
  }
  const actual = await sha256File(filePath);
  return actual === expected;
}

async function loadChecksums(version, repo, options = {}) {
  return parseChecksumManifest(await downloadText(checksumManifestUrl(version, repo), options));
}

function existingBinaryCandidates(targetPath, assetName) {
  const candidates = [targetPath];
  const assetPath = path.join(path.dirname(targetPath), assetName);
  if (assetPath !== targetPath) {
    candidates.push(assetPath);
  }
  return candidates;
}

async function adoptExistingBinaryIfValid(targetPath, assetName, version, getChecksums, marker) {
  const candidates = [];
  for (const candidate of existingBinaryCandidates(targetPath, assetName)) {
    if (await fileExists(candidate)) {
      candidates.push(candidate);
    }
  }
  if (candidates.length === 0) {
    return false;
  }

  const checksums = await getChecksums();
  for (const candidate of candidates) {
    if (!(await checksumMatches(candidate, assetName, checksums))) {
      continue;
    }
    preflightGlibc(candidate);
    if (candidate !== targetPath) {
      await rename(candidate, targetPath);
    }
    if (process.platform !== "win32") {
      await chmod(targetPath, 0o755);
    }
    await writeFile(marker, String(version), "utf8");
    return true;
  }
  return false;
}

async function ensureBinary(targetPath, assetName, version, repo, getChecksums, options = {}) {
  const marker = `${targetPath}.version`;
  const downloadIfNeeded =
    process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD === "1" || process.env.DEEPSEEK_FORCE_DOWNLOAD === "1";
  if (!downloadIfNeeded) {
    const existing = await fileExists(targetPath);
    if (existing) {
      const markerVersion = await readLocalVersion(marker);
      if (markerVersion === String(version)) {
        return targetPath;
      }
    }
    if (await adoptExistingBinaryIfValid(targetPath, assetName, version, getChecksums, marker)) {
      return targetPath;
    }
  }
  const checksums = await getChecksums();
  const url = releaseAssetUrl(assetName, version, repo);
  const destination = `${targetPath}.${process.pid}.${Date.now()}.download`;
  await download(url, destination, { assetName, context: options.context });
  try {
    await verifyChecksum(destination, assetName, checksums);
    preflightGlibc(destination);
  } catch (error) {
    await unlink(destination).catch(() => {});
    throw error;
  }
  if (process.platform !== "win32") {
    await chmod(destination, 0o755);
  }
  await rename(destination, targetPath);
  await writeFile(marker, String(version), "utf8");
  return targetPath;
}

// Optional install may only downgrade retryable download failures to warnings.
// Unsupported platforms, checksum mismatches, glibc compatibility errors, and
// malformed release metadata must still fail with actionable diagnostics.
function shouldIgnoreInstallFailure(
  context,
  error,
  argv = process.argv.slice(2),
  env = process.env,
) {
  return isInstallContext(context) && isOptionalInstall(argv, env) && isRetryable(error);
}

async function run(options = {}) {
  const context =
    options.context === undefined || options.context === null ? "runtime" : options.context;
  if (process.env.DEEPSEEK_TUI_DISABLE_INSTALL === "1" || process.env.DEEPSEEK_DISABLE_INSTALL === "1") {
    return;
  }
  if (shouldSkipOptionalPostinstall(context)) {
    logInfo(
      "pnpm optional postinstall detected; skipping install-time download. The binary will be checked on first run.",
    );
    return;
  }
  const version = resolvePackageVersion();
  const repo = resolveRepo();
  const paths = binaryPaths();
  const releaseDir = releaseBinaryDirectory();
  await mkdir(releaseDir, { recursive: true });

  let checksumsPromise;
  const getChecksums = () => {
    if (!checksumsPromise) {
      checksumsPromise = loadChecksums(version, repo, { context });
    }
    return checksumsPromise;
  };

  await Promise.all([
    ensureBinary(paths.codewhale.target, paths.codewhale.asset, version, repo, getChecksums, { context }),
    ensureBinary(paths.tui.target, paths.tui.asset, version, repo, getChecksums, { context }),
  ]);
}

async function getBinaryPath(name) {
  await run({ context: "runtime" });
  const paths = binaryPaths();
  if (name === "codewhale") {
    return paths.codewhale.target;
  }
  if (name === "codewhale-tui") {
    return paths.tui.target;
  }
  throw new Error(`Unknown binary: ${name}`);
}

module.exports = {
  getBinaryPath,
  installFailureHint,
  run,
  _internal: {
    isOptionalInstall,
    adoptExistingBinaryIfValid,
    shouldIgnoreInstallFailure,
    shouldSkipOptionalPostinstall,
    defaultTimeoutMs,
    defaultStallMs,
    ensureBinary,
    maxAttempts,
    withRetry,
  },
};

if (require.main === module) {
  run({ context: "install" }).catch((error) => {
    console.error("codewhale install failed:", error.message);
    const hint = installFailureHint(error);
    if (hint) {
      console.error(hint);
    }
    if (shouldIgnoreInstallFailure("install", error)) {
      console.error(
        "Optional install enabled; continuing without a usable binary. The download will be retried on first run.",
      );
      process.exit(0);
    }
    process.exit(1);
  });
}
