// ============================================================================
// SHARED "TONIGHT'S SKY SCORE" DATA SERVICE
// Powers the navbar badge (assets/js/shared/navbar.jsx) shown on every page -
// a single 0-10 score answering "is tonight worth going out with a camera",
// built from three things already used elsewhere on the site: cloud cover
// (Open-Meteo, same provider weather.html uses), moonlight (SunCalc, same
// library the moon-phase calendar and planning page use), and the aurora Kp
// forecast (assets/js/shared/aurora-service.jsx, already shared with weather.
// html/planning.html).
//
// Deliberately lighter than weather.html's full astro-conditions panel
// (which also factors seeing/transparency/dew risk across the whole hourly
// forecast) - this runs on EVERY page via the navbar, so it fetches a lean
// Open-Meteo payload (cloud cover + sunset/sunrise only, not the dozen
// fields that panel needs) and caches aggressively. The popover this powers
// links through to weather.html for the full breakdown.
//
// Aurora is deliberately NOT part of the numeric score - a quiet Kp forecast
// doesn't make a clear, dark night any less worth shooting the Milky Way, it
// just means the aurora isn't the story tonight. It's surfaced as an
// informational bonus row instead (see buildVerdict/factors below).
//
// Same "null means we don't know, never guess" rule aurora-service.jsx
// documents at length - a fetch failure here shows the badge as unavailable,
// never a fabricated score.
// ============================================================================

const SKY_SCORE_LOCATION_KEY = 'np_sky_score_location';
const SKY_SCORE_CACHE_KEY = 'np_sky_score_cache';
const SKY_SCORE_CACHE_TTL_MS = 30 * 60 * 1000; // matches aurora-service.jsx's cache window

// Bamburgh - already the site's reference "Northumberland coast" point (see
// data/webcams.json) and one of NorthernPixl's own most-shot locations, so
// it's a sensible default before we know where a visitor actually is.
const SKY_SCORE_DEFAULT_LOCATION = { lat: 55.6092, lng: -1.7099, name: 'Northumberland Coast', isDefault: true };

const SUNCALC_URL = 'https://unpkg.com/suncalc@1.9.0/suncalc.js';
let _sunCalcPromise = null;
const loadSunCalc = () => {
    if (window.SunCalc) return Promise.resolve();
    if (_sunCalcPromise) return _sunCalcPromise;
    _sunCalcPromise = new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = SUNCALC_URL;
        script.onload = () => resolve();
        script.onerror = () => reject(new Error('Failed to load SunCalc'));
        document.head.appendChild(script);
    }).catch((err) => {
        _sunCalcPromise = null; // let a later attempt retry instead of staying permanently rejected
        throw err;
    });
    return _sunCalcPromise;
};

const _skyScoreFetchWithTimeout = (url, ms = 6000) => {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => reject(new Error('Timeout')), ms);
        fetch(url, { cache: 'no-store' })
            .then(res => { clearTimeout(timer); resolve(res); })
            .catch(err => { clearTimeout(timer); reject(err); });
    });
};

// --------------------------------------------------------------------------
// Location: nothing on the site currently remembers where a visitor is
// (weather.html's map pin is per-session, not persisted). Order: a location
// already resolved this browser before (geolocation or default, cached
// indefinitely so we never re-prompt automatically) -> ask browser
// geolocation once -> fall back to the default, clearly marked isDefault so
// the UI can label it rather than presenting it as personal.
// --------------------------------------------------------------------------
const resolveSkyScoreLocation = () => {
    try {
        const stored = JSON.parse(localStorage.getItem(SKY_SCORE_LOCATION_KEY) || 'null');
        if (stored && typeof stored.lat === 'number' && typeof stored.lng === 'number') {
            return Promise.resolve(stored);
        }
    } catch (e) { /* fall through to fresh resolution */ }

    const persistAndResolve = (loc, resolve) => {
        try { localStorage.setItem(SKY_SCORE_LOCATION_KEY, JSON.stringify(loc)); } catch (e) { /* ignore */ }
        resolve(loc);
    };

    return new Promise((resolve) => {
        if (!navigator.geolocation) { persistAndResolve({ ...SKY_SCORE_DEFAULT_LOCATION }, resolve); return; }
        const timer = setTimeout(() => persistAndResolve({ ...SKY_SCORE_DEFAULT_LOCATION }, resolve), 6000);
        navigator.geolocation.getCurrentPosition(
            (pos) => {
                clearTimeout(timer);
                persistAndResolve({ lat: pos.coords.latitude, lng: pos.coords.longitude, name: 'Your Location', isDefault: false }, resolve);
            },
            () => { clearTimeout(timer); persistAndResolve({ ...SKY_SCORE_DEFAULT_LOCATION }, resolve); },
            { timeout: 5000, maximumAge: 3600000 }
        );
    });
};

// Used by the popover's "Change location" link - clears the remembered
// location AND the cached score (sessionStorage, not localStorage - the
// score cache lives there, see getSkyScoreCached below) so the next
// resolution genuinely re-asks geolocation and genuinely re-fetches, rather
// than the location changing but instantly replaying an old cached score
// for it.
const resetSkyScoreLocation = () => {
    try {
        localStorage.removeItem(SKY_SCORE_LOCATION_KEY);
        sessionStorage.removeItem(SKY_SCORE_CACHE_KEY);
    } catch (e) { /* ignore */ }
};

// --------------------------------------------------------------------------
// Moon visibility: SunCalc.getMoonTimes(date, lat, lng) returns rise/set for
// ONE calendar day (that day's local midnight to the next) - a shooting
// window that runs from evening into the small hours crosses that boundary,
// so this checks both the sunset-day and the following day and sweeps the
// combined, de-duplicated events to work out what fraction of the actual
// sunset->sunrise window the moon is above the horizon.
// --------------------------------------------------------------------------
const getMoonWindowInfo = (sunsetTime, sunriseTime, lat, lng) => {
    const dayA = new Date(sunsetTime);
    const dayB = new Date(sunsetTime + 24 * 3600000);
    const timesA = window.SunCalc.getMoonTimes(dayA, lat, lng);
    const timesB = window.SunCalc.getMoonTimes(dayB, lat, lng);

    const rawEvents = [];
    [timesA, timesB].forEach((t) => {
        if (t.rise) rawEvents.push({ type: 'rise', time: t.rise.getTime() });
        if (t.set) rawEvents.push({ type: 'set', time: t.set.getTime() });
    });
    const seen = new Set();
    const events = rawEvents.filter((e) => {
        const key = `${e.type}-${Math.round(e.time / 60000)}`; // de-dupe same event seen from both day queries
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
    }).sort((a, b) => a.time - b.time);

    // Is the moon already up at sunset? alwaysUp/alwaysDown cover the polar
    // edge cases directly; otherwise the most recent event before sunset
    // tells us the state at that instant.
    let upAtSunset = !!timesA.alwaysUp;
    if (!timesA.alwaysUp && !timesA.alwaysDown) {
        const before = events.filter((e) => e.time <= sunsetTime).pop();
        upAtSunset = before ? before.type === 'rise' : false;
    }

    let upMs = 0;
    let cursor = sunsetTime;
    let state = upAtSunset;
    events.forEach((e) => {
        if (e.time <= sunsetTime || e.time >= sunriseTime) return;
        if (state) upMs += e.time - cursor;
        cursor = e.time;
        state = e.type === 'rise';
    });
    if (state) upMs += sunriseTime - cursor;

    const windowMs = Math.max(1, sunriseTime - sunsetTime);
    const upFraction = Math.max(0, Math.min(1, upMs / windowMs));
    const eventsInWindow = events.filter((e) => e.time > sunsetTime && e.time < sunriseTime);

    return { upFraction, eventsInWindow, downAllNight: upFraction < 0.02, upAllNight: upFraction > 0.98 };
};

const formatMoonTiming = (moonWindow, tz) => {
    const fmt = (ms) => new Date(ms).toLocaleTimeString('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit' });
    if (moonWindow.downAllNight) return 'Down all night';
    if (moonWindow.upAllNight) return 'Up all night';
    if (moonWindow.eventsInWindow.length === 0) return moonWindow.upFraction > 0.5 ? 'Up most of the night' : 'Down most of the night';
    return moonWindow.eventsInWindow.map((e) => `${e.type === 'rise' ? 'Rises' : 'Sets'} ${fmt(e.time)}`).join(' · ');
};

// --------------------------------------------------------------------------
// Composite score (0-10). Sky clarity is the main driver (cloud cover is the
// difference between "went home with photos" and "went home wet"); moonlight
// is weighted by how much of the actual shooting window the moon is above
// the horizon, not just its phase - a bright moon that's below the horizon
// all night doesn't cost anything.
// --------------------------------------------------------------------------
const buildVerdict = (tier, avgCloud, moonWindow, maxKp) => {
    if (tier === 'poor') {
        if (avgCloud > 70) return 'Heavy cloud expected tonight - not a night for the camera.';
        return 'A bright moon will wash out faint detail for most of tonight.';
    }
    if (tier === 'fair') {
        if (avgCloud > 40) return 'Patchy cloud tonight - worth a look, but temper expectations.';
        return 'Some moonlight tonight, but skies should stay fairly clear.';
    }
    if (maxKp !== null && maxKp >= 5) return 'Excellent skies and a real aurora chance tonight.';
    if (moonWindow.downAllNight) return 'Excellent for the Milky Way - clear skies and the moon is down all night.';
    return 'Excellent for the Milky Way tonight - clear, dark skies.';
};

const fetchSkyScore = async (lat, lng) => {
    const params = `latitude=${lat.toFixed(4)}&longitude=${lng.toFixed(4)}&hourly=cloudcover&daily=sunset,sunrise&timezone=auto&forecast_days=2`;
    const [weatherRes] = await Promise.allSettled([
        _skyScoreFetchWithTimeout(`https://api.open-meteo.com/v1/forecast?${params}`, 6000),
        loadSunCalc()
    ]);

    if (weatherRes.status !== 'fulfilled' || !weatherRes.value.ok) return null;
    const weather = await weatherRes.value.json();
    if (!window.SunCalc) return null; // SunCalc failed to load - don't show a moon-blind score as if it were complete

    const tz = weather.timezone || 'Europe/London';
    const sunsetTime = weather.daily?.sunset?.[0] ? new Date(weather.daily.sunset[0]).getTime() : null;
    const sunriseTime = weather.daily?.sunrise?.[1] ? new Date(weather.daily.sunrise[1]).getTime() : null;
    if (!sunsetTime || !sunriseTime || !weather.hourly?.time || !weather.hourly?.cloudcover) return null;

    let cloudSum = 0, cloudCount = 0;
    weather.hourly.time.forEach((t, i) => {
        const time = new Date(t).getTime();
        if (time >= sunsetTime && time <= sunriseTime) {
            const c = weather.hourly.cloudcover[i];
            if (c !== null && c !== undefined && !isNaN(c)) { cloudSum += c; cloudCount++; }
        }
    });
    if (cloudCount === 0) return null;
    const avgCloud = Math.round(cloudSum / cloudCount);

    const illum = window.SunCalc.getMoonIllumination(new Date(sunsetTime));
    const moonWindow = getMoonWindowInfo(sunsetTime, sunriseTime, lat, lng);

    const skyPoints = Math.max(0, Math.min(10, 10 - avgCloud / 10));
    const moonPoints = Math.max(0, Math.min(10, 10 - (illum.fraction * 10 * moonWindow.upFraction)));
    const score = Math.max(0, Math.min(10, Math.round(skyPoints * 0.6 + moonPoints * 0.4)));
    const tier = score >= 8 ? 'good' : score >= 4 ? 'fair' : 'poor';

    // Aurora: informational only, never affects the score above - see file header.
    let maxKp = null, kpSource = null;
    try {
        const { points: kpPoints, source } = await fetchAuroraKpPoints();
        kpSource = source;
        if (kpPoints.length > 0) maxKp = maxAuroraKpInWindow(kpPoints, sunsetTime, sunriseTime);
    } catch (e) { /* aurora is a bonus row - its absence shouldn't break the score */ }

    const auroraTier = maxKp === null ? 'unknown' : maxKp >= 5 ? 'likely' : maxKp >= 3 ? 'possible' : 'unlikely';

    return {
        score,
        tier,
        verdict: buildVerdict(tier, avgCloud, moonWindow, maxKp),
        sky: { cloud: avgCloud, tier: avgCloud <= 20 ? 'good' : avgCloud <= 45 ? 'fair' : 'poor' },
        moon: {
            illuminationPct: Math.round(illum.fraction * 100),
            timing: formatMoonTiming(moonWindow, tz),
            tier: moonWindow.downAllNight ? 'good' : illum.fraction <= 0.3 ? 'good' : illum.fraction <= 0.6 ? 'fair' : 'poor'
        },
        aurora: { maxKp, tier: auroraTier, source: kpSource },
        sunsetTime,
        sunriseTime,
        fetchedAt: Date.now()
    };
};

const _cacheKeyFor = (lat, lng) => `${lat.toFixed(2)}_${lng.toFixed(2)}_${new Date().toDateString()}`;

const getSkyScoreCached = async (lat, lng) => {
    const key = _cacheKeyFor(lat, lng);
    try {
        const cached = JSON.parse(sessionStorage.getItem(SKY_SCORE_CACHE_KEY) || 'null');
        if (cached && cached.key === key && (Date.now() - cached.fetchedAt) < SKY_SCORE_CACHE_TTL_MS) {
            return cached.result;
        }
    } catch (e) { /* fall through to a fresh fetch */ }

    const result = await fetchSkyScore(lat, lng);
    if (result) {
        try { sessionStorage.setItem(SKY_SCORE_CACHE_KEY, JSON.stringify({ key, fetchedAt: Date.now(), result })); } catch (e) { /* ignore */ }
    }
    return result;
};

// Hook used by navbar.jsx.
const useSkyScore = () => {
    const [state, setState] = useState({ status: 'loading', data: null, location: null, locationNotice: null });

    const load = async (forceLocation) => {
        setState((prev) => ({ ...prev, status: 'loading' }));
        if (forceLocation) resetSkyScoreLocation();
        const location = await resolveSkyScoreLocation();
        const data = await getSkyScoreCached(location.lat, location.lng);
        // Only worth telling someone their location couldn't be used when
        // they just explicitly asked (clicked "Change location") - showing
        // this on every silent, automatic first-load resolution would nag
        // anyone who has location permission denied on every single page.
        const locationNotice = (forceLocation && location.isDefault)
            ? "Couldn't get your location (check your browser's location permission) - showing the Northumberland coast instead."
            : null;
        setState({ status: data ? 'ready' : 'error', data, location, locationNotice });
    };

    useEffect(() => { load(false); }, []);

    // Used when the visitor picks a location manually - the navbar's search
    // box (Nominatim, same as weather.html's own) or "pick on the weather
    // map" hand this a {lat, lng, name} directly, bypassing geolocation
    // entirely. Same persistence as resolveSkyScoreLocation's own writes,
    // so it's remembered on this device exactly like a geolocation
    // resolution would be, and clears the score cache so the new location's
    // score is genuinely fetched rather than reusing whatever's cached for
    // the old one.
    const setManualLocation = (loc) => {
        try {
            localStorage.setItem(SKY_SCORE_LOCATION_KEY, JSON.stringify({ lat: loc.lat, lng: loc.lng, name: loc.name, isDefault: false }));
            sessionStorage.removeItem(SKY_SCORE_CACHE_KEY);
        } catch (e) { /* ignore */ }
        load(false);
    };

    return {
        status: state.status,
        data: state.data,
        location: state.location,
        locationNotice: state.locationNotice,
        changeLocation: () => load(true),
        setManualLocation
    };
};
