/**
 * WeatherData — the shared data layer behind every weather surface.
 *
 * WHY THIS EXISTS. WeatherPanel and WeatherPage were built in parallel and each grew its
 * own copy of the same four concerns: acquiring a location, fetching /api/weather,
 * normalising the payload, and decoding WMO condition codes. Two copies of a mapper is how
 * the two drifted apart — and, when both happened to call theirs `mapPayload`, how one
 * silently overwrote the other in an app with no module system.
 *
 * The split is deliberate:
 *
 *   WeatherData.normalise()    — the ONE place the API contract is written down.
 *   WeatherData.condition()    — WMO/OpenWeather codes to a glyph key, once.
 *
 * (fetch(), assessDamp() and the withWeather HOC were removed 2026-08-06: nothing
 * consumed them — the panel and page own their fetching, and each surface keeps its own
 * damp read. See git history to resurrect.)
 *
 * ES5 only — Babel 6 standalone compiles this in the browser. No arrows, no template
 * literals, no const/let, no spread.
 */


/* ─────────────────────────────────────────────────────────────────────────────
   THE API CONTRACT. Everything this app knows about /api/weather's shape is in
   normalise(). Change the backend, change this function, and every weather
   surface follows.
   ───────────────────────────────────────────────────────────────────────────── */

/** Coerce to a finite number, else null. Tolerates "72%" and numeric strings. */
var wdNum = 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;
};

/** 'YYYY-MM-DD' to a LOCAL Date. new Date('2026-07-12') is UTC midnight and can land on
 *  the previous day in negative-offset zones, which shifts every label by one. */
var wdDate = function (value) {
    if (typeof value !== 'string') return null;
    var m = value.match(/^(\d{4})-(\d{2})-(\d{2})/);
    if (!m) {
        var parsed = new Date(value);
        return isNaN(parsed.getTime()) ? null : parsed;
    }
    return new Date(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10));
};

/** One day, from either the `daily` forecast or the `history` archive.
 *  The two feeds deliberately share field names wherever the measurement is the same, so
 *  one shape covers both and a chart can concatenate them into a single series. */
var wdDay = function (d, isFuture) {
    if (!d || typeof d !== 'object') return null;
    return {
        date: wdDate(d.date),
        dateISO: typeof d.date === 'string' ? d.date : null,
        tempMax: wdNum(d.temp_max_c),
        tempMin: wdNum(d.temp_min_c),
        humidityMean: wdNum(d.humidity_mean_pct),
        humidityMax: wdNum(d.humidity_max_pct),
        precipMm: wdNum(d.precipitation_mm),
        // The three variables that make this a mould feature rather than a weather one.
        // The platform only returns them when named explicitly.
        wetHours: wdNum(d.humid_hours_above_80),
        dewSpread: wdNum(d.dew_point_spread_min_c),
        condensationRisk: d.condensation_risk === true,
        // Archive only: fraction of the day actually ingested. Surfaced rather than used
        // to filter — a partly-complete day is real data, and dropping it silently leaves
        // an unexplained gap while plotting it as whole renders a dip that never happened.
        completeness: wdNum(d.completeness),
        weatherCode: wdNum(d.weather_code),
        future: !!isFuture,
    };
};

var wdCompact = function (list, isFuture) {
    var out = [];
    if (!list || typeof list.length !== 'number') return out;
    for (var i = 0; i < list.length; i++) {
        var day = wdDay(list[i], isFuture);
        if (day) out.push(day);
    }
    return out;
};

/**
 * The whole API contract, in one function.
 * Returns null when there is nothing usable, so a view can show its empty state rather
 * than a card full of dashes.
 */
var wdNormalise = function (raw) {
    if (!raw || typeof raw !== 'object') return null;
    var current = raw.current && typeof raw.current === 'object' ? raw.current : {};
    var freshness = raw.freshness && typeof raw.freshness === 'object' ? raw.freshness : {};

    var history = wdCompact(raw.history, false);
    var forecast = wdCompact(raw.daily, true);
    // The archive trails the forecast by several days, so today usually appears in
    // `daily`. Mark it, rather than assuming index 0 of either list.
    var todayMs = (function () { var d = new Date(); d.setHours(0, 0, 0, 0); return d.getTime(); })();
    for (var i = 0; i < forecast.length; i++) {
        forecast[i].future = forecast[i].date ? forecast[i].date.getTime() > todayMs : true;
        forecast[i].today = forecast[i].date ? forecast[i].date.getTime() === todayMs : false;
    }

    return {
        lat: wdNum(raw.location && raw.location.lat),
        lon: wdNum(raw.location && raw.location.lon),
        timezone: raw.timezone || null,
        localTime: raw.local_time || null,
        observedAt: raw.observed_at || null,
        isDay: current.is_day !== false,

        temp: wdNum(current.temp_c),
        humidity: wdNum(current.humidity_pct),
        windKmh: wdNum(current.wind_speed_kmh),
        gustKmh: wdNum(current.wind_gusts_kmh),
        tempMax: wdNum(current.temp_max_c),
        tempMin: wdNum(current.temp_min_c),
        weatherCode: wdNum(current.weather_code),

        history: history,
        forecast: forecast,
        // One continuous series for charting: observed days then predicted ones.
        series: history.concat(forecast),

        flood: raw.flood || null,

        // `stale: null` means UNKNOWN — /health was unreachable. It must never render as
        // fresh. `true` means the platform itself said its data is old, which it does via
        // a 503 far more often than not.
        stale: freshness.stale === true ? true : (freshness.stale === false ? false : null),
        ageHours: wdNum(freshness.age_hours),
        archiveEdge: freshness.history_latest_complete_date || null,
        cached: raw.cached === true,
    };
};

/* ─────────────────────────────────────────────────────────────────────────────
   CONDITION DECODING — one table, not one per view.
   ───────────────────────────────────────────────────────────────────────────── */

/** WMO (0-99) or OpenWeather (2xx-8xx) code to a glyph key. */
var wdCondition = function (code) {
    var c = wdNum(code);
    if (c === null) return 'unknown';
    if (c <= 99) {
        if (c === 0) return 'sun';
        if (c === 1 || c === 2) return 'partly';
        if (c === 3) return 'cloud';
        if (c === 45 || c === 48) return 'fog';
        if (c >= 51 && c <= 67) return 'rain';
        if (c >= 71 && c <= 77) return 'snow';
        if (c >= 80 && c <= 82) return 'rain';
        if (c === 85 || c === 86) return 'snow';
        if (c >= 95) return 'storm';
        return 'unknown';
    }
    if (c >= 200 && c < 300) return 'storm';
    if (c >= 300 && c < 600) return 'rain';
    if (c >= 600 && c < 700) return 'snow';
    if (c >= 700 && c < 800) return 'fog';
    if (c === 800) return 'sun';
    if (c === 801 || c === 802) return 'partly';
    if (c > 802 && c < 900) return 'cloud';
    return 'unknown';
};

var WD_CONDITION_TEXT = {
    sun: 'Clear', partly: 'Partly cloudy', cloud: 'Cloudy', rain: 'Rain',
    storm: 'Storms', snow: 'Snow', fog: 'Fog', unknown: '—',
};

/* ─────────────────────────────────────────────────────────────────────────────
   DAMP ASSESSMENT — the mould read, shared so the panel and the page can never
   disagree about the same weather.
   ───────────────────────────────────────────────────────────────────────────── */

window.WeatherData = {
    normalise: wdNormalise,
    condition: wdCondition,
};
