/**
 * AirCache — IndexedDB cache for /api/air, plus the extraction logic that turns a raw
 * response into a small `airSnapshot` a scan record can carry. Sibling of
 * `components/WeatherCache/WeatherCache.jsx` — same contract, verbatim-adapted, so read
 * that file's header first if this one is unclear; only the air-specific differences are
 * called out below.
 *
 * WHY THE RAW PAYLOAD IS CACHED, NOT A MAPPED VIEW: identical reasoning to WeatherCache
 * §2.2 — the air panel and air page each run their own independent mapper over the raw
 * /api/air response, and caching a mapped shape would re-couple both views to one schema.
 * AirCache stores the verbatim raw response; each view maps its own copy, cached or live.
 *
 * SAMPLE-DATA ATTRIBUTION OBLIGATION (Copernicus CC-BY licence term — not optional):
 * whenever any surface displays data sourced from `loadSample()` (the bundled
 * `sample-air-data.json` fixture, used as a fallback rung when no cache exists and no
 * live fetch has completed yet), that surface MUST visibly label the reading as sample
 * data AND display the payload's own `attribution` string verbatim
 * ("Generated using Copernicus Atmosphere Monitoring Service information 2026" as of this
 * writing — read it from the payload, never hardcode it, since the platform can revise
 * the wording). This applies to the sample fixture specifically; a live or cached /v1/air
 * response carries the same `attribution` field and the same display obligation applies
 * to it too — CAMS attribution travels with the data everywhere it is shown, not only in
 * the sample-fallback case.
 *
 * Fallback ladder (built here, wired up by the UI layer): IndexedDB cache (this file's
 * load()) -> bundled sample data (loadSample(), labelled "sample data") -> live network
 * upgrade (/api/air, then save() to warm the cache for next time).
 *
 * Written in ES5 for Babel 6 standalone. No React dependency. Every public method is
 * Promise-based and NEVER rejects, and every promise chain here terminates in its own
 * `['catch']` — a cache or fixture-load failure must never break air display or scan
 * saving, so every method swallows its own errors and resolves null/undefined instead.
 */
var AirCache = (function () {

    var CACHE_KEY = 'mould-detect-air-cache';
    var SCHEMA_V = 1;
    var SAMPLE_URL = 'components/AirCache/sample-air-data.json';

    // Own localforage instance — same physical database as StorageService/WeatherCache
    // (name/storeName must match to share the IndexedDB object store), own key.
    var store = localforage.createInstance({
        name: 'MouldDetect',
        storeName: 'app_data',
    });

    /**
     * How long a cached reading is served as the immediate paint before falling through
     * to the next rung of the fallback ladder. 24h, matching WeatherCache.MAX_AGE_MS: both
     * air surfaces always show a data-age label alongside the reading, so a day-old cached
     * response is never presented as current — and 24h also matches the CAMS global
     * aerosol model's roughly one-run-per-day cadence, past which a cached reading is
     * genuinely a different model run, not merely an old paint of the current one.
     */
    var MAX_AGE_MS = 24 * 60 * 60 * 1000;

    /* ────────────────────────────────────────────────────────────────────────────
       Tolerant extraction helpers — kept LOCAL to this file and `ac`-prefixed, following
       WeatherCache's `wc`-prefix discipline (see that file's NAMESPACED comments): this
       app has no module system, so every top-level `var` in every <script> becomes a
       `window` property, and per-file prefixes are what keeps two files' same-named
       helpers from silently clobbering each other.
       ──────────────────────────────────────────────────────────────────────────── */

    /** Coerce to a finite number, else null. */
    var acNum = function (v) {
        if (typeof v === 'number') return isFinite(v) ? v : null;
        if (typeof v === 'string') {
            var n = parseFloat(v);
            return isFinite(n) ? n : null;
        }
        return null;
    };

    /** Coerce to a trimmed, non-empty string, else null (never '', per the snapshot's
     *  JSON-primitives-only, null-not-undefined contract). */
    var acStr = function (v) {
        if (typeof v === 'string') { var t = v.trim(); return t ? t : null; }
        if (typeof v === 'number' && isFinite(v)) return String(v);
        return null;
    };

    /**
     * buildSnapshot — the entire /api/air -> airSnapshot contract lives here.
     * Pure and defensive: reads only the raw payload's `now` block — the point-in-time
     * reading a scan needs, not the `hourly`/`daily` forecast arrays, which this function
     * never iterates. Every field is picked independently and anything missing or
     * unparsable becomes null (never undefined), so the object survives a
     * JSON.stringify round-trip untouched — same guarantee weatherSnapshot and
     * captureContext already rely on for the future opaque cloud-sync blob. Contains no
     * references into `raw` (every value is copied out as a primitive), so the caller is
     * free to drop its reference to the (potentially large, multi-day) raw payload the
     * moment this returns.
     *
     * `source` is 'cache' when derived from a stored cache record (getSnapshot()) or
     * 'live' when built straight from a fresh network response (AnalysisPage's 60-minute
     * backfill) — same convention as WeatherCache.buildSnapshot.
     *
     * Bands: the server's own `now.<species>.band` is used when present (it is the
     * authoritative classification); AirGuidelines.bandForValue is used to derive one
     * ONLY when a value is present but its band is missing, and reproduces the server's
     * thresholds exactly (see AirGuidelines.jsx header) so the two never disagree.
     */
    function buildSnapshot(lat, lon, place, raw, source) {
        var r = (raw && typeof raw === 'object') ? raw : {};
        var now = (r.now && typeof r.now === 'object') ? r.now : {};

        var snapshot = {
            v: SCHEMA_V,
            capturedAt: Date.now(),
            source: source === 'live' ? 'live' : 'cache',
            run: acStr(r.run),
            lat: acNum(lat),
            lon: acNum(lon),
            place: acStr(place),
            guideline: acStr(r.guideline),
            vocProxy: acStr(r.voc_proxy),
            worstSpecies: null,
            worstBand: null,
        };

        // Filled per-species now-block: server band where present, else derived from the
        // value via the identical AirGuidelines thresholds. Built once and reused both to
        // populate the flat snapshot fields below and to compute the worst-band summary,
        // so the two can never disagree with each other.
        var filledNow = {};
        for (var i = 0; i < AirGuidelines.SPECIES.length; i++) {
            var species = AirGuidelines.SPECIES[i];
            var entry = now[species];
            var value = acNum(entry && entry.value);
            var band = (entry && typeof entry.band === 'string') ? entry.band : null;
            if (!band && value !== null) band = AirGuidelines.bandForValue(species, value);
            filledNow[species] = { value: value, band: band };
            snapshot[species] = value;
            snapshot[species + 'Band'] = band;
        }

        var worst = AirGuidelines.worstBand(filledNow);
        if (worst) {
            snapshot.worstSpecies = worst.species;
            snapshot.worstBand = worst.band;
        }

        return snapshot;
    }

    /**
     * load() -> the cached record, or null when there is nothing usable: missing, wrong
     * schema version, or older than MAX_AGE_MS. Never rejects.
     */
    function load() {
        return store.getItem(CACHE_KEY)
            .then(function (rec) {
                if (!rec || typeof rec !== 'object') return null;
                if (rec.v !== SCHEMA_V) return null;
                if (typeof rec.savedAt !== 'number') return null;
                if (Date.now() - rec.savedAt > MAX_AGE_MS) return null;
                return rec;
            })
            ['catch'](function () { return null; });
    }

    /**
     * save() — overwrite the single cache record (bounded storage by design: one
     * overwritten reading, never a growing history). Void return; errors are swallowed —
     * a failed cache write must not surface as an air-quality failure.
     */
    function save(lat, lon, place, raw) {
        var rec = {
            v: SCHEMA_V,
            savedAt: Date.now(),
            lat: lat,
            lon: lon,
            place: place || null,
            raw: raw,
        };
        return store.setItem(CACHE_KEY, rec)
            .then(function () {})
            ['catch'](function () {});
    }

    /**
     * getSnapshot() — the airSnapshot for "right now", derived from whatever is currently
     * cached (source: 'cache'). Used by AnalysisPage at scan-save time. Resolves null when
     * there is no valid cache — capture never blocks or delays a scan save.
     */
    function getSnapshot() {
        return load()
            .then(function (rec) {
                if (!rec) return null;
                return buildSnapshot(rec.lat, rec.lon, rec.place, rec.raw, 'cache');
            })
            ['catch'](function () { return null; });
    }

    /**
     * loadSample() -> the bundled sample-air-data.json, parsed, or null on any failure
     * (missing file, bad JSON, offline). Never rejects. This is the middle rung of the
     * fallback ladder the UI layer implements (cache -> sample -> live) — see the
     * attribution obligation in this file's header, which applies to whatever this
     * resolves.
     */
    function loadSample() {
        return fetch(SAMPLE_URL, { headers: { 'Accept': 'application/json' } })
            .then(function (res) {
                if (!res.ok) return null;
                return res.json();
            })
            ['catch'](function () { return null; });
    }

    return {
        load: load,
        save: save,
        getSnapshot: getSnapshot,
        buildSnapshot: buildSnapshot,
        loadSample: loadSample,
        MAX_AGE_MS: MAX_AGE_MS,
    };
})();

window.AirCache = AirCache;
