/**
 * AirGuidelines — WHO-2021 Air Quality Guideline reference concentrations, the
 * good/fair/poor/very_poor banding rule, and the Ventilation Planner's core decision rule.
 *
 * Pure logic only. No React, no DOM, no fetch — safe to unit test and to call from any
 * surface (panel, page, planner, AirCache's snapshot builder).
 *
 * PORTED FROM the platform demo's `~/Projects/Roz-Weather-API/demo/air/components/
 * guidelines.js` (READ-ONLY reference — never edit that repo from here). The numbers in
 * GUIDELINES and HCHO_BOUNDS are kept NUMERICALLY IDENTICAL to that source on purpose:
 * the /v1/air contract's `now.*.band` field is computed server-side using these exact
 * thresholds, but `hourly.*` ships as raw values only (no per-hour band) — any surface
 * banding an hourly value client-side must reproduce the server's classification exactly
 * or `now.pm2_5.band` and a client-recomputed `hourly.pm2_5[-1]` band could disagree for
 * the same reading. If the platform ever changes these thresholds, this file and the
 * server must change together.
 *
 * WHY WHO-2021 SHORT-TERM, NOT ANNUAL: these are short-term (hourly/daily) readings, not
 * annual means, so the *short-term* guideline concentrations are used — PM2.5 15, PM10 45,
 * O3 100 (8h peak season), NO2 25, SO2 40, CO 4000 (µg/m³). HCHO has no WHO guideline at
 * all; the API labels it a heuristic VOC proxy with its own absolute bounds, never
 * presented as a regulatory limit.
 *
 * NAMESPACED comments: this app has no module system — every top-level `var` in every
 * `<script>` becomes a `window` property (see the WeatherCache.jsx NAMESPACED story,
 * `wc`-prefixed helpers, itself referencing an earlier `wxPanelMap`/`wxPageMap` collision).
 * This file follows the same discipline with `ag`-prefixed internal helpers, so it can
 * never collide with `wx`/`wc`/bare-named helpers in the weather files that load beside it.
 *
 * JUSTIFIED SHARING: unlike per-view mappers (which stay deliberately separate — see
 * WeatherCache.jsx §2.2), banding and ventilation-status are pure thresholds with exactly
 * ONE correct answer, consumed identically by the air panel, the air page, the ventilation
 * planner, and AirCache's snapshot builder. Duplicating this logic per-view would risk the
 * same divergence bug that namespacing here prevents.
 */

var AirGuidelines = (function () {

    /** @type {Record<string, number>} species key -> WHO-2021 reference concentration (µg/m³). */
    var GUIDELINES = {
        pm2_5: 15,
        pm10: 45,
        o3: 100,
        no2: 25,
        so2: 40,
        co: 4000,
    };

    /**
     * HCHO has NO WHO ambient guideline. The server bands it with absolute heuristic
     * boundaries (good <= 2, fair <= 6, poor <= 12, very_poor above, µg/m³) rather than a
     * guideline multiple — these MUST stay numerically identical to the server's own
     * HCHO good/fair/poor bounds or a client-recomputed hcho band diverges from
     * `now.hcho.band`.
     */
    var HCHO_BOUNDS = { good: 2, fair: 6, poor: 12 };

    /** Fixed order the /v1/air contract's `now`/`hourly`/`daily` blocks carry species in. */
    var SPECIES = ['pm2_5', 'pm10', 'o3', 'no2', 'so2', 'co', 'hcho'];

    var BAND_ORDER = ['good', 'fair', 'poor', 'very_poor'];

    /** ag-prefixed: severity rank per band, for worst-of comparisons. Internal only. */
    var agSeverity = { good: 0, fair: 1, poor: 2, very_poor: 3 };

    /**
     * Classify a raw concentration exactly as the server does: WHO-2021 multiple for the
     * six guideline species, absolute HCHO_BOUNDS for hcho.
     * @param {string} species — a SPECIES key.
     * @param {number|null|undefined} value — concentration in µg/m³.
     * @returns {'good'|'fair'|'poor'|'very_poor'|null} null = missing/unknowable — NEVER
     *   defaults to 'good': a data gap must not read as safe air, least of all feeding the
     *   ventilation planner.
     */
    function bandForValue(species, value) {
        if (typeof value !== 'number' || !isFinite(value)) return null;
        if (species === 'hcho') {
            if (value <= HCHO_BOUNDS.good) return 'good';
            if (value <= HCHO_BOUNDS.fair) return 'fair';
            if (value <= HCHO_BOUNDS.poor) return 'poor';
            return 'very_poor';
        }
        var guideline = GUIDELINES[species];
        if (!guideline) return null;
        var multiple = value / guideline;
        if (multiple <= 1) return 'good';
        if (multiple <= 2) return 'fair';
        if (multiple <= 4) return 'poor';
        return 'very_poor';
    }

    /**
     * Worst band across every species present in a `now`-shaped object
     * (`{ time, pm2_5: {value, band}, pm10: {...}, ... }` — the /v1/air `now` block, or an
     * equivalent object built client-side).
     * @param {object} nowBlock
     * @returns {{species: string, band: string}|null} null when nowBlock is missing or
     *   every species entry is missing/unbanded — never fabricates a "good" verdict from
     *   an empty read.
     */
    function worstBand(nowBlock) {
        if (!nowBlock || typeof nowBlock !== 'object') return null;
        var worstSpecies = null;
        var worst = null;
        for (var i = 0; i < SPECIES.length; i++) {
            var key = SPECIES[i];
            var entry = nowBlock[key];
            if (!entry || entry.band == null) continue; // missing sample: not evidence of good air
            if (worstSpecies === null || agSeverity[entry.band] > agSeverity[worst]) {
                worst = entry.band;
                worstSpecies = key;
            }
        }
        if (worstSpecies === null) return null;
        return { species: worstSpecies, band: worst };
    }

    /**
     * ventStatus — the Ventilation Planner's core rule: should you open windows right now?
     * Ported EXACTLY from the platform demo. Severe particulate matter always wins over a
     * dry indoor reading, and any unknown input can NEVER produce OPEN — a data gap reads
     * as "we don't know", never as "safe to open".
     *
     * @param {object} input
     * @param {string|null} input.pm25Band — bandForValue('pm2_5', ...) or now.pm2_5.band.
     * @param {string|null} input.pm10Band — bandForValue('pm10', ...) or now.pm10.band.
     * @param {number|null} input.rhMean — indoor or local mean relative humidity, percent.
     * @returns {'OPEN'|'CAUTION'|'CLOSED'}
     */
    function ventStatus(input) {
        var pm25Band = (input && input.pm25Band) || null;
        var pm10Band = (input && input.pm10Band) || null;
        var rhMean = (input && typeof input.rhMean === 'number' && isFinite(input.rhMean))
            ? input.rhMean
            : null;

        var pm25Severe = pm25Band === 'poor' || pm25Band === 'very_poor';
        var pm10Severe = pm10Band === 'poor' || pm10Band === 'very_poor';
        var rhHigh = rhMean !== null && rhMean > 75;

        // Severe PM always wins — checked first and unconditionally, ahead of any humidity
        // reading, however dry.
        if (pm25Severe || pm10Severe || rhHigh) return 'CLOSED';

        // OPEN requires every input to be a KNOWN good reading. Any null (unknown band,
        // unknown humidity) falls through to CAUTION below, never to OPEN.
        if (pm25Band === 'good' && pm10Band === 'good' && rhMean !== null && rhMean < 65) {
            return 'OPEN';
        }

        return 'CAUTION';
    }

    return {
        GUIDELINES: GUIDELINES,
        HCHO_BOUNDS: HCHO_BOUNDS,
        SPECIES: SPECIES,
        BAND_ORDER: BAND_ORDER,
        bandForValue: bandForValue,
        worstBand: worstBand,
        ventStatus: ventStatus,
    };
})();

window.AirGuidelines = AirGuidelines;
