// store.jsx — data model, localStorage persistence, geo + geometry helpers
// Everything is attached to window for cross-<script> sharing.

const STORAGE_KEY = "wmc.state.v1";

// ── Seed data ────────────────────────────────────────────────
// Simulated "you are here" — central London (used when GPS is unavailable)
const SIM_LOCATION = { lat: 51.51138, lng: -0.11982 };

function seedState() {
  const now = Date.now();
  return {
    cars: [
      {
        id: "audi",
        name: "Audi A1",
        colorName: "Silver",
        color: "#c3c8cc",
        emoji: "🚗",
        isDefault: true,
        current: {
          lat: 51.51015,
          lng: -0.12355,
          note: "Level 3 · Bay 42 — NCP Drury Lane",
          accuracy: 8,
          timestamp: now - 1000 * 60 * 52, // 52 min ago
        },
        history: [
          { lat: 51.5074, lng: -0.1247, note: "Outside the office", timestamp: now - 1000 * 60 * 60 * 27 },
          { lat: 51.5033, lng: -0.1196, note: "Embankment meter", timestamp: now - 1000 * 60 * 60 * 51 },
          { lat: 51.5155, lng: -0.1418, note: "Selfridges car park", timestamp: now - 1000 * 60 * 60 * 96 },
        ],
      },
      {
        id: "t5",
        name: "VW T5.1",
        colorName: "Toffee Brown",
        color: "#8a5a2b",
        emoji: "🚐",
        isDefault: false,
        current: null,
        history: [
          { lat: 51.4769, lng: -0.0005, note: "Greenwich Park gate", timestamp: now - 1000 * 60 * 60 * 30 },
          { lat: 51.5391, lng: -0.1426, note: "Camden side street", timestamp: now - 1000 * 60 * 60 * 74 },
        ],
      },
    ],
    selectedCarId: "audi",
  };
}

function loadState() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return seedState();
    const parsed = JSON.parse(raw);
    if (!parsed || !Array.isArray(parsed.cars)) return seedState();
    return parsed;
  } catch (e) {
    return seedState();
  }
}

function saveState(state) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
  } catch (e) {
    /* quota / private mode — ignore */
  }
}

// ── Backend sync ─────────────────────────────────────────────
// The server (Express + SQLite) is the source of truth; localStorage is an
// offline cache. Mutations apply optimistically, then POST to the API.
const API_BASE = "/api";

// Fire a mutation at the backend. Fire-and-forget — UI already updated
// optimistically, and localStorage holds the latest copy if we're offline.
function apiSend(method, path, body) {
  try {
    return fetch(API_BASE + path, {
      method,
      headers: body ? { "Content-Type": "application/json" } : undefined,
      body: body ? JSON.stringify(body) : undefined,
      keepalive: true,
    }).catch(() => {});
  } catch (e) {
    return Promise.resolve();
  }
}

// React hook: single source of truth, persisted to server + localStorage.
function useStore() {
  const [state, setState] = React.useState(loadState);
  React.useEffect(() => { saveState(state); }, [state]);

  // On mount, hydrate from the backend (falls back to the cached state above).
  React.useEffect(() => {
    let cancelled = false;
    fetch(API_BASE + "/state")
      .then((r) => (r.ok ? r.json() : null))
      .then((server) => {
        if (!cancelled && server && Array.isArray(server.cars)) setState(server);
      })
      .catch(() => { /* offline — keep cached state */ });
    return () => { cancelled = true; };
  }, []);

  const api = React.useMemo(() => ({
    selectCar(id) {
      setState((s) => ({ ...s, selectedCarId: id }));
      apiSend("PUT", "/selected", { id });
    },
    addCar(car) {
      const id = "car_" + Math.random().toString(36).slice(2, 8);
      setState((s) => {
        const isFirst = s.cars.length === 0;
        const next = { id, current: null, history: [], isDefault: isFirst, ...car };
        return { ...s, cars: [...s.cars, next], selectedCarId: s.selectedCarId || id };
      });
      apiSend("POST", "/cars", { id, ...car });
      return id;
    },
    updateCar(id, patch) {
      setState((s) => ({
        ...s,
        cars: s.cars.map((c) => (c.id === id ? { ...c, ...patch } : c)),
      }));
      apiSend("PATCH", "/cars/" + id, patch);
    },
    deleteCar(id) {
      setState((s) => {
        const remaining = s.cars.filter((c) => c.id !== id);
        // keep a default
        if (remaining.length && !remaining.some((c) => c.isDefault)) {
          remaining[0] = { ...remaining[0], isDefault: true };
        }
        let sel = s.selectedCarId;
        if (sel === id) sel = remaining.find((c) => c.isDefault)?.id || remaining[0]?.id || null;
        return { ...s, cars: remaining, selectedCarId: sel };
      });
      apiSend("DELETE", "/cars/" + id);
    },
    setDefault(id) {
      setState((s) => ({
        ...s,
        cars: s.cars.map((c) => ({ ...c, isDefault: c.id === id })),
      }));
      apiSend("POST", "/cars/" + id + "/default");
    },
    // Save a new parking spot for a car; previous spot moves into history.
    parkCar(id, spot) {
      setState((s) => ({
        ...s,
        cars: s.cars.map((c) => {
          if (c.id !== id) return c;
          const history = c.current ? [c.current, ...c.history].slice(0, 12) : c.history;
          return { ...c, current: { ...spot }, history };
        }),
      }));
      apiSend("POST", "/cars/" + id + "/park", spot);
    },
    // Edit the live parking spot (note / pin nudge) without touching history.
    updateSpot(id, patch) {
      setState((s) => ({
        ...s,
        cars: s.cars.map((c) =>
          c.id === id && c.current ? { ...c, current: { ...c.current, ...patch } } : c
        ),
      }));
      apiSend("PATCH", "/cars/" + id + "/spot", patch);
    },
    clearSpot(id) {
      setState((s) => ({
        ...s,
        cars: s.cars.map((c) => {
          if (c.id !== id || !c.current) return c;
          const history = [c.current, ...c.history].slice(0, 12);
          return { ...c, current: null, history };
        }),
      }));
      apiSend("DELETE", "/cars/" + id + "/spot");
    },
    removeHistory(id, ts) {
      setState((s) => ({
        ...s,
        cars: s.cars.map((c) =>
          c.id === id ? { ...c, history: c.history.filter((h) => h.timestamp !== ts) } : c
        ),
      }));
      apiSend("DELETE", "/cars/" + id + "/history/" + ts);
    },
    resetAll() {
      // Ask the server to reseed, then adopt its fresh state (keeps ids in sync).
      apiSend("POST", "/reset")
        .then((r) => (r && r.ok ? r.json() : null))
        .then((data) => { if (data && data.state) setState(data.state); })
        .catch(() => {});
      setState(seedState());
    },
  }), []);

  return [state, api];
}

// ── Geometry ─────────────────────────────────────────────────
function toRad(d) { return (d * Math.PI) / 180; }
function toDeg(r) { return (r * 180) / Math.PI; }

// Great-circle distance in metres
function haversine(a, b) {
  const R = 6371000;
  const dLat = toRad(b.lat - a.lat);
  const dLng = toRad(b.lng - a.lng);
  const la1 = toRad(a.lat), la2 = toRad(b.lat);
  const h = Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
}

// Initial bearing from a → b, degrees clockwise from North
function bearing(a, b) {
  const la1 = toRad(a.lat), la2 = toRad(b.lat);
  const dLng = toRad(b.lng - a.lng);
  const y = Math.sin(dLng) * Math.cos(la2);
  const x = Math.cos(la1) * Math.sin(la2) - Math.sin(la1) * Math.cos(la2) * Math.cos(dLng);
  return (toDeg(Math.atan2(y, x)) + 360) % 360;
}

const COMPASS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
function compassPoint(deg) {
  return COMPASS[Math.round(deg / 45) % 8];
}

function formatDistance(metres, units) {
  if (metres == null || isNaN(metres)) return "—";
  if (units === "mi") {
    const feet = metres * 3.28084;
    if (feet < 528) return `${Math.round(feet / 10) * 10} ft`;
    return `${(metres / 1609.34).toFixed(metres < 16093 ? 2 : 1)} mi`;
  }
  if (metres < 1000) return `${Math.round(metres / 5) * 5} m`;
  return `${(metres / 1000).toFixed(metres < 10000 ? 2 : 1)} km`;
}

// "Open in Maps" deep link (walking directions). Apple on iOS/Mac, else Google.
function mapsUrl(spot) {
  const isApple = /iPhone|iPad|iPod|Macintosh/.test(navigator.userAgent);
  if (isApple) return `https://maps.apple.com/?daddr=${spot.lat},${spot.lng}&dirflg=w`;
  return `https://www.google.com/maps/dir/?api=1&destination=${spot.lat},${spot.lng}&travelmode=walking`;
}

// ── Time formatting ──────────────────────────────────────────
function relativeTime(ts) {
  const diff = Date.now() - ts;
  const min = Math.round(diff / 60000);
  if (min < 1) return "Just now";
  if (min < 60) return `${min} min ago`;
  const hr = Math.round(min / 60);
  if (hr < 24) return `${hr} hr${hr > 1 ? "s" : ""} ago`;
  const day = Math.round(hr / 24);
  if (day === 1) return "Yesterday";
  if (day < 7) return `${day} days ago`;
  return new Date(ts).toLocaleDateString(undefined, { month: "short", day: "numeric" });
}

function clockTime(ts) {
  return new Date(ts).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
}
function dateLabel(ts) {
  return new Date(ts).toLocaleDateString(undefined, { weekday: "short", day: "numeric", month: "short" });
}

// ── Geolocation with graceful fallback ───────────────────────
// Destination point a given distance/bearing from an origin (for demo positioning).
function destPoint(from, dist, brgDeg) {
  const R = 6371000;
  const d = dist / R, b = toRad(brgDeg);
  const la1 = toRad(from.lat), lo1 = toRad(from.lng);
  const la2 = Math.asin(Math.sin(la1) * Math.cos(d) + Math.cos(la1) * Math.sin(d) * Math.cos(b));
  const lo2 = lo1 + Math.atan2(Math.sin(b) * Math.sin(d) * Math.cos(la1), Math.cos(d) - Math.sin(la1) * Math.sin(la2));
  return { lat: toDeg(la2), lng: toDeg(lo2) };
}

// Current simulated "you are here" — overridable from the Tweaks demo controls.
function simPoint() {
  const p = window.__simLocation || SIM_LOCATION;
  return { lat: p.lat, lng: p.lng, accuracy: p.accuracy || 22 };
}

// Resolves { lat, lng, accuracy, simulated }.
function getPosition({ timeout = 6000 } = {}) {
  return new Promise((resolve) => {
    if (!navigator.geolocation) {
      resolve({ ...simPoint(), simulated: true });
      return;
    }
    let settled = false;
    const fall = setTimeout(() => {
      if (!settled) { settled = true; resolve({ ...simPoint(), simulated: true }); }
    }, timeout + 500);
    navigator.geolocation.getCurrentPosition(
      (pos) => {
        if (settled) return; settled = true; clearTimeout(fall);
        resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude, accuracy: pos.coords.accuracy, simulated: false });
      },
      () => {
        if (settled) return; settled = true; clearTimeout(fall);
        resolve({ ...simPoint(), simulated: true });
      },
      { enableHighAccuracy: true, timeout, maximumAge: 10000 }
    );
  });
}

// Live position hook for the Find screen. Watches real GPS if granted,
// otherwise holds the simulated point.
function useLivePosition(active, simKey) {
  const [pos, setPos] = React.useState(null);
  React.useEffect(() => {
    if (!active) return;
    let watchId = null;
    let cancelled = false;
    // seed quickly
    getPosition().then((p) => { if (!cancelled) setPos(p); });
    if (navigator.geolocation) {
      watchId = navigator.geolocation.watchPosition(
        (p) => setPos({ lat: p.coords.latitude, lng: p.coords.longitude, accuracy: p.coords.accuracy, simulated: false }),
        () => {},
        { enableHighAccuracy: true, maximumAge: 5000, timeout: 8000 }
      );
    }
    return () => {
      cancelled = true;
      if (watchId != null && navigator.geolocation) navigator.geolocation.clearWatch(watchId);
    };
  }, [active, simKey]);
  return pos;
}

Object.assign(window, {
  STORAGE_KEY, SIM_LOCATION, seedState, loadState, saveState, useStore,
  haversine, bearing, compassPoint, formatDistance, mapsUrl,
  relativeTime, clockTime, dateLabel, getPosition, useLivePosition, destPoint, simPoint,
});
