// Draft persistence hook + save-status banner.
//
// NOTE ON SHAPE (deviation from the plan's pseudocode): app.jsx does NOT keep
// the draft in a single useState store — it's ~13 independent slices
// (layoutDrafts, moduleOverrides, addedModules, …, courseSettings). The plan's
// `[state, setState]` swap would force a big state restructure, which the
// prompt's "do not touch the shape of course state" rule forbids. So instead of
// returning a state tuple, this hook takes the live aggregate snapshot + a
// `hydrate(content|null)` callback and owns only the persistence lifecycle
// (boot-restore, debounced save, retry/backoff, reset). Consumers keep their
// existing setters untouched. See the report for rationale.

const DRAFT_DEBOUNCE_MS = 1500;
const DRAFT_MAX_RETRIES = 4;

// Minimal inline stand-in for DraftContentSchema.parse (no Zod in the
// prototype). Goal per the plan: catch *obviously broken* / older-shape state
// and start fresh — not full validation (that's Phase 3). We assert the slice
// keys are the right TYPE when present, and reject the legacy full-course shape
// (a top-level `modules` array) which a prior version may have stored.
function isValidDraftContent(c) {
  if (!c || typeof c !== 'object' || Array.isArray(c)) return false;
  // Legacy / foreign shape (e.g. a whole course object with `modules`).
  if ('modules' in c) return false;
  const objOk = (v) => v == null || (typeof v === 'object' && !Array.isArray(v));
  const arrOk = (v) => v == null || Array.isArray(v);
  const objKeys = ['layoutDrafts', 'moduleOverrides', 'layoutOrders',
    'groupTitleOverrides', 'layoutStatusOverrides', 'addedLayouts', 'courseSettings'];
  const arrKeys = ['addedModules', 'addedGroups', 'deletedLayoutIds',
    'deletedModuleIds', 'deletedGroupIds'];
  for (const k of objKeys) if (!objOk(c[k])) return false;
  for (const k of arrKeys) if (!arrOk(c[k])) return false;
  if (!(c.moduleOrder == null || Array.isArray(c.moduleOrder))) return false;
  return true;
}

// usePersistedDraft(courseId, aggregate, hydrate)
//   aggregate — the live draft object to persist (a useMemo of all slices)
//   hydrate(content|null) — apply a restored content object to the slices;
//                           null means "reset everything to defaults"
// Returns { bootStatus, saveBlocked, reset }.
//   bootStatus: 'loading' | 'restored' | 'fresh' | 'invalid'
function usePersistedDraft(courseId, aggregate, hydrate) {
  const [bootStatus, setBootStatus] = React.useState('loading');
  const [saveBlocked, setSaveBlocked] = React.useState(false);
  const debounceRef = React.useRef(null);
  const failuresRef = React.useRef(0);
  const skipNextSaveRef = React.useRef(false);
  const tokenRef = React.useRef(0);

  // Boot — restore from IndexedDB once per courseId.
  React.useEffect(() => {
    let cancelled = false;
    const P = window.DraftPersistence;
    if (!P) { setBootStatus('fresh'); return; }
    P.loadDraft(courseId).then((record) => {
      if (cancelled) return;
      if (!record) { setBootStatus('fresh'); return; }
      if (isValidDraftContent(record.content)) {
        skipNextSaveRef.current = true; // don't re-save the just-restored data
        hydrate(record.content);
        setBootStatus('restored');
      } else {
        P.clearDraft(courseId).catch(() => {});
        setBootStatus('invalid');
      }
    }).catch(() => { if (!cancelled) setBootStatus('fresh'); });
    return () => { cancelled = true; };
    // hydrate is stable (useCallback in the consumer); courseId is the key.
  }, [courseId]);

  // Debounced save on any aggregate change (after boot completes).
  React.useEffect(() => {
    if (bootStatus === 'loading') return;            // never write during boot
    if (skipNextSaveRef.current) { skipNextSaveRef.current = false; return; }
    const P = window.DraftPersistence;
    if (!P) return;
    clearTimeout(debounceRef.current);
    const token = ++tokenRef.current;
    debounceRef.current = setTimeout(async () => {
      for (let attempt = 0; attempt < DRAFT_MAX_RETRIES; attempt++) {
        if (token !== tokenRef.current) return;       // superseded by a newer edit
        try {
          await P.saveDraft(courseId, aggregate);
          failuresRef.current = 0;
          setSaveBlocked(false);
          return;
        } catch (e) {
          failuresRef.current += 1;
          if (failuresRef.current >= 3) setSaveBlocked(true);
          // exponential backoff: 1s, 2s, 4s, 8s
          await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
        }
      }
    }, DRAFT_DEBOUNCE_MS);
    return () => clearTimeout(debounceRef.current);
  }, [aggregate, bootStatus, courseId]);

  const reset = React.useCallback(async () => {
    const P = window.DraftPersistence;
    skipNextSaveRef.current = true;
    if (P) { try { await P.clearDraft(courseId); } catch (e) {} }
    hydrate(null);
    failuresRef.current = 0;
    setSaveBlocked(false);
  }, [courseId, hydrate]);

  return { bootStatus, saveBlocked, reset };
}

// Top-of-app banner. Fixed strip so it never disturbs the grid layout.
//  - saveBlocked  → red, persistent (clears only when a save succeeds)
//  - bootStatus 'invalid' → amber, dismissible
function SaveStatusBanner({ bootStatus, saveBlocked }) {
  const [dismissed, setDismissed] = React.useState(false);
  React.useEffect(() => { if (bootStatus !== 'invalid') setDismissed(false); }, [bootStatus]);

  const showBlocked = !!saveBlocked;
  const showInvalid = bootStatus === 'invalid' && !dismissed;
  if (!showBlocked && !showInvalid) return null;

  // saveBlocked takes priority if somehow both are true.
  const blocked = showBlocked;
  const bg = blocked ? '#7f1d1d' : '#92610a';
  const Icon = I.AlertCircle || I.Info;

  return (
    <div role="alert" aria-live={blocked ? 'assertive' : 'polite'} style={{
      position: 'fixed', top: 0, left: 0, right: 0, zIndex: 200,
      display: 'flex', alignItems: 'center', gap: 10,
      padding: '8px 16px', background: bg, color: '#fff',
      fontSize: 12.5, lineHeight: 1.4, fontFamily: 'inherit',
      boxShadow: '0 1px 4px rgba(0,0,0,.3)',
    }}>
      <Icon size={15} style={{ flex: '0 0 auto' }} />
      <span style={{ flex: 1 }}>
        {blocked
          ? 'Your changes aren’t being saved locally. Copy any unsaved work to a safe place, then refresh.'
          : 'Your saved draft was from an older version and couldn’t be restored — starting fresh.'}
      </span>
      {!blocked && (
        <button onClick={() => setDismissed(true)} aria-label="Dismiss"
          style={{ flex: '0 0 auto', display: 'inline-flex', alignItems: 'center',
            justifyContent: 'center', width: 22, height: 22, padding: 0,
            border: 0, borderRadius: 4, background: 'rgba(255,255,255,.15)',
            color: '#fff', cursor: 'default', fontFamily: 'inherit' }}>
          <I.X size={13} />
        </button>
      )}
    </div>
  );
}

Object.assign(window, { usePersistedDraft, SaveStatusBanner, isValidDraftContent });
