/**
 * WeatherCache — IndexedDB cache for /api/weather, plus the extraction logic that turns a
 * raw response into a small `weatherSnapshot` a scan record can carry.
 *
 * WHY A SEPARATE STORE FROM StorageService. StorageService owns scans and properties —
 * arrays that grow and get rewritten as a whole on every change. Weather is a single,
 * frequently-refreshed reading with its own lifecycle (stale-while-revalidate), so it gets
 * its own key rather than riding inside a scan/property write.
 *
 * WHY THE RAW PAYLOAD IS CACHED, NOT A MAPPED VIEW. WeatherPanel and WeatherPage each run
 * their own independent mapper (wxPanelMap / wxPageMap) over /api/weather — deliberately
 * separate after a prior bug where both were named `mapPayload` and one silently overwrote
 * the other (see the NAMESPACED comments in those files). Caching a mapped shape would
 * re-couple the two views to one schema. Caching the verbatim raw response lets each view
 * keep running its own mapper, unchanged, over whichever copy — network or cache — it has.
 *
 * TWO CONSUMERS:
 *   1. WeatherPanel / WeatherPage — stale-while-revalidate: paint from load() immediately,
 *      refresh over the network in the background, save() the result either way.
 *   2. AnalysisPage — getSnapshot() at scan-save time, to attach a small point-in-time
 *      weatherSnapshot to the scan record (see buildSnapshot below).
 *
 * Written in ES5 for Babel 6 standalone. No React dependency — a plain service like
 * StorageService. Every public method is Promise-based and NEVER rejects: a cache failure
 * (quota, corrupt record, IndexedDB unavailable) must never break weather display or scan
 * saving, so every method swallows its own errors and resolves null/undefined instead.
 */
var WeatherCache = (function () {

    var CACHE_KEY = 'mould-detect-weather-cache';
    var SCHEMA_V = 1;

    // Own localforage instance — same physical database as StorageService
    // (name/storeName must match to share the IndexedDB object store), but weather gets
    // its own key rather than living inside the scans/properties arrays.
    var store = localforage.createInstance({
        name: 'MouldDetect',
        storeName: 'app_data',
    });

    /**
     * How long a cached reading is served as the immediate paint before the skeleton
     * returns instead. Both weather surfaces always show a "last updated" age alongside the
     * reading (see wxFormatAge / freshness in WeatherPanel and WeatherPage), so serving a
     * day-old cached response is honest — the UI never claims it is current — while still
     * saving a network round trip and a skeleton flash on almost every visit. Past this age
     * the data is old enough that showing nothing (skeleton → fresh fetch) beats showing a
     * reading whose "Updated Xh ago" footer would undersell how stale it really is.
     */
    var MAX_AGE_MS = 24 * 60 * 60 * 1000;

    /* ────────────────────────────────────────────────────────────────────────────
       Tolerant extraction helpers — kept LOCAL to this file and `wc`-prefixed.
       This app has no module system: every top-level `var` in every <script> becomes a
       `window` property, and two files once both declared a helper called `mapPayload`,
       with one silently clobbering the other (see WeatherPanel/WeatherPage NAMESPACED
       comments). WeatherPanel uses `wx`-prefixed names, WeatherPage uses bare names
       (`num`, `pickNum`, `pickVal`) — so this file uses `wc`-prefixed names to collide
       with neither.
       ──────────────────────────────────────────────────────────────────────────── */

    /** First present, non-empty value among `keys` on `obj`. */
    var wcPick = function (obj, keys) {
        if (!obj || typeof obj !== 'object') return undefined;
        for (var i = 0; i < keys.length; i++) {
            var v = obj[keys[i]];
            if (v !== undefined && v !== null && v !== '') return v;
        }
        return undefined;
    };

    /** Coerce to a finite number, else null. Tolerates "72%" and numeric strings. */
    var wcNum = function (v) {
        if (typeof v === 'number') return isFinite(v) ? v : null;
        if (typeof v === 'string') {
            var n = parseFloat(v.replace('%', ''));
            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 wcStr = 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/weather → weatherSnapshot contract lives here.
     * Pure and defensive: every field is picked independently from the raw payload, and
     * anything missing or unparsable becomes null (never undefined) so the object survives
     * a JSON.stringify round-trip untouched — it rides inside a scan record through the
     * opaque S3 cloud-sync blob later, same guarantee as captureContext.
     *
     * `source` is 'cache' when derived from a stored cache record (getSnapshot(), and the
     * SWR cached paint in WeatherPanel/WeatherPage) or 'live' when built straight from a
     * fresh network response (AnalysisPage's 60-minute backfill). Exported separately from
     * getSnapshot() so both call sites share one extraction path rather than duplicating it.
     */
    function buildSnapshot(lat, lon, place, raw, source) {
        var r = (raw && typeof raw === 'object') ? raw : {};

        // "Current conditions" have lived under half a dozen names across the weather
        // surfaces this app has integrated with — mirrors wxPanelMap's own list.
        var cur = wcPick(r, ['current', 'currently', 'now', 'observation', 'current_weather']);
        if (!cur || typeof cur !== 'object') cur = r;

        var observedAt = wcStr(wcPick(r, ['observed_at', 'updated_at', 'observation_time', 'as_of', 'timestamp']));

        var freshness = (r.freshness && typeof r.freshness === 'object') ? r.freshness : {};
        var ageHours = wcNum(wcPick(freshness, ['age_hours', 'ageHours']));
        if (ageHours === null) ageHours = wcNum(wcPick(r, ['age_hours', 'ageHours', 'data_age_hours']));
        if (ageHours === null && observedAt) {
            var whenMs = new Date(observedAt).getTime();
            if (!isNaN(whenMs)) ageHours = (Date.now() - whenMs) / 3600000;
        }
        if (ageHours !== null && ageHours < 0) ageHours = 0;

        var staleFlag = wcPick(freshness, ['stale', 'is_stale']);
        if (staleFlag === undefined) staleFlag = wcPick(r, ['stale', 'is_stale']);
        var stale = (staleFlag === true || staleFlag === 'true') ? true
            : (staleFlag === false || staleFlag === 'false') ? false
            : (ageHours !== null ? ageHours >= 3 : null);

        var placeName = wcStr(place);
        if (!placeName) {
            var loc = wcPick(r, ['location', 'place', 'station']);
            if (loc && typeof loc === 'object') {
                placeName = wcStr(wcPick(loc, ['label', 'name', 'display_name', 'suburb', 'city', 'locality']));
            } else {
                placeName = wcStr(wcPick(r, ['location_name', 'place_name', 'city', 'suburb', 'name']));
            }
        }

        var condRaw = wcPick(cur, ['condition', 'conditions', 'summary', 'description', 'weather_text', 'text', 'short_text']);
        var conditionText = null;
        if (condRaw && typeof condRaw === 'object') {
            conditionText = wcStr(wcPick(condRaw, ['text', 'description', 'summary', 'label', 'main']));
        } else {
            conditionText = wcStr(condRaw);
            if (!conditionText) {
                var arr = wcPick(cur, ['weather']);
                if (arr instanceof Array && arr.length && arr[0]) {
                    conditionText = wcStr(wcPick(arr[0], ['description', 'main']));
                }
            }
        }

        return {
            v: SCHEMA_V,
            capturedAt: Date.now(),
            source: source === 'live' ? 'live' : 'cache',
            observedAt: observedAt,
            ageHours: ageHours,
            lat: wcNum(lat),
            lon: wcNum(lon),
            place: placeName,
            temp: wcNum(wcPick(cur, ['temp', 'temperature', 'temp_c', 'temperature_c', 'air_temperature', 'temperature_2m'])),
            humidity: wcNum(wcPick(cur, ['humidity', 'relative_humidity', 'relative_humidity_2m', 'humidity_pct', 'rel_hum', 'rh'])),
            dewPoint: wcNum(wcPick(cur, ['dew_point', 'dew_point_2m', 'dewpoint', 'dew_point_c'])),
            windKmh: wcNum(wcPick(cur, ['wind_speed_kmh', 'wind_speed_10m', 'wind_speed', 'windspeed', 'wind'])),
            weatherCode: wcNum(wcPick(cur, ['weather_code', 'weathercode', 'condition_code', 'code'])),
            conditionText: conditionText,
            precip: wcNum(wcPick(cur, ['precipitation', 'precip', 'rain', 'precipitation_sum'])),
            stale: stale,
        };
    }

    /**
     * dayMeanRH(raw, dateStr) — a calendar day's mean relative humidity from a raw
     * /api/weather payload, or null (never a guess) when the payload doesn't carry it.
     * Tolerant of both row-wise and columnar daily shapes.
     *
     * Lives HERE, not in the air views that consume it: this is weather-payload schema
     * knowledge, which this module owns, and both AirQualityPanel and AirQualityPage
     * feed it into AirGuidelines.ventStatus — two drifting copies would let the panel
     * and the page disagree about whether the same day is safe to ventilate. (The
     * per-view-mapper rule does not apply: that rule is about each view mapping ITS OWN
     * payload; neither air view owns the weather payload.)
     */
    function dayMeanRH(raw, dateStr) {
        if (!raw || typeof raw !== 'object' || !dateStr) return null;
        var daily = raw.daily || raw.forecast || raw.days || (raw.data && raw.data.daily) || null;
        if (!daily) return null;

        if (daily instanceof Array) {
            for (var i = 0; i < daily.length; i++) {
                var d = daily[i];
                if (!d || typeof d !== 'object') continue;
                var dv = wcStr(d.date || d.time || d.day || d.valid_date || d.timestamp) || '';
                if (dv.substring(0, 10) !== dateStr) continue;
                var rh = wcNum(d.humidity_mean_pct !== undefined ? d.humidity_mean_pct :
                    (d.relative_humidity_2m_mean !== undefined ? d.relative_humidity_2m_mean :
                    (d.humidity_mean !== undefined ? d.humidity_mean :
                    (d.humidity !== undefined ? d.humidity : d.rh_mean))));
                if (rh !== null) return rh;
            }
            return null;
        }

        if (typeof daily === 'object') {
            var times = daily.time || daily.date || daily.dates || null;
            if (!(times instanceof Array)) return null;
            var idx = -1;
            for (var t = 0; t < times.length; t++) {
                if ((wcStr(times[t]) || '').substring(0, 10) === dateStr) { idx = t; break; }
            }
            if (idx === -1) return null;
            var col = daily.humidity_mean_pct || daily.relative_humidity_2m_mean ||
                      daily.humidity_mean || daily.humidity || daily.rh_mean || null;
            if (!(col instanceof Array)) return null;
            return wcNum(col[idx]);
        }

        return null;
    }

    /**
     * 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. Void return (errors are swallowed and
     * logged nowhere: a failed cache write must not surface as a weather 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 weatherSnapshot 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; });
    }

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

window.WeatherCache = WeatherCache;
