// ------------------------------------------------------------------------
//  useForecast - fetches the weather forecast and air quality for the
//  given location and keeps them fresh.
//
//  Responsibilities:
//    1. Fetch /v1/forecast on location change, with a 5-minute
//       localStorage cache. fetchedAt reflects the cache timestamp.
//    2. Soil data (soil_temperature, soil_moisture) waterfall:
//       a. Main auto API — works if the regional model includes soil.
//       b. ICON Global fallback — used when the auto model returns nulls
//          (e.g. UK Met Office / MetNo don't expose soil variables).
//       c. Stale soil cache — used if ICON fetch fails.
//    3. Fetch /v1/air-quality on location change, with a 6-hour
//       localStorage cache (AQI / pollen update slowly).
//    4. Auto-refresh forecast every 5 minutes for Pro, 15 minutes for
//       free (Open-Meteo updates ~every 15 min). Air quality only
//       refetches if cache is stale.
//    5. Tick `now` every 2 minutes to keep the current-row highlight
//       accurate without any API cost.
//
//  Inputs:
//    location  - { lat, lon, name, country }
//    isPro     - boolean; true tightens auto-refresh to 5 min
//
//  Outputs:
//    forecast    - raw /v1/forecast response, or null
//    airQuality  - raw /v1/air-quality response, or null
//    loading     - true while the forecast fetch is in flight
//    error       - fetch error message, or null
//    now         - Date that ticks every 2 minutes (drives the current row)
//    fetchedAt   - Date the forecast data was last fetched or read from cache
// ------------------------------------------------------------------------

import { useState, useEffect, useRef } from '../../vendor/preact-hooks.js';
import { solarElevationDeg } from '../physics.js';

const FIVE_MIN_MS    =  5 * 60 * 1000;
const FIFTEEN_MIN_MS = 15 * 60 * 1000;
const THIRTY_MIN_MS  = 30 * 60 * 1000;
const SIX_HOURS_MS   =  6 * 60 * 60 * 1000;
const ONE_MIN_MS     =  1 * 60 * 1000;

function buildForecastUrl(loc) {
  return `https://api.open-meteo.com/v1/forecast` +
    `?latitude=${loc.lat}&longitude=${loc.lon}` +
    `&hourly=temperature_2m,relative_humidity_2m,dew_point_2m,` +
    `wind_speed_10m,wind_direction_10m,wind_gusts_10m,` +
    `direct_radiation,diffuse_radiation,shortwave_radiation,` +
    `cloud_cover,cloud_cover_low,cloud_cover_mid,cloud_cover_high,` +
    `uv_index,precipitation,precipitation_probability,snowfall,visibility,` +
    `soil_temperature_0cm,soil_temperature_6cm,soil_moisture_0_to_1cm` +
    `&wind_speed_unit=ms&timezone=auto&forecast_days=14`;
}

function buildAirQualityUrl(loc) {
  return `https://air-quality-api.open-meteo.com/v1/air-quality` +
    `?latitude=${loc.lat}&longitude=${loc.lon}` +
    `&hourly=european_aqi,` +
    `grass_pollen,birch_pollen,alder_pollen,` +
    `mugwort_pollen,olive_pollen,ragweed_pollen` +
    `&timezone=auto&forecast_days=5`;
}

function forecastCacheKey(loc) {
  return `sunscope_fc_${loc.lat.toFixed(4)}_${loc.lon.toFixed(4)}`;
}

function airQualityCacheKey(loc) {
  return `sunscope_aq_${loc.lat.toFixed(4)}_${loc.lon.toFixed(4)}`;
}

// ─── SOIL (ECMWF) ─────────────────────────────────────────────────────────
// Open-Meteo's auto-selected regional models (UK Met Office, MetNo, etc.)
// don't include soil variables — they return all-null arrays. ECMWF IFS
// covers soil globally so we fetch soil fields separately and merge them in.

function buildSoilUrl(loc) {
  return `https://api.open-meteo.com/v1/forecast` +
    `?latitude=${loc.lat}&longitude=${loc.lon}` +
    `&hourly=soil_temperature_0cm,soil_temperature_6cm,soil_moisture_0_to_1cm` +
    `&timezone=auto&forecast_days=14&models=icon_global`;
}

function soilCacheKey(loc) {
  return `sunscope_soil_${loc.lat.toFixed(4)}_${loc.lon.toFixed(4)}`;
}

// Fetches ICON soil hourly data with its own cache.
// Returns stale cache of any age if the fetch fails — caller uses this as last resort.
async function fetchSoilHourly(loc) {
  const key = soilCacheKey(loc);
  let stale = null;
  try {
    const cached = localStorage.getItem(key);
    if (cached) {
      const { ts, data } = JSON.parse(cached);
      if (Date.now() - ts < FIVE_MIN_MS) return data; // fresh — skip fetch
      stale = data;                                     // stale — keep as fallback
    }
  } catch (e) { /* ignore bad cache */ }
  try {
    const r = await fetch(buildSoilUrl(loc));
    if (!r.ok) return stale;
    const json = await r.json();
    const h = json.hourly;
    try { localStorage.setItem(key, JSON.stringify({ ts: Date.now(), data: h })); } catch (e) {}
    return h;
  } catch (e) { return stale; } // network failure — return stale if available
}

// Merges soil arrays into forecast.hourly, aligned by time string.
function mergeSoil(forecastData, soilHourly) {
  if (!soilHourly) return forecastData;
  const timeToIdx = {};
  soilHourly.time.forEach((t, i) => { timeToIdx[t] = i; });
  const h = { ...forecastData.hourly };
  ['soil_temperature_0cm', 'soil_temperature_6cm', 'soil_moisture_0_to_1cm'].forEach(field => {
    h[field] = forecastData.hourly.time.map(t => {
      const i = timeToIdx[t];
      return i !== undefined ? (soilHourly[field]?.[i] ?? null) : null;
    });
  });
  return { ...forecastData, hourly: h };
}

export function useForecast(location, isPro = false) {
  const [forecast,  setForecast]  = useState(null);
  const [airQuality, setAirQuality] = useState(null);
  const [loading,   setLoading]   = useState(false);
  const [error,     setError]     = useState(null);
  const [now,       setNow]       = useState(new Date());
  const [fetchedAt, setFetchedAt] = useState(null);

  // Mirrors the latest forecast so the loader's catch block can tell whether
  // data is already on screen, even inside stale setInterval closures.
  const forecastRef = useRef(null);
  useEffect(() => { forecastRef.current = forecast; }, [forecast]);

  // ─── FORECAST LOADER ──────────────────────────────────────────────
  //
  //  Waterfall:
  //    1. Cache fresh (< refresh interval)  → serve immediately, no fetch needed
  //    2. Cache stale / missing  → fetch main auto API
  //    3. Main API soil is null  → try ICON for soil data
  //    4. ICON fails             → fall back to stale cache (any age)
  //    5. No cache but data shown → keep existing data (don't show error)
  //    6. Nothing loaded at all  → show error
  //
  async function loadForecast(loc) {
    const key = forecastCacheKey(loc);
    let staleCache = null;

    // Step 1 — stale-while-revalidate: serve any cached data immediately,
    // then continue to fetch fresh data silently in the background.
    // Only show the loading spinner if there is nothing cached at all.
    const cacheMaxAge = isPro ? FIFTEEN_MIN_MS : THIRTY_MIN_MS;
    try {
      const cached = localStorage.getItem(key);
      if (cached) {
        const { ts, data } = JSON.parse(cached);
        staleCache = { ts, data };
        // Always show cached data immediately - no spinner
        setForecast(data);
        setFetchedAt(new Date(ts));
        // If fresh enough, no need to re-fetch
        if (Date.now() - ts < cacheMaxAge) return;
      }
    } catch (e) { /* ignore bad cache */ }

    // Only show spinner if nothing could be served from cache
    if (!staleCache) { setLoading(true); }
    setError(null);
    try {
      // Step 2 — fetch main forecast
      const r = await fetch(buildForecastUrl(loc));
      if (!r.ok) throw new Error(`Open-Meteo HTTP ${r.status}`);
      const data = await r.json();

      // Step 3 — if main API has no soil data, fetch ICON
      const mainHasSoil = data.hourly?.soil_moisture_0_to_1cm?.some(v => v != null);
      const soilH = mainHasSoil ? null : await fetchSoilHourly(loc);
      const merged = mergeSoil(data, soilH);

      const ts = Date.now();
      setForecast(merged);
      setFetchedAt(new Date(ts));
      try {
        localStorage.setItem(key, JSON.stringify({ ts, data: merged }));
      } catch (e) { /* ignore storage errors */ }

    } catch (e) {
      // Step 4 — API failed, serve stale cache so users still see something
      if (staleCache) {
        setForecast(staleCache.data);
        setFetchedAt(new Date(staleCache.ts)); // old timestamp signals stale data
      } else if (forecastRef.current) {
        // Step 5 — no cache (e.g. an auto-refresh cleared it first), but data
        // is already on screen. Keep showing it rather than replacing the page
        // with a transient network error.
      } else {
        setError(e.message); // Step 6 — nothing loaded yet, surface the error
      }
    }
    finally { setLoading(false); }
  }

  // ─── AIR QUALITY LOADER ───────────────────────────────────────────
  async function loadAirQuality(loc) {
    const key = airQualityCacheKey(loc);
    let stale = null;
    try {
      const cached = localStorage.getItem(key);
      if (cached) {
        const { ts, data } = JSON.parse(cached);
        if (Date.now() - ts < SIX_HOURS_MS) {
          setAirQuality(data);
          return;
        }
        stale = data; // keep stale for fallback
      }
    } catch (e) { /* ignore bad cache */ }
    try {
      const r = await fetch(buildAirQualityUrl(loc));
      if (!r.ok) { if (stale) setAirQuality(stale); return; }
      const data = await r.json();
      setAirQuality(data);
      try {
        localStorage.setItem(key, JSON.stringify({ ts: Date.now(), data }));
      } catch (e) { /* ignore storage errors */ }
    } catch (_) {
      if (stale) setAirQuality(stale); // network failure — use stale silently
    }
  }

  // ─── INITIAL FETCH on location change ─────────────────────────────
  useEffect(() => {
    loadForecast(location);
    loadAirQuality(location);
  }, [location]);

  // ─── 1-MINUTE CLOCK TICK (current-row highlight + live scope) ─────
  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), ONE_MIN_MS);
    return () => clearInterval(id);
  }, []);

  // ─── LIVE SOLAR ELEVATION (updates every minute with now tick) ─────
  const [liveElev, setLiveElev] = useState(0);
  useEffect(() => {
    if (location?.lat == null) return;
    setLiveElev(solarElevationDeg(location.lat, location.lon, now));
  }, [now, location]);

  // ─── AUTO REFRESH (Pro: 15 min, free: 30 min) ─────────────────────
  //  Open-Meteo updates roughly every 15 min; Pro gets tighter polling.
  useEffect(() => {
    const refreshMs = isPro ? FIFTEEN_MIN_MS : THIRTY_MIN_MS;
    const id = setInterval(() => {
      async function refresh() {
        const key = forecastCacheKey(location);
        try { localStorage.removeItem(key); } catch (e) { /* ignore */ }
        await loadForecast(location);

        const aqKey = airQualityCacheKey(location);
        try {
          const cached = localStorage.getItem(aqKey);
          if (cached) {
            const { ts } = JSON.parse(cached);
            if (Date.now() - ts < SIX_HOURS_MS) return;
          }
        } catch (e) { /* ignore */ }
        loadAirQuality(location);
      }
      refresh();
    }, refreshMs);
    return () => clearInterval(id);
  }, [location, isPro]);

  return { forecast, airQuality, loading, error, now, fetchedAt, liveElev };
}