/*
 * Human-friendly value formatting for metric units.
 * Converts raw values like 1048576 bytes → "1 MB", 3661 seconds → "1.02 hr", etc.
 */
const byteUnits = [
    { threshold: 1e15, unit: "PB", divisor: 1e15 },
    { threshold: 1e12, unit: "TB", divisor: 1e12 },
    { threshold: 1e9, unit: "GB", divisor: 1e9 },
    { threshold: 1e6, unit: "MB", divisor: 1e6 },
    { threshold: 1e3, unit: "KB", divisor: 1e3 },
    { threshold: 0, unit: "B", divisor: 1 },
];
const secondUnits = [
    { threshold: 86400, unit: "days", divisor: 86400 },
    { threshold: 3600, unit: "hours", divisor: 3600 },
    { threshold: 60, unit: "min", divisor: 60 },
    { threshold: 1, unit: "sec", divisor: 1 },
    { threshold: 0.001, unit: "ms", divisor: 0.001 },
    { threshold: 0.000001, unit: "µs", divisor: 0.000001 },
    { threshold: 0, unit: "ns", divisor: 0.000000001 },
];
const millisecondUnits = [
    { threshold: 86400000, unit: "days", divisor: 86400000 },
    { threshold: 3600000, unit: "hours", divisor: 3600000 },
    { threshold: 60000, unit: "min", divisor: 60000 },
    { threshold: 1000, unit: "sec", divisor: 1000 },
    { threshold: 1, unit: "ms", divisor: 1 },
    { threshold: 0.001, unit: "µs", divisor: 0.001 },
    { threshold: 0, unit: "ns", divisor: 0.000001 },
];
const microsecondUnits = [
    { threshold: 1e6, unit: "sec", divisor: 1e6 },
    { threshold: 1e3, unit: "ms", divisor: 1e3 },
    { threshold: 1, unit: "µs", divisor: 1 },
    { threshold: 0, unit: "ns", divisor: 0.001 },
];
const nanosecondUnits = [
    { threshold: 1e9, unit: "sec", divisor: 1e9 },
    { threshold: 1e6, unit: "ms", divisor: 1e6 },
    { threshold: 1e3, unit: "µs", divisor: 1e3 },
    { threshold: 0, unit: "ns", divisor: 1 },
];
// Maps common metric unit strings to their scaling table
const unitTableMap = {
    // Byte variants
    bytes: byteUnits,
    byte: byteUnits,
    by: byteUnits,
    b: byteUnits,
    // Second variants
    seconds: secondUnits,
    second: secondUnits,
    sec: secondUnits,
    s: secondUnits,
    // Millisecond variants
    milliseconds: millisecondUnits,
    millisecond: millisecondUnits,
    ms: millisecondUnits,
    // Microsecond variants
    microseconds: microsecondUnits,
    microsecond: microsecondUnits,
    us: microsecondUnits,
    µs: microsecondUnits,
    // Nanosecond variants
    nanoseconds: nanosecondUnits,
    nanosecond: nanosecondUnits,
    ns: nanosecondUnits,
};
/*
 * Maps UCUM / OpenTelemetry unit codes to human-readable unit names.
 * Used for labelling (e.g. badges) where the raw code "By" is hard to read.
 */
const readableUnitMap = {
    // Dimensionless — OpenTelemetry uses "1" for unitless counts
    "1": "",
    // Bytes (decimal)
    by: "Bytes",
    byte: "Bytes",
    bytes: "Bytes",
    b: "Bytes",
    kby: "Kilobytes",
    mby: "Megabytes",
    gby: "Gigabytes",
    tby: "Terabytes",
    pby: "Petabytes",
    // Bytes (binary)
    kiby: "Kibibytes",
    miby: "Mebibytes",
    giby: "Gibibytes",
    tiby: "Tebibytes",
    piby: "Pebibytes",
    // Bits
    bit: "Bits",
    bits: "Bits",
    kbit: "Kilobits",
    mbit: "Megabits",
    gbit: "Gigabits",
    // Time
    ns: "Nanoseconds",
    nanosecond: "Nanoseconds",
    nanoseconds: "Nanoseconds",
    us: "Microseconds",
    µs: "Microseconds",
    microsecond: "Microseconds",
    microseconds: "Microseconds",
    ms: "Milliseconds",
    millisecond: "Milliseconds",
    milliseconds: "Milliseconds",
    s: "Seconds",
    sec: "Seconds",
    second: "Seconds",
    seconds: "Seconds",
    min: "Minutes",
    minute: "Minutes",
    minutes: "Minutes",
    h: "Hours",
    hr: "Hours",
    hour: "Hours",
    hours: "Hours",
    d: "Days",
    day: "Days",
    days: "Days",
    // Percent
    "%": "Percent",
    percent: "Percent",
    // Frequency
    hz: "Hertz",
    khz: "Kilohertz",
    mhz: "Megahertz",
    ghz: "Gigahertz",
    // Temperature
    cel: "Celsius",
    "[degf]": "Fahrenheit",
    k: "Kelvin",
    // Electrical
    v: "Volts",
    a: "Amperes",
    w: "Watts",
    kw: "Kilowatts",
    j: "Joules",
};
function normalizeUnit(unit) {
    return unit.trim().toLowerCase();
}
function getReadableUnitPart(unit) {
    const normalized = normalizeUnit(unit);
    if (readableUnitMap[normalized] !== undefined) {
        return readableUnitMap[normalized];
    }
    return unit;
}
function formatWithThresholds(value, thresholds) {
    var _a;
    const absValue = Math.abs(value);
    /*
     * 0 is a tricky edge case for descending-threshold tables: the table
     * always has a `threshold: 0` entry at the bottom (the smallest sub-
     * unit — ns for seconds, B for bytes), and `0 >= 0` short-circuits the
     * loop on that entry. The result is that a 0-value `seconds` metric
     * renders as "0 ns" instead of "0 sec". Use the natural unit
     * (divisor === 1) when the value is exactly 0 so the suffix matches
     * the input scale.
     */
    if (absValue === 0) {
        const natural = (_a = thresholds.find((t) => {
            return t.divisor === 1;
        })) !== null && _a !== void 0 ? _a : thresholds[thresholds.length - 1];
        return {
            value: "0",
            unit: natural.unit,
            formatted: `0 ${natural.unit}`,
        };
    }
    for (const t of thresholds) {
        if (absValue >= t.threshold) {
            const scaled = value / t.divisor;
            const formatted = formatNumber(scaled);
            return {
                value: formatted,
                unit: t.unit,
                formatted: `${formatted} ${t.unit}`,
            };
        }
    }
    // Fallback: use last threshold
    const last = thresholds[thresholds.length - 1];
    const scaled = value / last.divisor;
    const formatted = formatNumber(scaled);
    return {
        value: formatted,
        unit: last.unit,
        formatted: `${formatted} ${last.unit}`,
    };
}
/*
 * Abbreviates large numbers (>= 1000) with K/M/B/T/P suffixes so chart
 * axis labels stay narrow. Counter-style metrics frequently reach into
 * the billions and the raw digit string overflows the y-axis tick width.
 *
 * Always renders 2 decimal places of the scaled value, then trims trailing
 * zeros — so neighbouring ticks like 119.20B / 119.25B remain distinct
 * (the user can see the counter rising) while round values stay clean
 * (5K, not 5.00K).
 */
function formatLargeNumber(value) {
    const absValue = Math.abs(value);
    let scaled;
    let suffix;
    if (absValue >= 1e15) {
        scaled = value / 1e15;
        suffix = "P";
    }
    else if (absValue >= 1e12) {
        scaled = value / 1e12;
        suffix = "T";
    }
    else if (absValue >= 1e9) {
        scaled = value / 1e9;
        suffix = "B";
    }
    else if (absValue >= 1e6) {
        scaled = value / 1e6;
        suffix = "M";
    }
    else {
        scaled = value / 1e3;
        suffix = "K";
    }
    let formatted = scaled.toFixed(2);
    if (formatted.includes(".")) {
        formatted = formatted.replace(/\.?0+$/, "");
    }
    return `${formatted}${suffix}`;
}
function formatNumber(value) {
    if (value === 0) {
        return "0";
    }
    const absValue = Math.abs(value);
    if (absValue >= 1e3) {
        return formatLargeNumber(value);
    }
    if (absValue >= 100) {
        return Math.round(value).toString();
    }
    if (absValue >= 10) {
        return (Math.round(value * 10) / 10).toString();
    }
    return (Math.round(value * 100) / 100).toString();
}
/*
 * Percent values always render with two decimal places. Trailing zeros are
 * preserved (so 100 → "100.00", 2 → "2.00") to keep axis ticks and tooltips
 * visually consistent across high- and low-utilization series.
 */
function formatPercent(value) {
    if (!Number.isFinite(value)) {
        return "0.00";
    }
    return value.toFixed(2);
}
export default class ValueFormatter {
    /*
     * Format a value with a unit into a human-friendly string.
     * e.g. formatValue(1048576, "bytes") → "1 MB"
     * e.g. formatValue(3661, "seconds") → "1.02 hr"
     * e.g. formatValue(42, "%") → "42%"
     * e.g. formatValue(0.25, "1", { metricName: "system.cpu.utilization" }) → "25%"
     */
    static formatValue(value, unit, options) {
        const trimmedUnit = (unit || "").trim();
        /*
         * OTel/UCUM ratio metrics carry a [0, 1] fraction with unit "1". Render
         * them as a percentage so chart axes / thresholds read 25.00% instead
         * of 0.25. Percent values always render with two decimal places so
         * low-utilization series (e.g. 2.04%) and exact values (e.g. 100.00%)
         * read consistently across axis ticks and tooltips.
         */
        if (trimmedUnit === "1" &&
            ValueFormatter.isFractionMetric(options === null || options === void 0 ? void 0 : options.metricName)) {
            return `${formatPercent(value * 100)}%`;
        }
        // OpenTelemetry uses "1" as the dimensionless marker — render as a bare number.
        if (trimmedUnit === "" || trimmedUnit === "1") {
            return formatNumber(value);
        }
        /*
         * "%" and its spelled-out / casual variants all render inline with no
         * separating space — `25.00%`, never `25 Percent`.
         */
        if (ValueFormatter.isPercentUnit(trimmedUnit)) {
            return `${formatPercent(value)}%`;
        }
        /*
         * UCUM annotation-only units (e.g. "{thread}", "{packets}", "{errors}")
         * are dimensionless — the brace contents are descriptive only and the
         * value itself is the count. Render the bare number so chart y-axis
         * labels stay short and don't wrap into the plot area.
         */
        const annotationOnlyUnitPattern = /^\{[^{}]*\}$/;
        if (annotationOnlyUnitPattern.test(trimmedUnit)) {
            return formatNumber(value);
        }
        const normalizedUnit = normalizeUnit(unit);
        const thresholds = unitTableMap[normalizedUnit];
        if (thresholds) {
            return formatWithThresholds(value, thresholds).formatted;
        }
        /*
         * Compound rate units like "By/s" or "ms/s". Scale the numerator with
         * its threshold table when we know one (so 1500000 By/s renders as
         * "1.5 MB/s" rather than "1500000 Bytes per Second"), and keep a
         * compact denominator. Falls back to the verbose readable form for
         * unrecognized compound units.
         */
        if (trimmedUnit.includes("/")) {
            const [numeratorRaw, ...denominatorParts] = trimmedUnit.split("/");
            const denominator = denominatorParts.join("/").trim();
            if (numeratorRaw && denominator) {
                const numeratorThresholds = unitTableMap[normalizeUnit(numeratorRaw)];
                if (numeratorThresholds) {
                    const scaled = formatWithThresholds(value, numeratorThresholds);
                    return `${scaled.value} ${scaled.unit}/${denominator}`;
                }
            }
        }
        // Unknown unit — format number and show the readable unit name when we have one.
        return `${formatNumber(value)} ${ValueFormatter.getReadableUnit(unit)}`;
    }
    /*
     * UCUM canonical is "%" but exporters (especially custom ones) often
     * write "percent", "percentage", or "pct". Treat them all the same so a
     * user adding a percent metric never sees the value rendered as
     * "25 Percent" with a stray space.
     */
    static isPercentUnit(unit) {
        if (!unit) {
            return false;
        }
        const normalized = unit.trim().toLowerCase();
        return (normalized === "%" ||
            normalized === "percent" ||
            normalized === "percentage" ||
            normalized === "pct");
    }
    /*
     * Returns true when the metric name follows a convention that signals a
     * [0, 1] ratio. Pair with unit "1" to render values as percentages.
     *
     * Conventions covered:
     *   - OTel `.utilization` (system.cpu.utilization, k8s.node.cpu.utilization, …)
     *   - OTel `.ratio` and `.fraction` (db.client.connection.usage_ratio, …)
     *   - Prometheus-style `_utilization` / `_ratio` / `_fraction` suffixes
     *   - Plain `_percent` / `.percent` / `_percentage` / `.percentage` names
     *
     * Adding a new suffix is one regex edit — no per-metric allowlist.
     */
    static isFractionMetric(metricName) {
        if (!metricName) {
            return false;
        }
        const fractionMetricSuffixRegex = /[._](utilization|ratio|fraction|percent|percentage)$/i;
        return fractionMetricSuffixRegex.test(metricName);
    }
    /*
     * Direction-of-goodness heuristic for trend coloring. Returns true when a
     * rising value on this metric should be shown as bad (red ↑) and a falling
     * value as good (green ↓) — the opposite of the default "up = good".
     *
     * Caller is responsible for the inversion; this method only classifies.
     * Conservative by default: when the metric name carries no signal we
     * return `false` so generic counters (request rate, network I/O, span
     * count) keep the up = good colour scheme. Explicit "higher is better"
     * tokens (uptime/availability/online/ready/healthy/available/success/
     * passed) take precedence — without that allowlist `oneuptime.monitor.
     * online` would match nothing and incorrectly fall through; the suffix
     * test then catches incident counts, error rates, MTTR/MTTA, response
     * times, CPU/memory usage, restarts, pressure, queue backlogs, etc.
     *
     * Adding a metric here is a single regex edit. If a dashboard widget
     * really needs to override this for a specific case, callers can set
     * `higherIsBetter` explicitly on the trend display.
     */
    static isHigherWorseMetric(metricName) {
        if (!metricName) {
            return false;
        }
        const lower = metricName.toLowerCase();
        const higherIsBetter = /\b(uptime|availability|online|ready|healthy|available|success|passed|ok|alive|up)\b/;
        if (higherIsBetter.test(lower)) {
            return false;
        }
        const higherIsWorse = /(error|fail(?:ure|ed)?|incident|exception|crash|restart|timeout|dropped|aborted?|reject(?:ed)?|mttr|mtta|latenc(?:y|ies)|duration|[._-]time\b|time[._-]to[._-]|usage|utilization|pressure|unresolved|pending|backlog|lag|delay|saturation|throttl|stall|missed?)/;
        return higherIsWorse.test(lower);
    }
    // Check if a unit is one we can auto-scale (bytes, seconds, etc.)
    static isScalableUnit(unit) {
        if (!unit || unit.trim() === "") {
            return false;
        }
        return unitTableMap[normalizeUnit(unit)] !== undefined;
    }
    /*
     * Convert a UCUM / OpenTelemetry unit code into a human-readable name.
     * e.g. "By" → "Bytes", "s" → "Seconds", "By/s" → "Bytes per Second",
     * "1" → "" (dimensionless). Falls back to the original string when unknown.
     * When `metricName` ends in `.utilization` and the unit is "1", returns
     * "Percent" so axis legends and badges match the formatted values.
     */
    static getReadableUnit(unit, options) {
        const trimmed = (unit || "").trim();
        if (trimmed === "1" &&
            ValueFormatter.isFractionMetric(options === null || options === void 0 ? void 0 : options.metricName)) {
            return "Percent";
        }
        if (trimmed === "" || trimmed === "1") {
            return "";
        }
        if (ValueFormatter.isPercentUnit(trimmed)) {
            return "Percent";
        }
        /*
         * UCUM annotation-only units (e.g. "{thread}", "{packets}") are
         * dimensionless. Render the inner word with a capital first letter
         * so badges read "Threads" / "Packets" instead of "{thread}".
         */
        const annotationMatch = trimmed.match(/^\{([^{}]+)\}$/);
        if (annotationMatch) {
            const inner = annotationMatch[1];
            return inner.charAt(0).toUpperCase() + inner.slice(1);
        }
        // Handle compound rate units like "By/s" → "Bytes per Second"
        if (unit.includes("/")) {
            const parts = unit.split("/");
            const readableParts = parts.map((part) => {
                return getReadableUnitPart(part);
            });
            // Singularize the denominator ("Seconds" → "Second") for nicer rate reading.
            const [numerator, ...denominators] = readableParts;
            const denominator = denominators
                .map((d) => {
                return d.endsWith("s") ? d.slice(0, -1) : d;
            })
                .join(" per ");
            return `${numerator} per ${denominator}`;
        }
        return getReadableUnitPart(unit);
    }
}
//# sourceMappingURL=ValueFormatter.js.map