import { ref, computed } from 'vue'
import { useAuthStore } from 'src/stores/auth'

import { HUB_URL } from 'src/config/hub'
import { setAgentRoster } from 'src/composables/useFormatters'

function agentHeaders (extra = {}) {
  try {
    const email = useAuthStore().user
    if (email && email !== 'authenticated') return { 'Content-Type': 'application/json', 'X-Authentik-Email': email, ...extra }
  } catch {}
  return { 'Content-Type': 'application/json', ...extra }
}

const conversations = ref([])
const discussions = ref([])
const activeToken = ref(null)
const activeDiscussion = ref(null)
const activeConv = ref(null)
const tickets = ref([]) // tickets (Issues ERPNext) ouverts — inbox unifié
const showAll = ref(true)
const loading = ref(false)
const panelOpen = ref(false)
const newDialogOpen = ref(false) // dialogue « Nouveau texto » partagé (ouvrable depuis le FAB sticky ou le panneau)
const newDialogChannel = ref('sms') // canal présélectionné quand on ouvre le dialogue (sms | email) — posé par le FAB
// Pré-remplissage de la fenêtre Compose depuis une autre page (ex. fiche client → brouillon d'invitation d'évaluation).
const composePrefill = ref(null) // { channel, to, phone, subject, html, text, customer, customerName }
function openComposeDraft (payload) { composePrefill.value = payload || null; if (payload && payload.channel) newDialogChannel.value = payload.channel; newDialogOpen.value = true }
const selectedIds = ref(new Set())
let sseSource = null
let ssePoll = null
// Résilience SSE (survit aux redéploiements du hub qui coupent tous les flux en mémoire) : sur (RE)connexion → resync
// fetchList() (rattrape les courriels arrivés pendant la coupure) + coupe le repli ; sur fermeture PERMANENTE (502 pendant
// le restart → EventSource CLOSED, plus de reconnexion native) → repli poll 25 s + reconnexion manuelle. reconnectFn re-crée+re-arme.
function armSSE (reconnectFn) {
  if (!sseSource) return
  sseSource.onopen = () => { if (ssePoll) { clearInterval(ssePoll); ssePoll = null } fetchList() }
  sseSource.onerror = () => { if (sseSource && sseSource.readyState === EventSource.CLOSED) { if (!ssePoll) ssePoll = setInterval(() => fetchList(), 25000); setTimeout(reconnectFn, 3000) } }
}

// Présence agent : quel autre agent OPS est en train d'écrire dans une conversation. { [convToken]: agentEmail }
const agentTyping = ref({})
const _typingTimers = {}
// Identifiant d'onglet (par session navigateur) : on ne filtre QUE sa propre frappe (même onglet),
// pas celle des autres onglets/users → deux users se voient, et 2 onglets du même compte aussi (testable seul).
const _tabSid = Math.random().toString(36).slice(2) + Date.now().toString(36)

// ── Notifications navigateur sur NOUVEAU courriel (Notifications API ; fonctionne quand l'app est ouverte, même onglet en arrière-plan) ──
const notifyEnabled = ref(false)
try { notifyEnabled.value = (typeof Notification !== 'undefined' && Notification.permission === 'granted') } catch (e) { /* */ }
async function requestNotifyPermission () {
  try { if (typeof Notification === 'undefined') return false; const p = await Notification.requestPermission(); notifyEnabled.value = (p === 'granted'); return notifyEnabled.value } catch (e) { return false }
}
function notifyNewEmail (d) {
  try {
    if (!notifyEnabled.value || typeof Notification === 'undefined' || Notification.permission !== 'granted') return
    if (d && d.triage && d.triage.noise) return // jamais de notif sur le bruit (infra / no-reply / interne)
    const who = (d && d.triage && d.triage.customer_name) || (d && d.email) || 'Nouveau courriel'
    const subj = (d && d.triage && d.triage.suggested_title) || 'Nouveau message dans l\'inbox'
    const n = new Notification('📨 ' + who, { body: subj, tag: 'conv:' + (d && d.token) })
    n.onclick = () => { try { window.focus(); if (d && d.token) window.location.hash = '#/communications/c/' + d.token } catch (e) { /* */ } }
  } catch (e) { /* */ }
}
function clearTyping (token) {
  if (!token) return
  clearTimeout(_typingTimers[token])
  if (agentTyping.value[token]) { const n = { ...agentTyping.value }; delete n[token]; agentTyping.value = n }
}
function handleAgentTyping (data) {
  if (!data || !data.token || !data.agent) return
  if (data.sid && data.sid === _tabSid) return // mon propre onglet → ne pas m'afficher à moi-même
  agentTyping.value = { ...agentTyping.value, [data.token]: data.agent }
  clearTimeout(_typingTimers[data.token])
  _typingTimers[data.token] = setTimeout(() => clearTyping(data.token), 6000)
}
let _lastTypingSent = 0
async function notifyTyping (token) {
  if (!token) return
  const now = Date.now()
  if (now - _lastTypingSent < 2500) return // throttle réseau
  _lastTypingSent = now
  try { await fetch(`${HUB_URL}/conversations/${token}/typing?sid=${_tabSid}`, { method: 'POST', headers: agentHeaders() }) } catch {}
}

// Brouillon partagé reçu d'un AUTRE agent (miroir temps réel) : { [token]: { agent, sid, html, text, ts } | null }
const sharedDraft = ref({})
let _lastDraftSent = 0
function handleConvDraft (data) {
  if (!data || !data.token) return
  if (data.sid && data.sid === _tabSid) return // mon propre brouillon → ne pas me le renvoyer à moi-même
  sharedDraft.value = { ...sharedDraft.value, [data.token]: data.draft || null }
}
function handleConvRead (data) {
  if (!data || !data.token || !data.agent) return
  const rd = { readAt: data.readAt, lastTs: data.lastTs }
  if (activeConv.value && activeToken.value === data.token) {
    activeConv.value.readBy = { ...(activeConv.value.readBy || {}), [data.agent]: rd }
  }
  // patch la LISTE (gras non-lu + avatars « vu par » en direct)
  const disc = discussions.value.find(d => d.token === data.token || (d.conversations || []).some(c => c.token === data.token))
  if (disc) disc.readBy = { ...(disc.readBy || {}), [data.agent]: rd }
}

export function useConversations () {
  async function fetchList () {
    loading.value = true
    try {
      const url = showAll.value ? `${HUB_URL}/conversations?all=1` : `${HUB_URL}/conversations`
      const res = await fetch(url)
      if (res.ok) {
        const data = await res.json()
        conversations.value = data.conversations || []
        discussions.value = data.discussions || []
      }
    } catch {}
    loading.value = false
  }

  // Tickets ouverts (Issues ERPNext) pour l'inbox unifié.
  async function fetchTickets () {
    try {
      const res = await fetch(`${HUB_URL}/conversations/inbox-tickets`, { headers: agentHeaders() })
      if (res.ok) { const d = await res.json(); tickets.value = d.tickets || [] }
    } catch {}
  }

  function openDiscussion (disc) {
    activeDiscussion.value = disc; activeToken.value = null; activeConv.value = null
    const active = disc.conversations.find(c => c.status === 'active') || disc.conversations[disc.conversations.length - 1]
    const tok = (active && active.token) || disc.token
    if (tok) {
      activeToken.value = tok
      markRead(tok)
      // charge le DÉTAIL (notes, file, assigné, gmailReplyUrl, suggestion) pour la barre de coordination
      fetch(`${HUB_URL}/conversations/${tok}`).then(r => r.ok ? r.json() : null).then(d => { if (d && activeToken.value === tok) activeConv.value = d }).catch(() => {})
    }
    loadDiscussionMessages(disc) // la LISTE est allégée (pas de messages) → on charge/fusionne les messages des tokens ici
    connectDiscussionSSE(disc.conversations.map(c => c.token))
  }
  // Charge + fusionne les messages de tous les tokens d'une discussion (liste légère → messages à la demande).
  async function loadDiscussionMessages (disc) {
    if (!disc || (Array.isArray(disc.messages) && disc.messages.length)) return // déjà chargés
    const toks = (disc.conversations || []).map(c => c.token).filter(Boolean)
    if (!toks.length) return
    try {
      const results = await Promise.all(toks.map(t => fetch(`${HUB_URL}/conversations/${t}`).then(r => r.ok ? r.json() : null).catch(() => null)))
      const merged = []
      results.forEach((d, i) => { if (d && Array.isArray(d.messages)) for (const m of d.messages) merged.push({ ...m, convToken: toks[i] }) })
      merged.sort((a, b) => String(a.ts || '').localeCompare(String(b.ts || '')))
      if (activeDiscussion.value && (activeDiscussion.value === disc || activeDiscussion.value.id === disc.id)) { disc.messages = merged; disc.messageCount = merged.length }
    } catch {}
  }

  async function openConversation (token) {
    activeToken.value = token
    markRead(token)
    try {
      const res = await fetch(`${HUB_URL}/conversations/${token}`)
      if (res.ok) {
        const d = await res.json()
        activeConv.value = d
        // IMPORTANT : la vue (surtout plein écran) s'affiche d'après activeDiscussion → on en construit une depuis le détail,
        // sinon un fil hors-liste (URL directe, conv non listée) reste coincé sur « Chargement… ».
        activeDiscussion.value = {
          token, customerName: d.customerName, email: d.email, phone: d.phone, channel: d.channel || 'email',
          status: d.status, subject: d.subject, messages: d.messages || [],
          conversations: [{ token, status: d.status }], messageCount: (d.messages || []).length,
          date: String(d.createdAt || '').slice(0, 10), readBy: d.readBy || {},
        }
      }
    } catch {}
    connectSSE(token)
  }

  function connectDiscussionSSE (tokens) {
    if (sseSource) sseSource.close()
    sseSource = new EventSource(`${HUB_URL}/sse?topics=${tokens.map(t => 'conv:' + t).concat(['conversations']).join(',')}`)
    sseSource.addEventListener('conv-message', e => {
      try {
        const data = JSON.parse(e.data)
        if (activeDiscussion.value && data.message) {
          const msg = { ...data.message, convToken: data.token }
          if (!activeDiscussion.value.messages.find(m => m.id === msg.id)) activeDiscussion.value.messages.push(msg)
        }
        updateListFromMessage(data)
      } catch {}
    })
    sseSource.addEventListener('conv-agent-typing', e => { try { handleAgentTyping(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-read', e => { try { handleConvRead(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-draft', e => { try { handleConvDraft(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-closed', handleConvClosed)
    sseSource.addEventListener('conv-deleted', () => fetchList())
    sseSource.addEventListener('conv-email', e => { try { const d = JSON.parse(e.data); fetchList(); notifyNewEmail(d) } catch (er) { /* */ } }) // NOUVEAU courriel ingéré → rafraîchit la liste EN DIRECT + notif navigateur
    armSSE(() => connectDiscussionSSE(tokens))
  }

  function updateListFromMessage (data) {
    clearTyping(data.token) // un message est arrivé → l'agent a fini d'écrire
    const idx = conversations.value.findIndex(c => c.token === data.token)
    if (idx >= 0) {
      conversations.value[idx].lastMessage = data.message
      conversations.value[idx].lastActivity = data.message.ts
      conversations.value[idx].messageCount++
    } else fetchList()
  }

  function handleConvClosed (e) {
    try {
      const data = JSON.parse(e.data)
      const idx = conversations.value.findIndex(c => c.token === data.token)
      if (idx >= 0) conversations.value[idx].status = 'closed'
      if (activeConv.value && data.token === activeToken.value) activeConv.value.status = 'closed'
    } catch {}
  }

  function connectSSE (token) {
    if (sseSource) sseSource.close()
    sseSource = new EventSource(`${HUB_URL}/sse?topics=conv:${token},conversations`)
    sseSource.addEventListener('conv-message', e => {
      try {
        const data = JSON.parse(e.data)
        if (activeConv.value && data.token === activeToken.value && data.message) {
          if (!activeConv.value.messages.find(m => m.id === data.message.id)) activeConv.value.messages.push(data.message)
        }
        updateListFromMessage(data)
      } catch {}
    })
    sseSource.addEventListener('conv-agent-typing', e => { try { handleAgentTyping(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-read', e => { try { handleConvRead(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-draft', e => { try { handleConvDraft(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-closed', handleConvClosed)
    sseSource.addEventListener('conv-deleted', () => fetchList())
    sseSource.addEventListener('conv-email', e => { try { const d = JSON.parse(e.data); fetchList(); notifyNewEmail(d) } catch (er) { /* */ } }) // NOUVEAU courriel ingéré → rafraîchit la liste EN DIRECT + notif navigateur
    armSSE(() => connectSSE(token))
  }

  function connectGlobalSSE () {
    if (sseSource) sseSource.close()
    sseSource = new EventSource(`${HUB_URL}/sse?topics=conversations`)
    sseSource.addEventListener('conv-message', e => { try { updateListFromMessage(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-agent-typing', e => { try { handleAgentTyping(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-read', e => { try { handleConvRead(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-draft', e => { try { handleConvDraft(JSON.parse(e.data)) } catch {} })
    sseSource.addEventListener('conv-deleted', () => fetchList())
    sseSource.addEventListener('conv-email', e => { try { const d = JSON.parse(e.data); fetchList(); notifyNewEmail(d) } catch (er) { /* */ } }) // NOUVEAU courriel ingéré → rafraîchit la liste EN DIRECT + notif navigateur
    armSSE(connectGlobalSSE)
  }

  // Recherche FLOUE omnichannel (SQL pg_trgm côté hub) : nom client / courriel / sujet + corps des messages, typo-tolérante.
  async function searchConversations (q) {
    const term = String(q || '').trim()
    if (!term) return []
    try {
      const res = await fetch(`${HUB_URL}/conversations/search?q=${encodeURIComponent(term)}`, { headers: agentHeaders() })
      if (!res.ok) return []
      const d = await res.json()
      return Array.isArray(d.results) ? d.results : []
    } catch { return [] }
  }

  // Priorité (style ClickUp) : urgent | high | normal | low | '' (aucune). Patch optimiste de la liste + du détail.
  async function setPriority (token, priority) {
    const res = await fetch(`${HUB_URL}/conversations/${token}/priority`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ priority }) })
    if (!res.ok) throw new Error('Échec priorité')
    const d = await res.json()
    if (activeConv.value && activeToken.value === token) activeConv.value.priority = d.priority
    const disc = discussions.value.find(x => x.token === token || (x.conversations || []).some(c => c.token === token))
    if (disc) disc.priority = d.priority
    return d.priority
  }

  // Lier (interdépendances) : relie un fil à un autre fil / fiche / ticket (ou retire avec remove:true).
  async function relateConversation (token, { targetType, targetId, targetLabel, rel, remove } = {}) {
    const res = await fetch(`${HUB_URL}/conversations/${token}/relate`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ targetType, targetId, targetLabel, rel, remove: !!remove }) })
    if (!res.ok) throw new Error('Échec du lien')
    const d = await res.json()
    if (activeConv.value && activeToken.value === token) activeConv.value.links = d.links || []
    return d.links || []
  }

  async function sendMessage (token, text, media, html, attachments, sendAs, cc, replyTo) {
    const res = await fetch(`${HUB_URL}/conversations/${token}/messages`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ text, media, html, attachments, sendAs, cc: cc || '', to: replyTo || '' }) })
    if (!res.ok) throw new Error('Failed to send')
    return res.json()
  }

  // Rafraîchir À LA DEMANDE : force le hub à interroger Gmail tout de suite (≠ fetchList qui relit l'état déjà ingéré).
  async function pollNow () {
    try { const r = await fetch(`${HUB_URL}/conversations/poll-now`, { method: 'POST', headers: agentHeaders() }); return r.ok ? await r.json() : null } catch { return null }
  }

  // Cherche les fiches client par téléphone → pour rattacher / choisir avant de démarrer le texto.
  async function resolvePhone (phone) {
    if (!phone || phone.replace(/\D/g, '').length < 7) return []
    try { const res = await fetch(`${HUB_URL}/conversations/resolve?phone=${encodeURIComponent(phone)}`, { headers: agentHeaders() }); if (!res.ok) return []; const d = await res.json(); return d.matches || [] } catch { return [] }
  }
  // Idem par COURRIEL (canal email) — alimente le même sélecteur de fiche que le SMS.
  async function resolveEmail (email) {
    if (!email || !/.+@.+\..+/.test(email)) return []
    try { const res = await fetch(`${HUB_URL}/conversations/resolve?email=${encodeURIComponent(email)}`, { headers: agentHeaders() }); if (!res.ok) return []; const d = await res.json(); return d.matches || [] } catch { return [] }
  }
  // Rattache la conversation à une fiche client choisie (résout les multi-fiches, comme pour le SMS).
  async function linkCustomer (token, customer, customerName) {
    const res = await fetch(`${HUB_URL}/conversations/${token}/link`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ customer, customerName }) })
    if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || 'Liaison échouée') }
    const d = await res.json()
    if (activeConv.value && activeToken.value === token) { activeConv.value.customer = d.customer; activeConv.value.customerName = d.customerName }
    if (activeDiscussion.value) { activeDiscussion.value.customer = d.customer; activeDiscussion.value.customerName = d.customerName }
    await fetchList()
    return d
  }

  async function startConversation ({ phone, customer, customerName, subject, message, media }) {
    const res = await fetch(`${HUB_URL}/conversations`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ phone, customer, customerName, subject, message, media }) })
    if (!res.ok) throw new Error('Failed to create conversation')
    const data = await res.json(); await fetchList(); return data
  }

  // Nouveau courriel SORTANT (part de cc@/support@ via Gmail) → conversation email suivie
  async function startEmail ({ to, cc, bcc, subject, body, html, attachments, customer, customerName, sendAs }) {
    const res = await fetch(`${HUB_URL}/conversations/email-new`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ to, cc, bcc, subject, body, html, attachments, customer, customerName, sendAs }) })
    if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || 'Envoi du courriel échoué') }
    const data = await res.json(); await fetchList(); return data
  }

  async function closeConversation (token) {
    await fetch(`${HUB_URL}/conversations/${token}/close`, { method: 'POST', headers: agentHeaders() })
    if (activeConv.value && activeToken.value === token) activeConv.value.status = 'closed'
    const idx = conversations.value.findIndex(c => c.token === token)
    if (idx >= 0) conversations.value[idx].status = 'closed'
  }

  async function deleteDiscussion (disc) {
    const res = await fetch(`${HUB_URL}/conversations/discussion`, {
      method: 'DELETE', headers: agentHeaders(),
      body: JSON.stringify({ phone: disc.phone, date: disc.id.includes(':active') ? 'active' : disc.date }),
    })
    if (!res.ok) throw new Error('Failed to delete')
    clearActiveIfMatch(disc); await fetchList()
  }

  async function bulkDelete (discs) {
    const res = await fetch(`${HUB_URL}/conversations/bulk`, {
      method: 'DELETE', headers: agentHeaders(),
      body: JSON.stringify({ discussions: discs.map(d => ({ tokens: d.conversations.map(c => c.token) })) }),
    })
    if (!res.ok) throw new Error('Failed to bulk delete')
    const data = await res.json()
    for (const d of discs) clearActiveIfMatch(d)
    selectedIds.value.clear(); await fetchList(); return data
  }

  async function archiveDiscussion (disc) {
    const res = await fetch(`${HUB_URL}/conversations/archive`, {
      method: 'POST', headers: agentHeaders(),
      body: JSON.stringify({ tokens: disc.conversations.map(c => c.token) }),
    })
    if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || 'Archive failed') }
    const data = await res.json(); clearActiveIfMatch(disc); await fetchList(); return data
  }

  async function bulkArchive (discs) {
    const results = []
    for (const d of discs) {
      const res = await fetch(`${HUB_URL}/conversations/archive`, {
        method: 'POST', headers: agentHeaders(),
        body: JSON.stringify({ tokens: d.conversations.map(c => c.token) }),
      })
      if (res.ok) results.push(await res.json())
    }
    selectedIds.value.clear(); await fetchList(); return results
  }

  function clearActiveIfMatch (disc) {
    if (activeDiscussion.value?.id === disc.id) { activeDiscussion.value = null; activeToken.value = null; activeConv.value = null }
  }

  // ── Coordination shared-inbox : file (label) + claim + notes ──
  const QUEUES = ['Facturations', 'Service à la clientèle', 'Supports', 'Technicien']
  async function assignQueue (token, queue) {
    await fetch(`${HUB_URL}/conversations/${token}/queue`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ queue }) })
    if (activeConv.value && activeToken.value === token) activeConv.value.queue = queue
    if (activeDiscussion.value) activeDiscussion.value.queue = queue
    await fetchList()
  }
  async function claimConv (token) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/claim`, { method: 'POST', headers: agentHeaders() })
    const d = await r.json().catch(() => ({}))
    if (activeConv.value && activeToken.value === token) activeConv.value.assignee = d.assignee
    if (activeDiscussion.value) activeDiscussion.value.assignee = d.assignee
    await fetchList(); return d
  }
  async function releaseConv (token) {
    await fetch(`${HUB_URL}/conversations/${token}/release`, { method: 'POST', headers: agentHeaders() })
    if (activeConv.value && activeToken.value === token) activeConv.value.assignee = null
    if (activeDiscussion.value) activeDiscussion.value.assignee = null
    await fetchList()
  }
  // Assignation directe (agent précis) + assistants (chips). assignee=null pour désassigner.
  async function assignConv (token, { assignee, assistants } = {}) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/assign`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ assignee, assistants }) })
    const d = await r.json().catch(() => ({}))
    for (const c of [activeConv.value, activeDiscussion.value]) { if (c && (c.token === token || activeToken.value === token)) { c.assignee = d.assignee; c.assistants = d.assistants || [] } }
    await fetchList(); return d
  }
  // État ticket : 'open' | 'pending' (+ pendingUntil YYYY-MM-DD) | 'closed'.
  async function setConvState (token, state, pendingUntil) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/state`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ state, pendingUntil }) })
    const d = await r.json().catch(() => ({}))
    for (const c of [activeConv.value, activeDiscussion.value]) { if (c && (c.token === token || activeToken.value === token)) { c.ticketState = d.ticketState; c.pendingUntil = d.pendingUntil; if (d.status) c.status = d.status } }
    await fetchList(); return d
  }
  let _agents = null
  async function fetchAgents () { if (_agents && _agents.length) return _agents; try { const r = await fetch(`${HUB_URL}/conversations/agents`, { headers: agentHeaders() }); if (r.ok) { const d = await r.json(); if (d.names) setAgentRoster(d.names); if (d.agents && d.agents.length) _agents = d.agents } } catch {} return _agents || [] } // peuple aussi le roster nom-complet ; ne cache PAS une liste vide → réessaie au prochain appel
  // Spam → marque le(s) courriel(s) SPAM côté Gmail + retire la conversation.
  async function spamConv (token) { const r = await fetch(`${HUB_URL}/conversations/${token}/spam`, { method: 'POST', headers: agentHeaders() }); const d = await r.json().catch(() => ({})); await fetchList(); return d }
  // Suppression par TOKEN (≠ deleteDiscussion par phone/date) → côté hub, met aussi les courriels liés à la corbeille Gmail.
  async function deleteConvByToken (token) { const r = await fetch(`${HUB_URL}/conversations/${token}`, { method: 'DELETE', headers: agentHeaders() }); if (!r.ok) throw new Error('Échec suppression'); await fetchList(); return r.json().catch(() => ({})) }
  // Pipeline de leads : board (colonnes par étape) + déplacement
  async function pipelineBoard () { try { const r = await fetch(`${HUB_URL}/conversations/pipeline-board`, { headers: agentHeaders() }); if (r.ok) return await r.json() } catch {} return { stages: [], columns: {} } }
  async function setPipeline (token, stage, value) { await fetch(`${HUB_URL}/conversations/${token}/pipeline`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ stage, value }) }) }
  async function nlCommand (token, command) {
    const res = await fetch(`${HUB_URL}/conversations/${token}/nl`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ command }) })
    if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || 'Commande échouée') }
    const d = await res.json()
    try { if (activeToken.value === token) { const r = await fetch(`${HUB_URL}/conversations/${token}`); if (r.ok) activeConv.value = await r.json() } } catch {}
    await fetchList()
    return d
  }
  async function addNote (token, text) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/note`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ text }) })
    const d = await r.json().catch(() => ({}))
    if (d.note) { if (!activeConv.value) activeConv.value = {}; activeConv.value.notes = activeConv.value.notes || []; activeConv.value.notes.push(d.note) }
    return d
  }
  async function editNote (token, id, text) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/note`, { method: 'PATCH', headers: agentHeaders(), body: JSON.stringify({ id, text }) })
    const d = await r.json().catch(() => ({}))
    if (!r.ok) throw new Error(d.error || `Hub ${r.status}`)
    const n = activeConv.value?.notes?.find(x => x.id === id); if (n) { n.text = d.note?.text ?? text; n.edited = d.note?.edited }
    return d
  }
  async function deleteNote (token, id) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/note`, { method: 'DELETE', headers: agentHeaders(), body: JSON.stringify({ id }) })
    const d = await r.json().catch(() => ({}))
    if (!r.ok) throw new Error(d.error || `Hub ${r.status}`)
    if (activeConv.value?.notes) activeConv.value.notes = activeConv.value.notes.filter(x => x.id !== id)
    return d
  }
  // Pièces jointes (PDF/JPG) du fil → { msgId: [{filename, mimeType, attachmentId, size, gmail_id}] }
  async function fetchAttachments (token) {
    try { const r = await fetch(`${HUB_URL}/conversations/${token}/attachments`, { headers: agentHeaders() }); const d = await r.json().catch(() => ({})); return d.attachments || {} } catch (e) { return {} }
  }
  // Fils (email + SMS) d'UN client → pour la timeline « Communications » sur la fiche
  async function fetchForCustomer ({ customer, email, phone } = {}) {
    const qs = new URLSearchParams()
    if (customer) qs.set('customer', customer); if (email) qs.set('email', email); if (phone) qs.set('phone', phone)
    if (!qs.toString()) return []
    try { const r = await fetch(`${HUB_URL}/conversations/for-customer?${qs}`, { headers: agentHeaders() }); const d = await r.json().catch(() => ({})); return d.discussions || [] } catch (e) { return [] }
  }

  // Crée un ticket (ERPNext Issue OUVERT) depuis une conversation — la conversation RESTE (≠ archive).
  async function createTicket (token, payload) {
    const res = await fetch(`${HUB_URL}/conversations/${token}/ticket`, {
      method: 'POST', headers: agentHeaders(), body: JSON.stringify(payload || {}),
    })
    if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || 'Création du ticket échouée') }
    const data = await res.json()
    try { if (activeToken.value === token) { const r = await fetch(`${HUB_URL}/conversations/${token}`); if (r.ok) activeConv.value = await r.json() } } catch {}
    await fetchList()
    return data
  }

  // SPLIT — détecte les sujets distincts (aperçu) puis crée 1 ticket par sujet sur confirmation.
  async function splitPreview (token) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/split-preview`, { headers: agentHeaders() })
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || 'Analyse échouée') }
    return await r.json()
  }
  async function splitConversation (token, topics) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/split`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ topics }) })
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || 'Scission échouée') }
    const d = await r.json()
    try { if (activeToken.value === token) { const x = await fetch(`${HUB_URL}/conversations/${token}`); if (x.ok) activeConv.value = await x.json() } } catch {}
    await fetchList()
    return d
  }
  // MERGE — fusionne la conversation SOURCE dans la CIBLE (target garde tout).
  async function mergeConversation (targetToken, fromToken) {
    const r = await fetch(`${HUB_URL}/conversations/${targetToken}/merge`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ from: fromToken }) })
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || 'Fusion échouée') }
    const d = await r.json()
    await fetchList()
    return d
  }
  // Preuve de paiement : OCR d'une image du fil → {is_payment, amount, date, reference, method…}. Affiche pour décider (pas d'écriture facturation).
  async function analyzePayment (token, image, messageId) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/payment-proof`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ image, messageId }) })
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || 'Analyse échouée') }
    const d = await r.json()
    if (activeConv.value && activeToken.value === token) activeConv.value.paymentProof = d.payment
    return d
  }
  // Résumé IA du fil (style « Summarise this email » de Gmail) → puces pour l'agent. Ne modifie rien.
  async function summarize (token) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/summarize`, { method: 'POST', headers: agentHeaders() })
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || 'Résumé indisponible') }
    return await r.json()
  }
  // Règles d'inbox (« Filtrer comme ceci ») : l'agent choisit ce qui est masqué/affiché (par expéditeur ou sujet).
  async function fetchInboxRules () { try { const r = await fetch(`${HUB_URL}/conversations/inbox-rules`, { headers: agentHeaders() }); if (r.ok) return (await r.json()).rules || [] } catch {} return [] }
  async function saveInboxRules (rules) {
    const r = await fetch(`${HUB_URL}/conversations/inbox-rules`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ rules }) })
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || 'Échec enregistrement règle') }
    const d = await r.json(); await fetchList(); return d
  }
  // Disponibilité du service : adresse (IA) → fibre (RQA) → brouillon de réponse. N'ENVOIE RIEN (l'agent valide).
  async function serviceability (token, address) {
    const r = await fetch(`${HUB_URL}/conversations/${token}/serviceability`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(address ? { address } : {}) })
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || 'Vérification indisponible') }
    return await r.json()
  }
  // Crée un ticket (Issue) AUTONOME — depuis le FAB « Créer un ticket » (hors conversation).
  async function createStandaloneTicket (payload) {
    const r = await fetch(`${HUB_URL}/collab/ticket`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify(payload || {}) })
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || 'Création échouée') }
    const d = await r.json()
    fetchTickets().catch(() => {})
    return d
  }
  // Réponses pré-écrites (canned)
  async function fetchCanned () { try { const r = await fetch(`${HUB_URL}/conversations/canned`, { headers: agentHeaders() }); if (r.ok) { const d = await r.json(); return d.responses || [] } } catch {} return [] }
  async function saveCanned (responses) { const r = await fetch(`${HUB_URL}/conversations/canned`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ responses }) }); return r.ok }

  function toggleSelect (discId) {
    const s = new Set(selectedIds.value)
    s.has(discId) ? s.delete(discId) : s.add(discId)
    selectedIds.value = s
  }

  function selectAll () { selectedIds.value = new Set(discussions.value.map(d => d.id)) }
  function clearSelection () { selectedIds.value = new Set() }

  const selectedDiscussions = computed(() => discussions.value.filter(d => selectedIds.value.has(d.id)))

  function disconnectSSE () { if (ssePoll) { clearInterval(ssePoll); ssePoll = null } if (sseSource) { sseSource.close(); sseSource = null } }

  function goBack () {
    activeToken.value = null; activeConv.value = null; activeDiscussion.value = null
    if (sseSource) sseSource.close()
    connectGlobalSSE()
  }

  // « Vu par » : signale que cet agent a ouvert la conversation (avatars chez les autres).
  async function markRead (token) {
    if (!token) return
    try { await fetch(`${HUB_URL}/conversations/${token}/read`, { method: 'POST', headers: agentHeaders() }) } catch {}
  }
  // Pousse le brouillon courant aux autres agents (throttle frappe ; l'envoi VIDE passe toujours pour effacer le miroir).
  function pushDraft (token, html, text, replyTo) {
    if (!token) return
    const empty = !String(html || '').replace(/<[^>]+>/g, '').trim() && !String(text || '').trim()
    const now = Date.now()
    if (!empty && now - _lastDraftSent < 700) return
    _lastDraftSent = now
    try { fetch(`${HUB_URL}/conversations/${token}/draft?sid=${_tabSid}`, { method: 'POST', headers: agentHeaders(), body: JSON.stringify({ html: html || '', text: text || '', replyTo: replyTo || null }) }) } catch {}
  }

  const activeCount = computed(() => discussions.value.filter(d => d.status === 'active').length)

  return {
    conversations, discussions, activeToken, activeDiscussion, activeConv, tickets,
    loading, panelOpen, newDialogOpen, newDialogChannel, composePrefill, openComposeDraft, createStandaloneTicket, showAll, selectedIds, selectedDiscussions,
    fetchList, fetchTickets, openDiscussion, openConversation, sendMessage, pollNow, startConversation, startEmail, resolvePhone, resolveEmail, searchConversations, relateConversation, setPriority, linkCustomer,
    closeConversation, deleteDiscussion, bulkDelete, archiveDiscussion, bulkArchive, createTicket,
    QUEUES, assignQueue, claimConv, releaseConv, assignConv, setConvState, fetchAgents, spamConv, deleteConvByToken, addNote, editNote, deleteNote, fetchAttachments, fetchForCustomer, nlCommand, pipelineBoard, setPipeline,
    splitPreview, splitConversation, mergeConversation, fetchCanned, saveCanned, analyzePayment, summarize, serviceability, fetchInboxRules, saveInboxRules,
    toggleSelect, selectAll, clearSelection,
    goBack, disconnectSSE, connectGlobalSSE, activeCount, HUB_URL,
    agentTyping, notifyTyping, sharedDraft, markRead, pushDraft,
    notifyEnabled, requestNotifyPermission,
  }
}
