/**
 * sync-client.jsx — WebSocket-basierter Live-Sync Hook
 *
 * Architektur:
 *   - Kapselt die WS-Verbindung, Op-Queue (Offline-Puffer), Echo-Detection
 *     und exponential-backoff Reconnect
 *   - send(op): appliziert Op lokal (optimistisch), schickt an Server;
 *     bei 422 wird ein Full-Resync per GET /book ausgelöst
 *   - Offline: Ops in localStorage puffern; bei Reconnect abarbeiten
 *   - Echo-Detection: vom Server reflektierte eigene Ops werden übersprungen
 */

// LS-Schlüssel
const LS_LAST_SEQ = 'cb_last_seq_v1'
const LS_OP_QUEUE = 'cb_op_queue_v1'

const { useState, useEffect, useRef, useCallback } = React

function useSyncClient({ onOp, onForceSync, onToast }) {
  const [status, setStatus] = useState('disconnected')
  const [user, setUser] = useState(null)
  const [online, setOnline] = useState([])
  const [events, setEvents] = useState([]) // letzte 50 Envelopes für Activity-Feed

  const wsRef = useRef(null)
  const reconnTimerRef = useRef(null)
  const reconnDelayRef = useRef(1000)
  const lastSeqRef = useRef(parseInt(localStorage.getItem(LS_LAST_SEQ) || '0', 10))
  // Set mit opIds, die wir selbst abgeschickt haben — für Echo-Detection
  const ownOpIds = useRef(new Set())
  // Offline-Queue: Ops die nicht gesendet werden konnten
  const [opQueue, setOpQueue] = useState(() => {
    try { return JSON.parse(localStorage.getItem(LS_OP_QUEUE) || '[]') } catch { return [] }
  })
  const opQueueRef = useRef(opQueue)

  useEffect(() => {
    opQueueRef.current = opQueue
    localStorage.setItem(LS_OP_QUEUE, JSON.stringify(opQueue))
  }, [opQueue])

  // ── WS-Verbindung ────────────────────────────────────────────────────────
  const connect = useCallback(() => {
    if (wsRef.current?.readyState === 1) return // schon verbunden

    const proto = location.protocol === 'https:' ? 'wss' : 'ws'
    const ws = new WebSocket(`${proto}://${location.host}/ws`)
    wsRef.current = ws
    setStatus('connecting')

    ws.onopen = () => {
      reconnDelayRef.current = 1000
      setStatus('connected')
      ws.send(JSON.stringify({ type: 'hello', lastSeenSeq: lastSeqRef.current }))
      // Offline-Queue abarbeiten
      flushQueue()
    }

    ws.onmessage = (evt) => {
      let msg
      try { msg = JSON.parse(evt.data) } catch { return }

      if (msg.type === 'welcome') {
        setUser(msg.you)
        lastSeqRef.current = msg.seq
        localStorage.setItem(LS_LAST_SEQ, msg.seq)
      }

      if (msg.type === 'catchup') {
        msg.events.forEach(env => {
          if (!ownOpIds.current.has(env.opId)) {
            onOp(env)
          }
          lastSeqRef.current = Math.max(lastSeqRef.current, env.seq)
        })
        localStorage.setItem(LS_LAST_SEQ, lastSeqRef.current)
        setEvents(prev => {
          const merged = [...msg.events.filter(e => !ownOpIds.current.has(e.opId)), ...prev]
          return merged.slice(0, 50)
        })
      }

      if (msg.type === 'op') {
        const env = msg.envelope
        lastSeqRef.current = Math.max(lastSeqRef.current, env.seq)
        localStorage.setItem(LS_LAST_SEQ, lastSeqRef.current)
        if (!ownOpIds.current.has(env.opId)) {
          // Fremde Op → lokal anwenden
          onOp(env)
          setEvents(prev => [env, ...prev].slice(0, 50))
        } else {
          // Eigene Op bestätigt → aus Pending-Set entfernen
          ownOpIds.current.delete(env.opId)
          setEvents(prev => [env, ...prev].slice(0, 50))
        }
      }

      if (msg.type === 'presence') setOnline(msg.online || [])
      if (msg.type === 'pong') { /* keepalive ok */ }
    }

    ws.onclose = () => {
      connections_cleanup()
    }

    ws.onerror = () => {
      ws.close()
    }
  }, []) // eslint-disable-line

  function connections_cleanup() {
    wsRef.current = null
    setStatus('reconnecting')
    const delay = reconnDelayRef.current
    reconnDelayRef.current = Math.min(delay * 2, 30000)
    reconnTimerRef.current = setTimeout(() => connect(), delay)
  }

  // Keepalive-Ping alle 25s
  useEffect(() => {
    const id = setInterval(() => {
      if (wsRef.current?.readyState === 1) {
        wsRef.current.send(JSON.stringify({ type: 'ping' }))
      }
    }, 25000)
    return () => clearInterval(id)
  }, [])

  // Seiten-Sichtbarkeit → Presence-Update
  useEffect(() => {
    const onVis = () => {
      if (wsRef.current?.readyState === 1) {
        wsRef.current.send(JSON.stringify({
          type: 'presence',
          status: document.hidden ? 'idle' : 'active',
        }))
      }
    }
    document.addEventListener('visibilitychange', onVis)
    return () => document.removeEventListener('visibilitychange', onVis)
  }, [])

  // Initiale Verbindung
  useEffect(() => {
    connect()
    return () => {
      if (reconnTimerRef.current) clearTimeout(reconnTimerRef.current)
      if (wsRef.current) wsRef.current.close()
    }
  }, []) // eslint-disable-line

  // ── Offline-Queue abarbeiten ─────────────────────────────────────────────
  const flushQueue = useCallback(async () => {
    const queue = [...opQueueRef.current]
    if (queue.length === 0) return
    setOpQueue([])
    for (const { opId, op } of queue) {
      try {
        const res = await fetch('/book/op', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ op, opId }),
        })
        if (!res.ok && res.status !== 422) {
          // Fehlgeschlagen → wieder einreihen
          setOpQueue(q => [...q, { opId, op }])
        }
      } catch {
        setOpQueue(q => [...q, { opId, op }])
        break // Netzwerk weg → Rest bleibt in Queue
      }
    }
  }, [])

  // ── Op senden ────────────────────────────────────────────────────────────
  const send = useCallback(async (op) => {
    const opId = 'op_' + crypto.randomUUID()
    ownOpIds.current.add(opId)

    const isConnected = wsRef.current?.readyState === 1

    if (!isConnected) {
      // Offline → in Queue puffern (Op wurde lokal schon appliziert vom Aufrufer)
      setOpQueue(q => [...q, { opId, op }])
      return { queued: true }
    }

    try {
      const res = await fetch('/book/op', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ op, opId }),
      })

      if (res.status === 422) {
        ownOpIds.current.delete(opId)
        const data = await res.json()
        // Optimistische Änderung rückgängig machen via Full-Resync
        try {
          const bookRes = await fetch('/book')
          if (bookRes.ok) {
            const { book, seq } = await bookRes.json()
            onForceSync(book, seq)
          }
        } catch { /**/ }
        if (onToast) onToast(`Fehler: ${data.error || 'Aktion fehlgeschlagen'}`)
        return { error: data.error }
      }

      if (!res.ok) {
        ownOpIds.current.delete(opId)
        setOpQueue(q => [...q, { opId, op }])
      }

      return { ok: true }
    } catch {
      ownOpIds.current.delete(opId)
      setOpQueue(q => [...q, { opId, op }])
      return { queued: true }
    }
  }, [onForceSync, onToast])

  // ── Undo senden ─────────────────────────────────────────────────────────
  const undo = useCallback(async (opId) => {
    try {
      const res = await fetch('/book/undo', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ opId }),
      })
      const data = await res.json()
      if (!res.ok) {
        if (onToast) onToast(`Undo fehlgeschlagen: ${data.error || ''}`)
        return { error: data.error }
      }
      return { ok: true }
    } catch {
      if (onToast) onToast('Undo: Verbindungsfehler')
      return { error: 'Verbindungsfehler' }
    }
  }, [onToast])

  return {
    status,
    user,
    online,
    events,
    send,
    undo,
    lastSeq: lastSeqRef.current,
    queueLength: opQueue.length,
  }
}

window.useSyncClient = useSyncClient
