// Reads and formats disk-space snapshots and warnings.
import fs from "node:fs";
import path from "node:path";

const LOW_DISK_SPACE_WARNING_THRESHOLD_BYTES = 1024 * 1024 * 1024;

type DiskSpaceSnapshot = {
  targetPath: string;
  checkedPath: string;
  availableBytes: number;
  totalBytes: number | null;
};

function finiteNonNegativeNumber(value: unknown): number | null {
  const numberValue = Number(value);
  return Number.isFinite(numberValue) && numberValue >= 0 ? numberValue : null;
}

function findExistingDiskSpacePath(targetPath: string): string | null {
  let current = path.resolve(targetPath);
  while (true) {
    try {
      const stats = fs.statSync(current);
      return stats.isDirectory() ? current : path.dirname(current);
    } catch {
      const parent = path.dirname(current);
      if (parent === current) {
        return null;
      }
      current = parent;
    }
  }
}

/** Reads available bytes for the volume containing a target path when statfs is available. */
export function tryReadDiskSpace(targetPath: string): DiskSpaceSnapshot | null {
  if (typeof fs.statfsSync !== "function") {
    return null;
  }
  // Install/update targets may not exist yet; statfs needs the nearest existing
  // ancestor to identify the backing volume.
  const checkedPath = findExistingDiskSpacePath(targetPath);
  if (!checkedPath) {
    return null;
  }
  try {
    const stats = fs.statfsSync(checkedPath);
    const blockSize = finiteNonNegativeNumber(stats.bsize);
    const availableBlocks = finiteNonNegativeNumber(stats.bavail);
    if (blockSize === null || availableBlocks === null) {
      return null;
    }
    const totalBlocks = finiteNonNegativeNumber(stats.blocks);
    return {
      targetPath,
      checkedPath,
      availableBytes: blockSize * availableBlocks,
      totalBytes: totalBlocks === null ? null : blockSize * totalBlocks,
    };
  } catch {
    return null;
  }
}

/** Formats byte counts for compact operator-facing disk-space warnings. */
export function formatDiskSpaceBytes(bytes: number): string {
  const mib = bytes / (1024 * 1024);
  // Round before choosing the unit so a value that rounds up to 1024 MiB is
  // promoted to "1.0 GiB" instead of printing the impossible "1024 MiB".
  const roundedMib = Math.max(0, Math.round(mib));
  if (roundedMib < 1024) {
    return `${roundedMib} MiB`;
  }
  const gib = mib / 1024;
  return `${gib.toFixed(gib < 10 ? 1 : 0)} GiB`;
}

/** Builds a soft low-disk warning for setup/update flows without failing the operation. */
export function createLowDiskSpaceWarning(params: {
  targetPath: string;
  purpose: string;
  thresholdBytes?: number;
}): string | null {
  const thresholdBytes = params.thresholdBytes ?? LOW_DISK_SPACE_WARNING_THRESHOLD_BYTES;
  const snapshot = tryReadDiskSpace(params.targetPath);
  if (!snapshot || snapshot.availableBytes >= thresholdBytes) {
    return null;
  }
  const location =
    path.resolve(snapshot.targetPath) === path.resolve(snapshot.checkedPath)
      ? snapshot.checkedPath
      : `${snapshot.targetPath} (volume checked at ${snapshot.checkedPath})`;
  return `Low disk space near ${location}: ${formatDiskSpaceBytes(snapshot.availableBytes)} available; ${params.purpose} may fail.`;
}
