// data-sources.jsx — realistic mock data + real API call shapes behind one interface.
// In preview: returns mock values that drift like the real thing.
// In production: hits /api/* on the Cloudflare Worker (which holds the keys).
//
// Swap between modes by editing DATA_MODE below or setting window.__DD_REAL__ = true.

const DATA_MODE = (typeof window !== 'undefined' && window.__DD_REAL__) ? 'real' : 'mock';

// ────────── utilities ──────────
const rand = (min, max) => min + Math.random() * (max - min);
const drift = (v, pct = 0.002) => v * (1 + rand(-pct, pct));

// ────────── CLOCK ────────── (no API — just system time, assumed ET on Pi)
function useClock() {
  const [now, setNow] = React.useState(() => new Date());
  React.useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  return now;
}

// ────────── WEATHER (Open-Meteo, no key needed — safe to call directly too) ──────────
// 02169 = Quincy MA → 42.2529, -71.0023
const WX_URL = 'https://api.open-meteo.com/v1/forecast'
  + '?latitude=42.2529&longitude=-71.0023'
  + '&current=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m,is_day'
  + '&daily=temperature_2m_max,temperature_2m_min,sunrise,sunset,weather_code'
  + '&hourly=temperature_2m,weather_code'
  + '&temperature_unit=fahrenheit&wind_speed_unit=mph'
  + '&timezone=America%2FNew_York&forecast_days=7';

function mockWeather() {
  const now = new Date();
  const hour = now.getHours();
  const isDay = hour >= 6 && hour < 19;
  const baseTemp = 52 + Math.sin((hour - 6) / 12 * Math.PI) * 10;
  const dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
  const todayIdx = now.getDay();
  return {
    tempF: Math.round(drift(baseTemp, 0.01)),
    code: 1,
    condition: 'Mostly clear',
    windMph: 8,
    windDir: 'NE',
    hi: 62, lo: 44,
    isDay,
    sunrise: '6:12 AM', sunset: '7:28 PM',
    feelsF: Math.round(baseTemp - 2),
    humidity: 48, dewF: 41, uv: 4,
    hourly: Array.from({ length: 6 }, (_, i) => ({
      hour: `${((hour + i) % 12) || 12}${(hour + i) % 24 < 12 ? 'a' : 'p'}`,
      tempF: Math.round(baseTemp + i * 0.8),
      code: i < 3 ? 1 : 2,
    })),
    daily: Array.from({ length: 7 }, (_, i) => ({
      day: i === 0 ? 'TDY' : dayNames[(todayIdx + i) % 7].toUpperCase(),
      hi: 58 + Math.round(Math.sin(i * 0.9) * 8 + i * 1.2),
      lo: 42 + Math.round(Math.sin(i * 0.7) * 5 + i * 0.8),
      code: [1, 1, 2, 3, 61, 2, 1][i],
    })),
    updated: now,
    error: null,
  };
}

async function fetchWeather() {
  if (DATA_MODE === 'mock') return mockWeather();
  try {
    const r = await fetch(WX_URL);
    const d = await r.json();
    const c = d.current, day = d.daily;
    const windDeg = c.wind_direction_10m;
    const dirs = ['N','NE','E','SE','S','SW','W','NW'];
    const windDir = dirs[Math.round(windDeg / 45) % 8];
    const fmtTime = (iso) => new Date(iso).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: 'America/New_York' });
    const nowH = new Date().getHours();
    return {
      tempF: Math.round(c.temperature_2m),
      code: c.weather_code,
      condition: (window.WX_CODE_MAP?.[c.weather_code]?.label) || 'Weather',
      windMph: Math.round(c.wind_speed_10m),
      windDir,
      hi: Math.round(day.temperature_2m_max[0]),
      lo: Math.round(day.temperature_2m_min[0]),
      isDay: !!c.is_day,
      sunrise: fmtTime(day.sunrise[0]),
      sunset: fmtTime(day.sunset[0]),
      feelsF: Math.round(c.temperature_2m - 2),
      humidity: null, dewF: null, uv: null,
      hourly: d.hourly.time.slice(nowH, nowH + 6).map((t, i) => ({
        hour: new Date(t).toLocaleTimeString('en-US', { hour: 'numeric', hour12: true }).replace(' ', '').toLowerCase().replace('m', ''),
        tempF: Math.round(d.hourly.temperature_2m[nowH + i]),
        code: d.hourly.weather_code[nowH + i],
      })),
      daily: (day.time || []).slice(0, 7).map((t, i) => ({
        day: i === 0 ? 'TDY' : new Date(t).toLocaleDateString('en-US', { weekday: 'short', timeZone: 'America/New_York' }).toUpperCase(),
        hi: Math.round(day.temperature_2m_max[i]),
        lo: Math.round(day.temperature_2m_min[i]),
        code: (day.weather_code && day.weather_code[i]) || 0,
      })),
      updated: new Date(),
      error: null,
    };
  } catch (e) {
    return { ...mockWeather(), error: 'offline' };
  }
}

// ────────── CRCL (Twelve Data via Worker proxy) ──────────
// Production call: fetch('/api/crcl') — the Worker adds your API key server-side.
let mockCrclState = { price: 142.88, prev: 139.67 };

function isMarketOpen(d = new Date()) {
  // Rough US equity hours in ET — good enough for UI state
  const et = new Date(d.toLocaleString('en-US', { timeZone: 'America/New_York' }));
  const day = et.getDay();
  const mins = et.getHours() * 60 + et.getMinutes();
  return day >= 1 && day <= 5 && mins >= 570 && mins < 960; // 9:30–16:00
}

function mockCrcl() {
  mockCrclState.price = Math.max(50, drift(mockCrclState.price, 0.004));
  const open = isMarketOpen();
  const changed = +(mockCrclState.price - mockCrclState.prev).toFixed(2);
  const pct = +(changed / mockCrclState.prev * 100).toFixed(2);
  const spark = Array.from({ length: 32 }, (_, i) =>
    mockCrclState.prev + Math.sin(i * 0.35) * 2 + (i / 32) * changed + rand(-0.8, 0.8));
  return {
    symbol: 'CRCL',
    price: +mockCrclState.price.toFixed(2),
    prev: mockCrclState.prev,
    changeAbs: changed,
    changePct: pct,
    open: 140.10, high: 143.12, low: 139.88,
    vol: '4.12M', mktCap: '34.2B',
    wk52hi: 301.50, wk52lo: 58.20,
    marketOpen: open,
    extended: !open ? +(mockCrclState.price + rand(-0.3, 0.3)).toFixed(2) : null,
    spark,
    updated: new Date(),
    error: null,
  };
}

async function fetchCrcl() {
  if (DATA_MODE === 'mock') return mockCrcl();
  try {
    const r = await fetch('/api/crcl');
    if (!r.ok) throw new Error('api');
    const d = await r.json();
    return { ...d, updated: new Date(), error: null };
  } catch (e) {
    return { ...mockCrcl(), error: 'offline' };
  }
}

// ────────── USDC (CoinGecko via Worker proxy) ──────────
let mockUsdcState = 60.4;
function mockUsdc() {
  mockUsdcState = drift(mockUsdcState, 0.0008);
  return {
    symbol: 'USDC',
    mktCap: +mockUsdcState.toFixed(2),   // in B
    mktCapStr: `$${mockUsdcState.toFixed(2)}B`,
    change24h: +rand(-0.15, 0.15).toFixed(2),
    price: 1.00,
    vol24h: 7.1,
    supply: +mockUsdcState.toFixed(2),
    rank: 7,
    updated: new Date(),
    error: null,
  };
}
async function fetchUsdc() {
  if (DATA_MODE === 'mock') return mockUsdc();
  try {
    const r = await fetch('/api/usdc');
    if (!r.ok) throw new Error('api');
    const d = await r.json();
    return { ...d, updated: new Date(), error: null };
  } catch (e) {
    return { ...mockUsdc(), error: 'offline' };
  }
}

// ────────── TIDE (NOAA Tides & Currents via Worker proxy) ──────────
// Mock cycles a real ~12.42 h tidal curve anchored to wall-clock time so the
// preview demonstrates the rising/falling toggle and all 5 image buckets.
const TIDE_CYCLE_MS = 12.42 * 3600 * 1000;
const TIDE_LOW_FT = 0.4;
const TIDE_HIGH_FT = 9.8;

function mockTide() {
  const now = Date.now();
  // Phase such that high tide lands at the cycle midpoint; offset by an
  // arbitrary anchor so different load times show different states.
  const ANCHOR = new Date('2026-01-01T05:00:00Z').getTime();
  const phase = ((now - ANCHOR) % TIDE_CYCLE_MS + TIDE_CYCLE_MS) % TIDE_CYCLE_MS;
  const t = phase / TIDE_CYCLE_MS;                // 0..1 over one cycle
  // Low at t=0 → high at t=0.5 → low at t=1
  const cosFactor = 0.5 - 0.5 * Math.cos(2 * Math.PI * t);
  const heightFt = TIDE_LOW_FT + (TIDE_HIGH_FT - TIDE_LOW_FT) * cosFactor;
  const direction = t < 0.5 ? 'rising' : 'falling';

  const fmt = (ms) => new Date(ms).toLocaleTimeString('en-US', {
    hour: 'numeric', minute: '2-digit', timeZone: 'America/New_York',
  });

  // Half-cycle progress (used both for image bucketing and for finding the
  // surrounding extremes).
  const halfCycle = TIDE_CYCLE_MS / 2;
  const sinceLastExtreme = (t < 0.5 ? phase : phase - halfCycle);
  const untilNextExtreme = halfCycle - sinceLastExtreme;
  const prevTime = now - sinceLastExtreme;
  const nextTime = now + untilNextExtreme;
  const prevType = direction === 'rising' ? 'L' : 'H';
  const nextType = direction === 'rising' ? 'H' : 'L';
  const prevHeight = direction === 'rising' ? TIDE_LOW_FT : TIDE_HIGH_FT;
  const nextHeight = direction === 'rising' ? TIDE_HIGH_FT : TIDE_LOW_FT;

  const heightFraction = Math.min(1, Math.max(0,
    (heightFt - TIDE_LOW_FT) / (TIDE_HIGH_FT - TIDE_LOW_FT)
  ));
  const imageIdx = Math.min(4, Math.floor(heightFraction * 5)) + 1;

  return {
    heightFt: +heightFt.toFixed(1),
    direction,
    pct: +heightFraction.toFixed(3),
    imageIdx,
    next: { type: nextType, timeStr: fmt(nextTime), heightFt: nextHeight },
    prev: { type: prevType, timeStr: fmt(prevTime), heightFt: prevHeight },
    station: '8443970',
    updated: new Date(),
    error: null,
  };
}

async function fetchTide() {
  if (DATA_MODE === 'mock') return mockTide();
  try {
    const r = await fetch('/api/tide');
    if (!r.ok) throw new Error('api');
    const d = await r.json();
    if (d.error) throw new Error(d.error);
    return { ...d, updated: new Date(), error: null };
  } catch (e) {
    return { ...mockTide(), error: 'offline' };
  }
}

// ────────── FLIGHTS (AirLabs → OpenSky via Worker proxy) ──────────
// 02169 center: 42.2529, -71.0023. 5-mile radius.
const MOCK_FLIGHTS = [
  { flight: 'JBU1432', from: 'LGA', to: 'BOS', alt: 32000, speed: 461, progress: 0.42 },
  { flight: 'DAL2245', from: 'ATL', to: 'BOS', alt: 28500, speed: 438, progress: 0.68 },
  { flight: 'AAL198',  from: 'BOS', to: 'ORD', alt: 18000, speed: 392, progress: 0.22 },
  { flight: 'UAL441',  from: 'BOS', to: 'SFO', alt: 35000, speed: 478, progress: 0.15 },
  { flight: 'SWA1102', from: 'BWI', to: 'BOS', alt: 24000, speed: 412, progress: 0.55 },
  { flight: 'FDX1234', from: 'MEM', to: 'BOS', alt: 30000, speed: 445, progress: 0.79 },
];

let mockFlightIdx = 0;
function mockFlight() {
  const f = MOCK_FLIGHTS[mockFlightIdx % MOCK_FLIGHTS.length];
  mockFlightIdx = (mockFlightIdx + 1) % MOCK_FLIGHTS.length;
  return { ...f, updated: new Date(), error: null };
}
async function fetchFlight() {
  if (DATA_MODE === 'mock') return mockFlight();
  try {
    const r = await fetch('/api/flight');
    if (!r.ok) return { flight: null, updated: new Date(), error: `worker_${r.status}`, diag: null };
    const d = await r.json();
    if (!d || !d.flight) {
      return {
        flight: null,
        updated: new Date(),
        error: d?._diag?.error || null,
        diag: d?._diag || null,
      };
    }
    return { ...d, updated: new Date(), error: null };
  } catch (e) {
    return { flight: null, updated: new Date(), error: 'offline', diag: null };
  }
}

// IATA → city name map. Loaded once from /airports.json (built from the
// public-domain OurAirports dataset by scripts/build-airports.mjs).
// Rendered as "Name (CODE)" in the ticker; missing codes fall back to the code.
let AIRPORT_NAMES = {};
fetch('/airports.json')
  .then(r => r.ok ? r.json() : {})
  .then(m => { AIRPORT_NAMES = m; })
  .catch(() => {});

function airportLabel(code) {
  if (!code || code === '—') return code || '';
  const name = AIRPORT_NAMES[code];
  return name ? `${name} (${code})` : code;
}

Object.assign(window, {
  DATA_MODE, useClock, fetchWeather, fetchCrcl, fetchUsdc, fetchFlight, fetchTide, isMarketOpen,
  airportLabel,
});
