/* ============================================================
   state.jsx — global state, block model, persistence
   ============================================================ */
const { useState, useEffect, useRef, useCallback, useMemo, useReducer, createContext, useContext } = React;

// -- reviewer mode detection --------------------------------------------
window.IS_REVIEWER_MODE = Boolean(new URLSearchParams(window.location.search).get('r'));

// -- remote row mappers -------------------------------------------------
function mapRemoteComment(row) {
  return {
    id: row.id,
    blockId: row.block_id,
    range: (row.range_start != null) ? { start: row.range_start, end: row.range_end } : null,
    text: row.text,
    author: row.author_name,
    color: row.author_color || '#8b9a54',
    resolved: !!row.resolved,
    createdAt: row.created_at ? new Date(row.created_at).getTime() : Date.now(),
    _remote: true,
  };
}
function mapRemoteSuggestion(row) {
  return {
    id: row.id,
    blockId: row.block_id,
    range: (row.range_start != null) ? { start: row.range_start, end: row.range_end } : null,
    original: row.original,
    proposed: row.proposed,
    author: row.author_name,
    color: row.author_color || '#8b9a54',
    status: row.status || 'pending',
    createdAt: row.created_at ? new Date(row.created_at).getTime() : Date.now(),
    _remote: true,
  };
}
function upsertById(list, item) {
  const idx = list.findIndex(x => x.id === item.id);
  if (idx < 0) return [...list, item];
  const next = [...list]; next[idx] = { ...next[idx], ...item }; return next;
}
function dedupeById(list) {
  const seen = new Set(); const out = [];
  for (const x of list) { if (seen.has(x.id)) continue; seen.add(x.id); out.push(x); }
  return out;
}

const uid = () => Math.random().toString(36).slice(2, 9);

// --- block factories --------------------------------------------------

const newBlock = (type, data = {}) => ({
  id: uid(),
  type,
  ...defaultBlockData(type),
  ...data,
});

function defaultBlockData(type) {
  switch (type) {
    case 'h1': return { text: 'Nieuwe titel' };
    case 'h2': return { text: 'Sectie' };
    case 'h3': return { text: 'Subsectie' };
    case 'p':  return { text: 'Begin hier met schrijven…' };
    case 'numlist': return { level: 1, items: ['Eerste punt', 'Tweede punt'] };
    case 'ul': return { items: ['Bullet één', 'Bullet twee'] };
    case 'callout': return {
      tone: 'green',
      label: 'Case uitgelicht',
      title: 'Samenwerking met RVO',
      body: 'Sinds 2009 werken wij voor de voorgangers van RVO. Diezelfde voorspelbaarheid zetten we nu opnieuw in.',
    };
    case 'quote': return { text: 'Wat ons bindt, is de verbinding zelf.', cite: 'Projectleider RVO' };
    case 'photos': return { cols: 2, labels: ['Team op locatie', 'Kickoff sessie'], urls: [null, null] };
    case 'diagram': return { title: 'Processchema' };
    case 'table': return {
      headers: ['Fase', 'Looptijd', 'Verantwoordelijk'],
      rows: [
        ['Voorbereiding', '16 mrt — 1 apr', 'Implementatiemanager'],
        ['Ketentest', '1 apr — 15 mei', 'Techniek team'],
        ['Go-live', '1 juni 2026', 'Gezamenlijk'],
      ],
    };
    case 'kpis': return {
      items: [
        { n: '32', unit: 'jr', label: 'ervaring in publieke sector' },
        { n: '550', unit: '', label: 'interim-professionals in vaste dienst' },
        { n: '98', unit: '%', label: 'klanttevredenheid bij RVO' },
      ],
    };
    case 'pagebreak': return {};
    default: return {};
  }
}

// --- default document -------------------------------------------------

function makeEmptyDoc() {
  return {
    meta: {
      title: 'Aanbesteding — Pleysier: eigen-regiemodel',
      orgName: 'Welten Groep',
      logoUrl: null,
      accent: '#1f3a2e',
      accentSecondary: '#8b9a54',
      headFont: 'Geist',
      bodyFont: 'Geist',
      fontSize: 15,
      margin: 72,
      columns: 1,
      orientation: 'portrait',
      showHeader: true,
      showFooter: true,
      headerText: 'Kwaliteit dienstverlening — Perceel 1',
      footerText: 'Vertrouwelijk • pagina {page}',
    },
    blocks: [],
    comments: [], // { id, blockId, range:{start,end}, author, color, text, resolved, createdAt }
    suggestions: [], // { id, blockId, range:{start,end}, author, color, original, proposed, status:'pending'|'accepted'|'rejected' }
    headingStyles: {
      h1: { size: 1, weight: 600, color: 'ink', case: 'none' },
      h2: { size: 1, weight: 600, color: 'accent', case: 'none' },
      h3: { size: 1, weight: 600, color: 'ink', case: 'none' },
    },
  };
}

function makeStarterDoc() {
  const d = makeEmptyDoc();
  d.blocks = [
    newBlock('h1', { text: 'Kwaliteit dienstverlening' }),
    newBlock('p',  { text: 'Sinds 2009 werken wij voor de voorgangers van RVO. Wat ons bindt, is de verbinding zelf — tussen mensen, overheden en sectoren.' }),
    newBlock('h2', { text: 'Onze drie kernbeloften' }),
    newBlock('numlist', { level: 1, items: [
      'Partner voor specialistische rollen in schaarse profielen',
      'Gegarandeerde kwaliteit, geborgd in ISO 9001 en NEN 4400-1',
      'Voorspelbare levering met een vast klantenteam',
    ]}),
    newBlock('callout', { tone: 'green', label: 'Case uitgelicht', title: 'COVID-19 opschaling bij RVO',
      body: 'In zes weken tijd 40+ extra professionals ingezet bij BMKB en GO, zonder verlies van kwaliteit of doorlooptijd.' }),
    newBlock('h3', { text: 'Meetpunten per 1 juni 2026' }),
    newBlock('kpis', { items: [
      { n: '100', unit: '%', label: 'acceptatiecriteria afgetekend' },
      { n: '< 4', unit: 'wk', label: 'monitoring na livegang' },
      { n: '1-2', unit: '', label: 'proefcasussen end-to-end' },
    ]}),
  ];
  return d;
}

// --- persistence ------------------------------------------------------

const STORAGE_KEY = 'tender-studio-v1';
function loadDoc() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (raw) return JSON.parse(raw);
  } catch (e) {}
  return null;
}
function saveDoc(doc) {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(doc)); } catch (e) {}
}

// --- app context ------------------------------------------------------

const AppCtx = createContext(null);
const useApp = () => useContext(AppCtx);

function AppProvider({ children }) {
  const [stage, setStageRaw] = useState(() => {
    if (window.IS_REVIEWER_MODE) return 'review';
    return localStorage.getItem('tender-stage') || 'setup';
  });
  const setStage = (s) => {
    if (window.IS_REVIEWER_MODE) return; // locked to review
    setStageRaw(s); localStorage.setItem('tender-stage', s);
  };

  // currentDocId — when set, doc comes from Supabase; when null, show docs list
  const [currentDocId, setCurrentDocIdRaw] = useState(() => {
    if (window.IS_REVIEWER_MODE) return null; // token-fetched, no local id
    if (!window.HAS_SUPABASE) return 'local';
    return localStorage.getItem('tender-current-doc') || null;
  });
  const setCurrentDocId = (id) => {
    if (window.IS_REVIEWER_MODE) return;
    setCurrentDocIdRaw(id);
    if (id) localStorage.setItem('tender-current-doc', id);
    else localStorage.removeItem('tender-current-doc');
  };

  const [doc, setDoc] = useState(() => {
    if (window.IS_REVIEWER_MODE) return makeEmptyDoc(); // replaced by token fetch
    return loadDoc() || makeEmptyDoc();
  });
  const [docLoading, setDocLoading] = useState(window.IS_REVIEWER_MODE || false);
  const [tokenDocMeta, setTokenDocMeta] = useState(null); // { permissions, reviewLinkId, organizationId, documentId }

  // Load doc by reviewer token
  useEffect(() => {
    if (!window.IS_REVIEWER_MODE) return;
    const token = new URLSearchParams(window.location.search).get('r');
    if (!token) return;
    let cancelled = false;
    (async () => {
      const loaded = await window.getDocumentByToken(token);
      if (cancelled) return;
      if (!loaded) {
        setDocLoading(false);
        setDoc({ ...makeEmptyDoc(), meta: { ...makeEmptyDoc().meta, title: 'Link ongeldig of verlopen' } });
        window.__TOKEN_INVALID = true;
        return;
      }
      // Also fetch remote comments/suggestions for this doc
      const [remoteC, remoteS] = await Promise.all([
        window.fetchRemoteComments(loaded.id),
        window.fetchRemoteSuggestions(loaded.id),
      ]);
      const mergedDoc = {
        ...loaded,
        comments: [...(loaded.comments || []), ...remoteC.map(mapRemoteComment)],
        suggestions: [...(loaded.suggestions || []), ...remoteS.map(mapRemoteSuggestion)],
      };
      setDoc(mergedDoc);
      setTokenDocMeta({
        documentId: loaded.id,
        permissions: loaded._permissions,
        reviewLinkId: loaded._reviewLinkId,
        organizationId: loaded._organizationId,
      });
      setDocLoading(false);

      // realtime: listen for new comments/suggestions from others
      const sb = getSupabase();
      if (sb) {
        sb.channel('rev-' + loaded.id)
          .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'comments', filter: `document_id=eq.${loaded.id}` },
              p => setDoc(d => ({ ...d, comments: upsertById(d.comments, mapRemoteComment(p.new)) })))
          .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'suggestions', filter: `document_id=eq.${loaded.id}` },
              p => setDoc(d => ({ ...d, suggestions: upsertById(d.suggestions, mapRemoteSuggestion(p.new)) })))
          .subscribe();
      }
    })();
    return () => { cancelled = true; };
  }, []);

  // Load from Supabase when currentDocId changes (owner mode)
  useEffect(() => {
    if (window.IS_REVIEWER_MODE) return;
    if (!window.HAS_SUPABASE) return;
    if (!currentDocId || currentDocId === 'local') return;
    let cancelled = false;
    setDocLoading(true);
    window.fetchDoc(currentDocId).then(async row => {
      if (cancelled) return;
      if (row) {
        const loaded = window.docFromRow(row);
        // also merge any remote comments/suggestions posted by reviewers
        const [remoteC, remoteS] = await Promise.all([
          window.fetchRemoteComments(currentDocId),
          window.fetchRemoteSuggestions(currentDocId),
        ]);
        const ownerDoc = {
          ...loaded,
          comments: dedupeById([...(loaded.comments || []), ...remoteC.map(mapRemoteComment)]),
          suggestions: dedupeById([...(loaded.suggestions || []), ...remoteS.map(mapRemoteSuggestion)]),
        };
        setDoc(ownerDoc);
      }
      setDocLoading(false);
    });

    // realtime subscription for owner
    const sb = getSupabase();
    let channel = null;
    if (sb) {
      channel = sb.channel('own-' + currentDocId)
        .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'comments', filter: `document_id=eq.${currentDocId}` },
            p => setDoc(d => ({ ...d, comments: upsertById(d.comments, mapRemoteComment(p.new)) })))
        .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'suggestions', filter: `document_id=eq.${currentDocId}` },
            p => setDoc(d => ({ ...d, suggestions: upsertById(d.suggestions, mapRemoteSuggestion(p.new)) })))
        .subscribe();
    }
    return () => {
      cancelled = true;
      if (channel && sb) sb.removeChannel(channel);
    };
  }, [currentDocId]);

  // Save: localStorage always + debounced Supabase when applicable
  const saveTimer = useRef(null);
  useEffect(() => {
    if (window.IS_REVIEWER_MODE) return; // reviewers never write back the doc wholesale
    saveDoc(doc);
    if (!window.HAS_SUPABASE || !currentDocId || currentDocId === 'local') return;
    if (saveTimer.current) clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => {
      window.saveDocRemote(currentDocId, doc);
    }, 800);
    return () => { if (saveTimer.current) clearTimeout(saveTimer.current); };
  }, [doc, currentDocId]);

  // history: stack of snapshots { doc, label, author, at } — max 50 kept
  const [history, setHistory] = useState([]);
  const recordSnapshot = (label, author) => {
    setHistory(h => [...h, { doc, label: label || 'edit', author: author || '—', at: Date.now() }].slice(-50));
  };
  const undo = () => {
    setHistory(h => {
      if (h.length === 0) return h;
      const last = h[h.length - 1];
      setDoc(last.doc);
      return h.slice(0, -1);
    });
  };
  const revertTo = (idx) => {
    setHistory(h => {
      const entry = h[idx]; if (!entry) return h;
      setDoc(entry.doc);
      return h.slice(0, idx);
    });
  };

  const [selectedBlockId, setSelectedBlockId] = useState(null);
  const [activeCommentId, setActiveCommentId] = useState(null);
  const [tweaks, setTweaks] = useState(() => window.__TWEAKS || { theme: 'matcha', role: 'instromer', density: 'comfortable', previewMode: 'doc' });
  const [rightTab, setRightTab] = useState('all'); // 'all' | 'comments' | 'suggestions' | 'resolved'

  // reviewer identity (persisted separately so it survives doc resets) -----
  const [reviewer, setReviewerRaw] = useState(() => {
    try {
      const raw = localStorage.getItem('tender-studio-reviewer');
      if (raw) return JSON.parse(raw);
    } catch (e) {}
    return null; // { name, org, color }
  });
  const setReviewer = (r) => {
    setReviewerRaw(r);
    try {
      if (r) localStorage.setItem('tender-studio-reviewer', JSON.stringify(r));
      else localStorage.removeItem('tender-studio-reviewer');
    } catch (e) {}
  };
  // derived: who is the current author when they place a comment/suggestion?
  // If a reviewer identity is set, that wins; otherwise fall back to the demo role persona.
  const currentAuthor = () => {
    if (reviewer && reviewer.name) {
      return { name: reviewer.name, color: reviewer.color || '#8b9a54', org: reviewer.org || '' };
    }
    const role = (window.ROLES && window.ROLES[tweaks.role]) || { name: 'Anoniem', color: '#8b9a54' };
    return { name: role.name, color: role.color, org: '' };
  };

  // block mutations
  const addBlock = (type, atIndex) => {
    const block = newBlock(type);
    recordSnapshot('Blok toegevoegd: ' + type, currentAuthor().name);
    setDoc(d => {
      const blocks = [...d.blocks];
      const i = atIndex == null ? blocks.length : atIndex;
      blocks.splice(i, 0, block);
      return { ...d, blocks };
    });
    setSelectedBlockId(block.id);
    return block.id;
  };
  const updateBlock = (id, patch) => setDoc(d => ({
    ...d,
    blocks: d.blocks.map(b => b.id === id ? { ...b, ...patch } : b),
  }));
  const removeBlock = (id) => {
    recordSnapshot('Blok verwijderd', currentAuthor().name);
    setDoc(d => ({ ...d, blocks: d.blocks.filter(b => b.id !== id) }));
  };
  const moveBlock = (fromIdx, toIdx) => setDoc(d => {
    const blocks = [...d.blocks];
    const [x] = blocks.splice(fromIdx, 1);
    blocks.splice(toIdx > fromIdx ? toIdx - 1 : toIdx, 0, x);
    return { ...d, blocks };
  });
  const updateMeta = (patch) => setDoc(d => ({ ...d, meta: { ...d.meta, ...patch } }));
  const updateHeadingStyle = (level, patch) => setDoc(d => ({
    ...d,
    headingStyles: { ...d.headingStyles, [level]: { ...d.headingStyles[level], ...patch } },
  }));

  // comments
  const addComment = ({ blockId, range, text, author, color }) => {
    const c = { id: uid(), blockId, range, text, author, color, resolved: false, createdAt: Date.now() };
    setDoc(d => ({ ...d, comments: [...d.comments, c] }));
    setActiveCommentId(c.id);
    // persist to Supabase
    if (window.IS_REVIEWER_MODE && tokenDocMeta && window.__REVIEWER_ID) {
      window.insertReviewerComment({
        documentId: tokenDocMeta.documentId,
        reviewerId: window.__REVIEWER_ID,
        blockId, range, text,
        authorName: author, authorColor: color,
      }).catch(e => console.error('insertReviewerComment', e));
    }
  };
  const updateComment = (id, patch) => setDoc(d => ({
    ...d, comments: d.comments.map(c => c.id === id ? { ...c, ...patch } : c),
  }));
  const removeComment = (id) => setDoc(d => ({ ...d, comments: d.comments.filter(c => c.id !== id) }));

  // suggestions
  const addSuggestion = ({ blockId, range, original, proposed, author, color }) => {
    const s = { id: uid(), blockId, range, original, proposed, author, color, status: 'pending', createdAt: Date.now() };
    setDoc(d => ({ ...d, suggestions: [...d.suggestions, s] }));
    setActiveCommentId(s.id);
    if (window.IS_REVIEWER_MODE && tokenDocMeta && window.__REVIEWER_ID) {
      window.insertReviewerSuggestion({
        documentId: tokenDocMeta.documentId,
        reviewerId: window.__REVIEWER_ID,
        blockId, range, original, proposed,
        authorName: author, authorColor: color,
      }).catch(e => console.error('insertReviewerSuggestion', e));
    }
  };
  const acceptSuggestion = (id) => setDoc(d => {
    const s = d.suggestions.find(x => x.id === id);
    if (!s) return d;
    recordSnapshot('Voorstel geaccepteerd', s.author);
    const blocks = d.blocks.map(b => {
      if (b.id !== s.blockId) return b;
      const before = b.text.slice(0, s.range.start);
      const after = b.text.slice(s.range.end);
      return { ...b, text: before + s.proposed + after };
    });
    return {
      ...d,
      blocks,
      suggestions: d.suggestions.map(x => x.id === id ? { ...x, status: 'accepted' } : x),
    };
  });
  const rejectSuggestion = (id) => setDoc(d => ({
    ...d, suggestions: d.suggestions.map(x => x.id === id ? { ...x, status: 'rejected' } : x),
  }));
  const removeSuggestion = (id) => setDoc(d => ({ ...d, suggestions: d.suggestions.filter(x => x.id !== id) }));

  const resetDoc = () => setDoc(makeEmptyDoc());
  const loadStarter = () => setDoc(makeStarterDoc());

  const value = {
    stage, setStage,
    currentDocId, setCurrentDocId, docLoading,
    tokenDocMeta,
    doc, setDoc,
    selectedBlockId, setSelectedBlockId,
    activeCommentId, setActiveCommentId,
    tweaks, setTweaks,
    rightTab, setRightTab,
    reviewer, setReviewer, currentAuthor,
    history, recordSnapshot, undo, revertTo,
    addBlock, updateBlock, removeBlock, moveBlock,
    updateMeta, updateHeadingStyle,
    addComment, updateComment, removeComment,
    addSuggestion, acceptSuggestion, rejectSuggestion, removeSuggestion,
    resetDoc, loadStarter,
  };
  return <AppCtx.Provider value={value}>{children}</AppCtx.Provider>;
}

// expose globally so other babel scripts can use
Object.assign(window, { AppProvider, AppCtx, useApp, uid, newBlock, makeEmptyDoc, makeStarterDoc });
