// Persistence + global state via custom hooks
const STORAGE_KEY = 'mle-sde-plan-v2-may18';

// SRS intervals (days) — Anki-lite
const SRS_INTERVALS = [2, 7, 21, 45];

// Apply a stored schedule shift (days) to PLAN_DATA in place, exactly once per load.
function applyScheduleShift(offset) {
  if (!offset || typeof offset !== 'number') return;
  const D = window.PLAN_DATA;
  if (D.__shifted === offset) return;
  function shift(s) {
    const [y, m, d] = s.split('-').map(Number);
    const dt = new Date(y, m - 1, d);
    dt.setDate(dt.getDate() + offset);
    const yy = dt.getFullYear();
    const mm = String(dt.getMonth() + 1).padStart(2, '0');
    const dd = String(dt.getDate()).padStart(2, '0');
    return `${yy}-${mm}-${dd}`;
  }
  function pretty(s) {
    const [y, m, d] = s.split('-').map(Number);
    const dt = new Date(y, m - 1, d);
    return dt.toLocaleString('en-US', { month: 'short', day: '2-digit' });
  }
  D.meta.sprintStart = shift(D.meta.sprintStart);
  D.meta.sprintEnd = shift(D.meta.sprintEnd);
  D.meta.interviewLaunch = shift(D.meta.interviewLaunch);
  for (const p of D.phases) {
    p.start = shift(p.start);
    p.end = shift(p.end);
    p.dates = `${pretty(p.start)} – ${pretty(p.end)}`;
  }
  for (const c of D.calendar) {
    const nd = shift(c.date);
    c.date = nd;
    c.display = `${nd} (${pretty(nd)})`;
  }
  D.__shifted = offset;
}

function loadState() {
  try {
    const s = localStorage.getItem(STORAGE_KEY);
    if (s) return JSON.parse(s);
  } catch (e) {}
  return {};
}
function saveState(state) {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
}

function hasMeaningfulLocalState(state) {
  return !!(
    Object.keys(state.days || {}).length ||
    Object.keys(state.dsa || {}).length ||
    Object.keys(state.lld || {}).length ||
    Object.keys(state.hld || {}).length ||
    Object.keys(state.apps || {}).length ||
    Object.keys(state.mocks || {}).length ||
    Object.keys(state.weak || {}).length ||
    Object.keys(state.stories || {}).length ||
    Object.keys(state.patterns || {}).length ||
    Object.keys(state.pipeline || {}).length
  );
}

const StoreContext = React.createContext(null);

function StoreProvider({ children }) {
  const remoteHydratedRef = React.useRef(false);
  const saveTimerRef = React.useRef(null);
  const lastRemoteSaveRef = React.useRef(0); // ms timestamp of last save we initiated
  const sessionRef = React.useRef(null);
  const [sync, setSync] = React.useState({
    configured: !!window.ScheduleSync?.configured,
    signedIn: false,
    email: '',
    status: window.ScheduleSync?.configured ? 'Checking sync...' : 'Local only',
    pendingEmail: false,
    error: '',
    lastSyncedAt: null,
  });

  const [state, setState] = React.useState(() => {
    const loaded = loadState();
    const hasLoadedState = hasMeaningfulLocalState(loaded);
    // Apply any persisted schedule shift before any view reads PLAN_DATA
    applyScheduleShift(loaded.scheduleShift || 0);
    return {
      // day-level: { 'YYYY-MM-DD': { done, note, actualHours, energy, sleep } }
      days: {},
      dsa: {},
      lld: {},
      hld: {},
      apps: {},
      mocks: {},
      // weak-problem queue: { id: { id, topic, title, addedOn, intervalIdx, dueDate, history: [date,...] } }
      weak: {},
      // STAR stories: { id: { id, theme, situation, task, action, result, rehearsals } }
      stories: {},
      // Pattern catalog per DSA topic: { topicNum: [{ id, name, trigger, template }] }
      patterns: {},
      // Pipeline per app num: { num: { stage, nextDate, nextNote, history: [{stage,date}] } }
      pipeline: {},
      // # of days the calendar has been shifted forward by user (auto-rescheduler)
      scheduleShift: 0,
      view: 'today',
      _updatedAt: hasLoadedState ? (loaded._updatedAt || new Date().toISOString()) : null,
      ...loaded,
    };
  });

  React.useEffect(() => { saveState(state); }, [state]);

  // Pull latest remote state and merge by most-recent-write-wins.
  // Exposed via the returned `refreshFromCloud()` action so users can force a
  // pull (the "Refresh" button), and also auto-called by realtime/polling.
  const pullFromCloud = React.useCallback(async ({ silent = false } = {}) => {
    const syncer = window.ScheduleSync;
    if (!syncer?.configured || !sessionRef.current) return;
    if (!silent) setSync(s => ({ ...s, status: 'Loading cloud data...' }));
    try {
      const remote = await syncer.loadState();
      if (!remote?.payload) {
        if (!silent) setSync(s => ({ ...s, status: 'Synced', error: '', lastSyncedAt: new Date().toISOString() }));
        return;
      }
      const remoteTime = remote.updatedAt ? Date.parse(remote.updatedAt) : 0;
      setState(prev => {
        const localTime = prev._updatedAt ? Date.parse(prev._updatedAt) : 0;
        // Remote wins if strictly newer than local. Tie → keep local (we likely
        // just wrote this exact payload).
        if (remoteTime > localTime) {
          saveState(remote.payload);
          return remote.payload;
        }
        return prev;
      });
      setSync(s => ({ ...s, status: 'Synced', error: '', lastSyncedAt: new Date().toISOString() }));
    } catch (err) {
      setSync(s => ({ ...s, status: 'Sync error', error: err.message || String(err) }));
    }
  }, []);

  React.useEffect(() => {
    const syncer = window.ScheduleSync;
    if (!syncer?.configured) return;

    let cancelled = false;
    let unsubscribeRealtime = () => {};
    let pollTimer = null;

    async function hydrate(session) {
      if (cancelled) return;
      sessionRef.current = session;
      // Tear down any prior realtime/polling on session change
      unsubscribeRealtime();
      unsubscribeRealtime = () => {};
      if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }

      if (!session) {
        remoteHydratedRef.current = false;
        setSync(s => ({ ...s, signedIn: false, email: '', status: 'Sign in to sync', error: '' }));
        return;
      }

      setSync(s => ({
        ...s,
        signedIn: true,
        email: session.user.email || '',
        status: 'Loading cloud data...',
        error: '',
      }));

      try {
        const remote = await syncer.loadState();
        if (cancelled) return;
        setState(prev => {
          const remoteTime = remote?.updatedAt ? Date.parse(remote.updatedAt) : 0;
          const localTime = prev._updatedAt ? Date.parse(prev._updatedAt) : 0;
          if (remote?.payload && (!prev._updatedAt || remoteTime > localTime)) {
            saveState(remote.payload);
            return remote.payload;
          }
          if (hasMeaningfulLocalState(prev)) {
            syncer.saveState(prev).catch(err => {
              setSync(s => ({ ...s, status: 'Sync error', error: err.message || String(err) }));
            });
          }
          return prev;
        });
        remoteHydratedRef.current = true;
        setSync(s => ({ ...s, status: 'Synced', error: '', lastSyncedAt: new Date().toISOString() }));

        // ---- Realtime: instant push from the other device ----
        if (syncer.subscribeToChanges) {
          unsubscribeRealtime = syncer.subscribeToChanges(({ payload, updatedAt }) => {
            if (cancelled || !payload) return;
            const remoteTime = updatedAt ? Date.parse(updatedAt) : 0;
            // Ignore echoes of our own writes (within 2s of our last save)
            if (Math.abs(remoteTime - lastRemoteSaveRef.current) < 2000) return;
            setState(prev => {
              const localTime = prev._updatedAt ? Date.parse(prev._updatedAt) : 0;
              if (remoteTime <= localTime) return prev;
              saveState(payload);
              return payload;
            });
            setSync(s => ({ ...s, status: 'Synced', error: '', lastSyncedAt: new Date().toISOString() }));
          });
        }

        // ---- Polling fallback: every 25s while signed in ----
        // Catches the case where realtime isn't enabled on the table.
        pollTimer = setInterval(() => {
          if (document.visibilityState === 'visible') {
            pullFromCloud({ silent: true });
          }
        }, 25000);
      } catch (err) {
        setSync(s => ({ ...s, status: 'Sync error', error: err.message || String(err) }));
      }
    }

    syncer.getSession().then(hydrate).catch(err => {
      setSync(s => ({ ...s, status: 'Sync error', error: err.message || String(err) }));
    });
    const unsubscribeAuth = syncer.onAuthChange(hydrate);

    // Re-pull whenever the tab regains focus — cheap, catches the "I opened
    // mobile after editing on laptop" case immediately.
    function onFocus() {
      if (sessionRef.current && document.visibilityState === 'visible') {
        pullFromCloud({ silent: true });
      }
    }
    window.addEventListener('focus', onFocus);
    document.addEventListener('visibilitychange', onFocus);

    return () => {
      cancelled = true;
      unsubscribeAuth();
      unsubscribeRealtime();
      if (pollTimer) clearInterval(pollTimer);
      window.removeEventListener('focus', onFocus);
      document.removeEventListener('visibilitychange', onFocus);
    };
  }, [pullFromCloud]);

  React.useEffect(() => {
    const syncer = window.ScheduleSync;
    if (!syncer?.configured || !sync.signedIn || !remoteHydratedRef.current) return;

    clearTimeout(saveTimerRef.current);
    setSync(s => ({ ...s, status: 'Saving...' }));
    saveTimerRef.current = setTimeout(() => {
      lastRemoteSaveRef.current = Date.now();
      syncer.saveState(state)
        .then(() => setSync(s => ({ ...s, status: 'Synced', error: '', lastSyncedAt: new Date().toISOString() })))
        .catch(err => setSync(s => ({ ...s, status: 'Sync error', error: err.message || String(err) })));
    }, 800);

    return () => clearTimeout(saveTimerRef.current);
  }, [state, sync.signedIn]);

  const update = React.useCallback((patch) => {
    setState(prev => {
      const next = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch };
      return { ...next, _updatedAt: new Date().toISOString() };
    });
  }, []);

  const signInToSync = React.useCallback(async (email) => {
    const syncer = window.ScheduleSync;
    if (!syncer?.configured) throw new Error('Supabase sync is not configured yet.');
    setSync(s => ({ ...s, pendingEmail: true, status: 'Sending sign-in link...', error: '' }));
    try {
      await syncer.signInWithEmail(email);
      setSync(s => ({ ...s, pendingEmail: false, status: 'Check your email for the sign-in link.', error: '' }));
    } catch (err) {
      setSync(s => ({ ...s, pendingEmail: false, status: 'Sync error', error: err.message || String(err) }));
      throw err;
    }
  }, []);

  const signOutOfSync = React.useCallback(async () => {
    const syncer = window.ScheduleSync;
    if (!syncer?.configured) return;
    await syncer.signOut();
  }, []);

  const refreshFromCloud = React.useCallback(() => pullFromCloud({ silent: false }), [pullFromCloud]);

  const value = { state, update, sync, signInToSync, signOutOfSync, refreshFromCloud };
  return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}

function useStore() { return React.useContext(StoreContext); }

// Date utilities — all anchored to IST (Asia/Kolkata, UTC+5:30)
const IST_OFFSET_MIN = 330; // +5:30

// Returns a Date object whose UTC fields represent the current IST wall-clock time.
// We use this so getFullYear/getMonth/getDate reflect IST regardless of host TZ.
function nowInIST() {
  const now = new Date();
  // shift epoch by IST offset; consume via getUTC* methods (or treat as local-looking date)
  const utcMs = now.getTime() + now.getTimezoneOffset() * 60000; // → real UTC ms
  return new Date(utcMs + IST_OFFSET_MIN * 60000);
}

function fmtDate(d) {
  const y = d.getFullYear();
  const m = String(d.getMonth()+1).padStart(2, '0');
  const da = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${da}`;
}
function parseDate(s) {
  const [y, m, d] = s.split('-').map(Number);
  return new Date(y, m-1, d);
}
function daysBetween(a, b) {
  // Normalize to midnight to avoid DST/TZ off-by-one
  const aMid = new Date(a.getFullYear(), a.getMonth(), a.getDate());
  const bMid = new Date(b.getFullYear(), b.getMonth(), b.getDate());
  return Math.round((bMid - aMid) / (1000*60*60*24));
}

// Reactive "today" — re-renders consuming components when the IST date rolls over.
function useToday() {
  const [today, setToday] = React.useState(() => {
    const ist = nowInIST();
    return new Date(ist.getFullYear(), ist.getMonth(), ist.getDate());
  });
  React.useEffect(() => {
    function tick() {
      const ist = nowInIST();
      const next = new Date(ist.getFullYear(), ist.getMonth(), ist.getDate());
      setToday(prev => prev.getTime() === next.getTime() ? prev : next);
    }
    // Check every minute — cheap, and catches IST midnight rollover within 60s
    const id = setInterval(tick, 60 * 1000);
    // Also check when tab regains focus (laptop sleep/resume case)
    window.addEventListener('focus', tick);
    document.addEventListener('visibilitychange', tick);
    return () => {
      clearInterval(id);
      window.removeEventListener('focus', tick);
      document.removeEventListener('visibilitychange', tick);
    };
  }, []);
  return today;
}

function todayStrFor(today) { return fmtDate(today); }

// Compute current phase relative to a given today
function currentPhaseFor(today) {
  const t = fmtDate(today);
  const phases = window.PLAN_DATA.phases;
  for (const p of phases) {
    if (t >= p.start && t <= p.end) return p;
  }
  // Before sprint? Return first. After sprint? Return last.
  if (t < phases[0].start) return phases[0];
  return phases[phases.length - 1];
}

// SRS scheduling: given a current intervalIdx (0 = first review), return next due date string
function srsNextDue(fromDate, intervalIdx) {
  const days = SRS_INTERVALS[Math.min(intervalIdx, SRS_INTERVALS.length - 1)];
  const d = new Date(fromDate);
  d.setDate(d.getDate() + days);
  return fmtDate(d);
}

// Shift the entire schedule forward by N days (auto-rescheduler).
// Persists in state, mutates window.PLAN_DATA in place so all views pick it up after reload.
function shiftScheduleForward(update, days) {
  if (!days) return;
  update(prev => ({ ...prev, scheduleShift: (prev.scheduleShift || 0) + days }));
  // Soft reload so PLAN_DATA mutation re-applies cleanly
  setTimeout(() => window.location.reload(), 50);
}

// Export the full state blob as a downloadable JSON file
function exportBackup() {
  const blob = new Blob([localStorage.getItem(STORAGE_KEY) || '{}'], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
  const a = document.createElement('a');
  a.href = url; a.download = `schedule-plan-backup-${ts}.json`;
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

// Import a backup file (overwrites all local state). Returns a promise.
function importBackup(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const text = reader.result;
        const parsed = JSON.parse(text);
        // Sanity check: must look like our state blob
        if (typeof parsed !== 'object' || parsed === null) throw new Error('Not a valid JSON object');
        const expectedKeys = ['days', 'dsa', 'lld', 'hld', 'apps', 'mocks'];
        const hasAtLeastOne = expectedKeys.some(k => k in parsed);
        if (!hasAtLeastOne) throw new Error('File does not look like a Schedule Plan backup');
        localStorage.setItem(STORAGE_KEY, text);
        resolve(parsed);
      } catch (e) {
        reject(e);
      }
    };
    reader.onerror = () => reject(new Error('File read failed'));
    reader.readAsText(file);
  });
}

Object.assign(window, {
  StoreProvider, useStore, fmtDate, parseDate, daysBetween,
  useToday, todayStrFor, currentPhaseFor, nowInIST,
  SRS_INTERVALS, srsNextDue, shiftScheduleForward,
  exportBackup, importBackup, STORAGE_KEY,
});
