// The Safehouse — main app.
//
// Boots from window.SAFEHOUSE_DATA (static fallbacks in data.js), then
// fetches /api/stats and overlays real values: server status, in-game
// date/time, and online players. Re-fetches every 60s.
//
// Static placeholders for things PZ doesn't expose centrally (weather,
// per-player jobs/locations, leaderboards, dispatch feed) are kept for
// design consistency — see info/README.md for what's live vs flavor.

const REFRESH_MS  = 60_000;
const STALE_AFTER = 8 * 60;  // age_seconds: stop trusting last "up" reading after this
const DEFAULT_SEC_PER_GAME_MIN = 2.5; // PZ default DayLength=3 → 1 in-game min per 2.5 real sec

const MONTHS = [
  "January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December",
];

function periodFromHour(h) {
  if (h == null) return "DAY";
  if (h < 5)  return "NIGHT";
  if (h < 8)  return "DAWN";
  if (h < 17) return "DAY";
  if (h < 20) return "DUSK";
  return "NIGHT";
}

function weekdayFor(y, m, d) {
  try {
    return new Date(y, m - 1, d).toLocaleDateString("en-US", { weekday: "long" });
  } catch { return ""; }
}

const COMPASS_8 = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
function bearingToCompass(deg) {
  if (deg == null) return null;
  const idx = Math.round((((deg % 360) + 360) % 360) / 45) % 8;
  return COMPASS_8[idx];
}

function describeConditions(wx) {
  if (!wx) return null;
  const parts = [];
  if (wx.is_thunder)             parts.push("THUNDERSTORM");
  else if (wx.precipitation > 0.5) parts.push("RAIN");
  else if (wx.precipitation > 0.1) parts.push("DRIZZLE");
  if (wx.fog > 0.5)              parts.push("FOG");
  if (parts.length === 0) {
    if (wx.cloud > 0.7)      parts.push("OVERCAST");
    else if (wx.cloud > 0.4) parts.push("PARTLY CLOUDY");
    else                     parts.push("CLEAR");
  }
  return parts.join(" · ");
}

function mergeWeather(baseWeather, wx, zmw) {
  // Prefer ZomboidManager's weather export (game_state.json) — it has working
  // temperature/wind on B42. Fall back to our SafehouseDashboard mod's fields.
  // Missing values stay null so the UI shows "—" rather than a static fake.
  const cToF = (c) => Math.round(c * 9 / 5 + 32);

  const tempF =
    zmw?.temperature  != null ? cToF(zmw.temperature) :
    wx?.temperature_c != null ? cToF(wx.temperature_c) :
    null;

  const windMph =
    zmw?.wind_intensity != null ? Math.round(zmw.wind_intensity * 30) :
    wx?.wind            != null ? Math.round(wx.wind * 30) :
    null;

  const windDir = wx?.wind_angle != null ? bearingToCompass(wx.wind_angle * 360) : null;

  const humidity =
    wx?.humidity != null ? Math.round(wx.humidity * 100) : null;

  const fog =
    zmw?.fog_intensity != null ? zmw.fog_intensity :
    wx?.fog            != null ? wx.fog :
    null;

  // ZM ships a single-word condition ("clear", "rain", "fog", "snow"); compose
  // ours from intensities for richer text, then fall back.
  const composed = describeConditions({
    is_thunder:    wx?.is_thunder,
    precipitation: zmw?.rain_intensity ?? wx?.precipitation,
    cloud:         wx?.cloud,
    fog:           zmw?.fog_intensity ?? wx?.fog,
  });
  const condition = composed
    || (zmw?.condition ? zmw.condition.toUpperCase() : null)
    || baseWeather.condition;

  return { ...baseWeather, temp_f: tempF, wind_mph: windMph, wind_dir: windDir, humidity, fog, condition };
}

function mergeLive(base, live, syncedAt) {
  if (!live) return base;
  const a = live.a2s        || {};
  const w = live.world      || null;
  const c = live.characters || {};
  const ageS = live.age_seconds ?? 0;

  // Status logic: stale push → don't trust; explicit a2s down → OFFLINE.
  const status =
    ageS > STALE_AFTER ? "OFFLINE"  :
    !a.up              ? "OFFLINE"  :
                         "ONLINE";

  const merged = JSON.parse(JSON.stringify(base));

  // Prefer the Lua mod's getOnlinePlayers() count when present — A2S query
  // can lag (it doesn't always advertise players before their first ack).
  const luaCount = Array.isArray(w?.online) ? w.online.length : null;
  const a2sCount = a.up ? (a.players ?? 0) : 0;
  merged.server = {
    ...merged.server,
    status,
    population:     luaCount ?? a2sCount,
    population_max: a.max_players ?? merged.server.population_max,
  };

  if (w) {
    merged.ingame = {
      ...merged.ingame,
      day:     (w.nights_survived ?? 0) + 1,
      weekday: weekdayFor(w.year, w.month, w.day) || merged.ingame.weekday,
      month:   MONTHS[(w.month - 1) % 12]         || merged.ingame.month,
      date:    w.day    ?? merged.ingame.date,
      year:    w.year   ?? merged.ingame.year,
      hour:    w.hour   ?? merged.ingame.hour,
      minute:  w.minute ?? merged.ingame.minute,
      period:  periodFromHour(w.hour),
    };

    if (Array.isArray(w.online)) {
      // Real names from the Lua mod; reuse static loc/job/status as flavor.
      merged.online = w.online.map((p, i) => ({
        name:   (p.name && p.name.trim()) || p.username || "Unknown",
        loc:    base.online[i]?.loc    ?? "—",
        job:    base.online[i]?.job    ?? "—",
        days:   base.online[i]?.days   ?? 0,
        status: base.online[i]?.status ?? "ALIVE",
      }));
    }

    merged.weather = mergeWeather(merged.weather, w.weather, live.zm?.game_state?.weather);
  }

  // Replace mock 5-day forecast with real last-N-hours history from sqlite.
  if (Array.isArray(live.weather_history) && live.weather_history.length) {
    const condIcon = { clear: "sun", rain: "rain", fog: "fog", storm: "storm", snow: "fog" };
    merged.weather = {
      ...merged.weather,
      forecast: live.weather_history.map((h) => {
        const d = new Date(h.ts_end * 1000);
        const label = `${String(d.getHours()).padStart(2, "0")}:00`;
        const toF = (c) => c == null ? null : Math.round(c * 9 / 5 + 32);
        return {
          day:     label,
          icon:    condIcon[h.condition] || "cloud",
          hi:      toF(h.max_c),
          lo:      toF(h.min_c),
          samples: h.samples,
        };
      }),
    };
  }

  if (c.total_characters != null) merged.characters_total = c.total_characters;
  if (c.alive            != null) merged.characters_alive = c.alive;

  // Leaderboard: persistent roster from the EC2's sqlite DB if present,
  // otherwise fall back to ZomboidManager's currently-online snapshot.
  const roster = Array.isArray(live.roster) && live.roster.length
    ? live.roster
    : (live.zm?.player_stats?.players || []).map((p) => ({
        username:       p.username,
        character_name: null,
        zeds:           p.zombie_kills   || 0,
        hours:          p.hours_survived || 0,
        skills:         Object.values(p.skills || {})
                          .reduce((a, b) => a + (Number(b) || 0), 0),
      }));

  const sortDesc = (key) =>
    [...roster]
      .map((r) => ({ name: r.character_name || r.username, val: key(r) }))
      .sort((a, b) => b.val - a.val);

  merged.leaderboard = roster.length ? {
    zombies: sortDesc((r) => r.zeds   || 0),
    hours:   sortDesc((r) => r.hours  || 0),
    skills:  sortDesc((r) => r.skills || 0),
  } : { zombies: [], hours: [], skills: [] };

  // For clock interpolation: when this snapshot was received locally + the
  // PZ tick rate so the Hero clock can advance between syncs at the right
  // cadence (default DayLength=3 → 1 in-game min per 2.5 real sec).
  merged.live_meta = {
    age_seconds: ageS,
    pushed_at: live.pushed_at,
    synced_at: syncedAt ?? Date.now(),
    world_written_at: w?.written_at ?? null,
    seconds_per_game_minute: w?.seconds_per_game_minute ?? DEFAULT_SEC_PER_GAME_MIN,
  };
  return merged;
}

const App = () => {
  const base = window.SAFEHOUSE_DATA;
  const [tick, setTick]         = useState(0);
  const [liveSync, setLiveSync] = useState(null); // { live, syncedAt } — local clock at receipt
  const [bootDone, setBootDone] = useState(false);

  useEffect(() => {
    const id = setInterval(() => setTick((x) => x + 1), 1000);
    return () => clearInterval(id);
  }, []);

  useEffect(() => {
    let cancelled = false;
    const load = async () => {
      try {
        const res = await fetch("/api/stats", { cache: "no-store" });
        if (!res.ok) return;
        const json = await res.json();
        if (!cancelled) setLiveSync({ live: json, syncedAt: Date.now() });
      } catch { /* keep prior state on transient failure */ }
    };
    load();
    const id = setInterval(load, REFRESH_MS);
    return () => { cancelled = true; clearInterval(id); };
  }, []);

  const data = useMemo(
    () => mergeLive(base, liveSync?.live, liveSync?.syncedAt),
    [base, liveSync]
  );

  return (
    <>
      {!bootDone && <BootScreen onDone={() => setBootDone(true)} />}
      <div className="bezel">
        <div className="tube flicker-on" data-screen-label="01 Safehouse Terminal">
          <div className="sweep"></div>

          <StatusBar data={data} tick={tick} />
          <Hero data={data} tick={tick} />

          <div className="grid">
            <div className="col">
              <ConnectPanel data={data} />
              <SurvivorsPanel data={data} />
              <WeatherPanel data={data} />
              <MapPanel data={data} />
              <NotePanel />
            </div>
            <div className="col">
              <LeaderboardPanel data={data} />
              <RulesPanel data={data} />
              <ModsPanel data={data} />
            </div>
          </div>

          <Ticker data={data} />
          <TubeControls data={data} />
        </div>

        <div className="bezel-led"><span className="led"></span>POWER</div>
        <div className="bezel-tag">SAFEHOUSE TERMINAL · MODEL ZX-93</div>
      </div>
    </>
  );
};

const BootScreen = ({ onDone }) => {
  const [lines, setLines] = useState([]);
  const seq = [
    "SAFEHOUSE TERMINAL ZX-93 · BIOS 4.7",
    "MEMORY CHECK ........ 640K OK",
    "TUNING TO BROADCAST 47.3 MHz ........ LOCKED",
    "DECRYPTING SURVIVOR ROSTER .......... DONE",
    "CONNECTING TO safe-house.cc:16261 ... OK",
    "WELCOME TO KNOX COUNTY.",
  ];
  useEffect(() => {
    let i = 0;
    const id = setInterval(() => {
      i++;
      setLines(seq.slice(0, i));
      if (i >= seq.length) {
        clearInterval(id);
        setTimeout(() => onDone && onDone(), 700);
      }
    }, 230);
    return () => clearInterval(id);
  }, []);
  return (
    <div className="boot">
      <pre>{lines.map((l) => `▸ ${l}`).join("\n")}<span className="cursor"></span></pre>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
