// Torpen — couche données live + fallback mock
// Wraps Open-Meteo Forecast + Marine APIs et expose useTorpenData(settings)
// avec fallback gracieux sur MOCK_DAYS si offline ou erreur.

const TORPEN_LOC = { lat: 47.7188, lon: -3.3727, name: "Anse Zanflamme" };

// Defaults — tirant d'eau Elfe 8 = 0.70 m
const DEFAULT_SETTINGS = {
  draft: 0.70,
  chartDepth: -0.6,        // sole asséchant 0.6m sous le zéro hydro
  exitChannelDepth: -1.0,  // chenal: assèche de 1.0m
  msl2cd: 2.7,             // décalage MSL → ZH (Lorient ≈ 2.7m)
  thresholds: {
    windGo: 12, windCaution: 18,
    gustGo: 18, gustCaution: 25,
    waveGo: 0.8, waveCaution: 1.5,
    depthGo: 0.5, depthCaution: 0.2,
    visMinKm: 2,
    coefCaution: 95, coefNoGo: 110,
  },
  outing: { morningMarginMin: 30, returnMarginMin: 60, minDuration: 4 },
};

async function fetchJSON(url) {
  const r = await fetch(url);
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return r.json();
}

async function fetchWeather() {
  const p = new URLSearchParams({
    latitude: TORPEN_LOC.lat, longitude: TORPEN_LOC.lon,
    hourly: ["temperature_2m","precipitation","weathercode","cloudcover","visibility","windspeed_10m","windgusts_10m","winddirection_10m"].join(","),
    daily: "sunrise,sunset",
    timezone: "Europe/Paris",
    forecast_days: 7,
    windspeed_unit: "kn",
  });
  return fetchJSON(`https://api.open-meteo.com/v1/forecast?${p}`);
}

async function fetchMarine() {
  const p = new URLSearchParams({
    latitude: TORPEN_LOC.lat, longitude: TORPEN_LOC.lon,
    hourly: ["wave_height","wave_direction","wave_period","sea_level_height_msl"].join(","),
    timezone: "Europe/Paris",
    forecast_days: 7,
  });
  return fetchJSON(`https://marine-api.open-meteo.com/v1/marine?${p}`);
}

function scoreHourLive(h, settings) {
  const t = settings.thresholds;
  const wind = h.wind ?? 0, gust = h.gust ?? 0, wave = h.wave ?? 0;
  const ws = wind <= t.windGo ? 0 : wind <= t.windCaution ? 1 : 2;
  const gs = gust <= t.gustGo ? 0 : gust <= t.gustCaution ? 1 : 2;
  const ps = wave <= t.waveGo ? 0 : wave <= t.waveCaution ? 1 : 2;
  let ds = 0;
  if (h.depth == null) ds = 1;
  else if (h.depth < t.depthCaution) ds = 2;
  else if (h.depth < t.depthGo) ds = 1;
  let cs = 0;
  if (h.tideCoef >= t.coefNoGo && wind >= t.windCaution) cs = 2;
  else if (h.tideCoef >= t.coefNoGo) cs = 1;
  else if (h.tideCoef >= t.coefCaution && wind >= t.windCaution) cs = 1;
  let vs = 0;
  const vk = h.visibility != null ? h.visibility / 1000 : 99;
  if (vk < t.visMinKm) vs = 2;
  else if (vk < 5) vs = 1;
  let rs = 0;
  if (h.precip > 5) rs = 2;
  else if (h.precip > 1) rs = 1;
  const total = Math.max(ws, gs, ps, ds, cs, vs, rs);
  const verdict = !h.navigable ? 'nogo' : (total === 0 ? 'go' : total === 1 ? 'caution' : 'nogo');
  return { wind: ws, gust: gs, wave: ps, depth: ds, coef: cs, vis: vs, rain: rs, total, verdict };
}

// Estimate tide coefficient from MSL amplitude (rough)
function estimateCoef(seaMslSeries) {
  // sample 12 hours window
  const w = seaMslSeries.slice(0, 12);
  const max = Math.max(...w), min = Math.min(...w);
  const range = max - min;
  // Lorient: range ~2.5m at coef 50, ~5.5m at coef 110
  return Math.round(50 + (range - 2.5) * 20);
}

function buildLiveSeries(weather, marine, settings) {
  const wT = weather.hourly.time;
  const mT = marine.hourly.time;
  const series = [];
  // Daily sunrise/sunset, mapped per dayKey
  const daily = weather.daily;
  const sunByDate = {};
  for (let i = 0; i < daily.time.length; i++) {
    sunByDate[daily.time[i]] = {
      sunrise: new Date(daily.sunrise[i]),
      sunset: new Date(daily.sunset[i]),
    };
  }
  for (let i = 0; i < wT.length; i++) {
    const date = new Date(wT[i]);
    const dayStr = wT[i].slice(0, 10);
    const sun = sunByDate[dayStr] || {};
    // Find marine index by time match
    const mi = mT.indexOf(wT[i]);
    const seaMsl = mi >= 0 ? marine.hourly.sea_level_height_msl[mi] : null;
    const tide = seaMsl != null ? seaMsl + settings.msl2cd : null;
    const depthMooring = tide != null ? settings.chartDepth + tide - settings.draft : null;
    const depthChannel = tide != null ? settings.exitChannelDepth + tide - settings.draft : null;
    const depth = (depthMooring != null && depthChannel != null) ? Math.min(depthMooring, depthChannel) : depthMooring;
    const sr = sun.sunrise, ss = sun.sunset;
    const navigable = sr && ss
      ? date >= new Date(sr.getTime() + settings.outing.morningMarginMin * 60000) && date <= new Date(ss.getTime() - settings.outing.returnMarginMin * 60000)
      : true;
    const h = {
      date, time: wT[i], dayKey: date.toDateString(),
      temp: weather.hourly.temperature_2m[i],
      precip: weather.hourly.precipitation[i] || 0,
      cloud: weather.hourly.cloudcover[i],
      visibility: weather.hourly.visibility[i],
      wind: weather.hourly.windspeed_10m[i] || 0,
      gust: weather.hourly.windgusts_10m[i] || 0,
      windDir: weather.hourly.winddirection_10m[i] || 0,
      wave: mi >= 0 ? marine.hourly.wave_height[mi] : 0,
      waveDir: mi >= 0 ? marine.hourly.wave_direction[mi] : 0,
      wavePeriod: mi >= 0 ? marine.hourly.wave_period[mi] : 6,
      seaMsl, tide, depthMooring, depthChannel, depth,
      sunrise: sr, sunset: ss, navigable,
    };
    // aliases for legacy dirs
    h.windSpeed = h.wind; h.windGust = h.gust; h.swell = h.wave;
    series.push(h);
  }
  // tideCoef per day
  const byDay = {};
  for (const h of series) {
    if (!byDay[h.dayKey]) byDay[h.dayKey] = [];
    byDay[h.dayKey].push(h);
  }
  for (const k in byDay) {
    const tides = byDay[k].map(h => h.tide).filter(t => t != null);
    const range = tides.length ? Math.max(...tides) - Math.min(...tides) : 3;
    const coef = Math.round(50 + (range - 2.5) * 20);
    byDay[k].forEach(h => h.tideCoef = coef);
  }
  for (const h of series) {
    h.score = scoreHourLive(h, settings);
  }
  return series;
}

function buildLiveDays(series, settings) {
  const byDay = {};
  for (const h of series) {
    if (!byDay[h.dayKey]) byDay[h.dayKey] = [];
    byDay[h.dayKey].push(h);
  }
  const wins = findLiveWindows(series, settings);
  const days = [];
  for (const k of Object.keys(byDay)) {
    const hours = byDay[k];
    const dayWins = wins.filter(w => w.start.dayKey === k);
    const bestWin = dayWins.length ? dayWins.reduce((a, b) => b.hours > a.hours ? b : a) : null;
    const d = hours[0].date;
    const dow = ['DIM','LUN','MAR','MER','JEU','VEN','SAM'][d.getDay()];
    const months = ['jan','fév','mar','avr','mai','juin','juil','août','sep','oct','nov','déc'];
    days.push({
      key: k, hours, label: dow,
      dateStr: `${dow.toLowerCase()} ${d.getDate()} ${months[d.getMonth()]}`,
      tideCoef: hours[0].tideCoef,
      window: bestWin,
      sunrise: hours[0].sunrise,
      sunset: hours[0].sunset,
    });
  }
  return days;
}

function findLiveWindows(series, settings) {
  const wins = [];
  let cur = null;
  for (const h of series) {
    const ok = h.score && h.score.total <= 1 && h.navigable;
    if (ok) {
      if (!cur) cur = { start: h, end: h, mode: h.score.total === 0 ? 'go' : 'caution' };
      else { cur.end = h; if (h.score.total === 1) cur.mode = 'caution'; }
    } else if (cur) {
      cur.hours = Math.round((cur.end.date - cur.start.date) / 3600000) + 1;
      cur.dayKey = cur.start.dayKey;
      if (cur.hours >= (settings.outing.minDuration || 4)) wins.push(cur);
      cur = null;
    }
  }
  if (cur) {
    cur.hours = Math.round((cur.end.date - cur.start.date) / 3600000) + 1;
    cur.dayKey = cur.start.dayKey;
    if (cur.hours >= (settings.outing.minDuration || 4)) wins.push(cur);
  }
  return wins;
}

function dayVerdictLive(day) {
  if (!day.window) return 'nogo';
  return day.window.mode;
}

function windowStatsLive(day, series) {
  const slice = day.window ? series.slice(series.indexOf(day.window.start), series.indexOf(day.window.end) + 1) : day.hours;
  return {
    windMax: Math.max(...slice.map(h => h.wind ?? 0)),
    windAvg: slice.reduce((a, h) => a + (h.wind ?? 0), 0) / slice.length,
    gustMax: Math.max(...slice.map(h => h.gust ?? 0)),
    swellMax: Math.max(...slice.map(h => h.wave ?? 0)),
    wavePeriod: slice.reduce((a, h) => a + (h.wavePeriod ?? 0), 0) / slice.length,
    rainMax: Math.max(...slice.map(h => h.precip ?? 0)),
    visMin: Math.min(...slice.map(h => (h.visibility != null ? h.visibility / 1000 : 99))),
    depthMin: Math.min(...slice.map(h => h.depth ?? 99)),
  };
}

// Hook : returns { status, days, series, error, updatedAt, refresh }
function useTorpenData(settings) {
  const [state, setState] = React.useState({ status: 'loading', series: null, days: null, error: null, updatedAt: null });
  const [tick, setTick] = React.useState(0);

  React.useEffect(() => {
    let cancelled = false;
    setState(s => ({ ...s, status: 'loading' }));
    Promise.all([fetchWeather(), fetchMarine()]).then(([weather, marine]) => {
      if (cancelled) return;
      const series = buildLiveSeries(weather, marine, settings);
      const days = buildLiveDays(series, settings);
      setState({ status: 'live', series, days, error: null, updatedAt: new Date() });
    }).catch(err => {
      if (cancelled) return;
      // Fallback to mock
      console.warn('Torpen API offline, using mock:', err);
      setState({
        status: 'mock',
        series: window.MOCK_SERIES,
        days: window.MOCK_DAYS,
        error: String(err.message || err),
        updatedAt: new Date(),
      });
    });
    return () => { cancelled = true; };
  }, [tick]);

  // Re-score when settings change without refetch
  const computed = React.useMemo(() => {
    if (!state.series || state.status !== 'live') return state;
    for (const h of state.series) {
      h.tide = h.seaMsl != null ? h.seaMsl + settings.msl2cd : null;
      h.depthMooring = h.tide != null ? settings.chartDepth + h.tide - settings.draft : null;
      h.depthChannel = h.tide != null ? settings.exitChannelDepth + h.tide - settings.draft : null;
      h.depth = (h.depthMooring != null && h.depthChannel != null) ? Math.min(h.depthMooring, h.depthChannel) : h.depthMooring;
      if (h.sunrise && h.sunset) {
        h.navigable = h.date >= new Date(h.sunrise.getTime() + settings.outing.morningMarginMin * 60000) && h.date <= new Date(h.sunset.getTime() - settings.outing.returnMarginMin * 60000);
      }
      h.score = scoreHourLive(h, settings);
    }
    const days = buildLiveDays(state.series, settings);
    return { ...state, days };
  }, [state.series, state.status, JSON.stringify(settings)]);

  return { ...computed, refresh: () => setTick(t => t + 1) };
}

Object.assign(window, {
  TORPEN_LOC, DEFAULT_SETTINGS,
  fetchWeather, fetchMarine, scoreHourLive, buildLiveSeries, buildLiveDays,
  findLiveWindows, dayVerdictLive, windowStatsLive,
  useTorpenData,
});
