/**
 * WebSocket endpoint for the staff live-chat module — /chat/ws
 *
 * One socket per browser tab carries everything live: messages, presence,
 * heartbeats and activity (active/idle) signals. REST (server/api/chat/*) is
 * used only for history / unread seed / mark-read.
 *
 * Auth + staff gate run in open() using the cookie on the upgrade request, so
 * a non-staff / limited-role connection is rejected before it can do anything.
 *
 * Kill-switch: when runtimeConfig.chatEnabled !== true every connection is
 * closed immediately. Removing the chat module = delete the chat files; nothing
 * else references this handler.
 *
 * Client frames:  { type:'ping' } | { type:'activity', active }
 *               | { type:'read', peer } | { type:'typing', to, typing }
 *               | { type:'message', to, body, clientMsgId, fromName, meta? }
 *               | { type:'react', messageId, emoji, on }
 * Server frames:  connected | presence-snapshot | presence | message
 *               | message-update | reaction | pong | read | read-receipt
 *               | typing | error
 * (Voice messages are uploaded over REST — server/api/chat/voice — which then
 *  fans out the `message` / `message-update` frames the same way.)
 */

import { parseCookieHeader, decodeJwt, assertStaffAccess, isStaffUser } from '../../utils/chatAccess'
import * as presence from '../../utils/chatPresence'
import { insertMessage, markConversationRead, getMessageById, setReaction } from '../../utils/chatDb'
import { sendPushToUser, buildChatMessagePayload } from '../../utils/pushNotifier'
import { sendFcmToUser } from '../../utils/fcmNotifier'

const chatEnabled = () => useRuntimeConfig().chatEnabled === true

const getCookieHeader = (peer: any): string => {
  const h = peer?.request?.headers
  if (!h) return ''
  if (typeof h.get === 'function') return h.get('cookie') || ''
  return h.cookie || ''
}

// Bundled app fallback: the WebView WS upgrade can't send an Authorization header or app.logship.de
// cookies, so the client puts the bearer token in the upgrade URL query (?access_token=...).
const getQueryToken = (peer: any): string => {
  try {
    const url = peer?.request?.url || ''
    if (!url) return ''
    return new URL(url, 'http://localhost').searchParams.get('access_token') || ''
  } catch { return '' }
}

const toText = (message: any): string => {
  if (typeof message === 'string') return message
  if (message && typeof message.text === 'function') return message.text()
  return String(message ?? '')
}

const send = (peer: any, frame: any) => {
  try { peer.send(JSON.stringify(frame)) } catch { /* gone */ }
}

// Per-socket send rate-limit (sliding window). In-memory, per-process — a
// backstop against a runaway tab, not a security boundary. Cleared on close.
const RATE_MAX = 20
const RATE_WINDOW_MS = 10000
const sendTimes = new Map<string, number[]>()
const allowSend = (peerId: string): boolean => {
  const now = Date.now()
  const recent = (sendTimes.get(peerId) || []).filter((t) => now - t < RATE_WINDOW_MS)
  if (recent.length >= RATE_MAX) { sendTimes.set(peerId, recent); return false }
  recent.push(now)
  sendTimes.set(peerId, recent)
  return true
}

export default defineWebSocketHandler({
  async open(peer) {
    if (!chatEnabled()) { try { peer.close(3001, 'chat disabled') } catch {}; return }
    try {
      const cookies = parseCookieHeader(getCookieHeader(peer))
      // Web sends the httpOnly logship_it cookie; the bundled app passes ?access_token (see above).
      const token = cookies['logship_it'] || getQueryToken(peer)
      if (!token) throw new Error('no token')

      const payload = decodeJwt(token)
      const userId = Number(payload.AD_User_ID)
      const roleId = Number(payload.AD_Role_ID)
      if (!userId || !roleId) throw new Error('bad token')

      await assertStaffAccess(userId, roleId, token) // throws if not staff / limited role

      presence.addPeer({ peer, peerId: peer.id, userId, roleId, token })
      console.log(`[ChatWS] connected user=${userId} peer=${peer.id}`)
    } catch (err: any) {
      console.warn('[ChatWS] rejected:', err?.message)
      try { peer.close(3000, 'unauthorized') } catch {}
    }
  },

  async message(peer, message) {
    if (!chatEnabled()) return
    const meta = presence.getPeer(peer.id)
    if (!meta) return // not authed (gate failed or still pending)

    let frame: any
    try { frame = JSON.parse(toText(message)) } catch { return }
    if (!frame || typeof frame !== 'object') return

    switch (frame.type) {
      case 'ping':
        presence.touch(peer.id)
        send(peer, { type: 'pong' })
        return

      case 'activity':
        presence.setActive(peer.id, !!frame.active)
        return

      case 'typing': {
        // Ephemeral, never stored. Relay to the recipient only if they're online.
        const to = Number(frame.to)
        if (!to || to === meta.userId) return
        if (presence.isConnected(to)) {
          presence.sendToUser(to, { type: 'typing', from: meta.userId, typing: !!frame.typing })
        }
        return
      }

      case 'read': {
        const peerId = Number(frame.peer)
        if (!peerId) return
        try { markConversationRead(meta.userId, peerId, Date.now()) } catch {}
        // Sync the reader's own other tabs.
        presence.sendToUser(meta.userId, { type: 'read', peer: peerId })
        // Tell the original sender their messages were read (✓✓). From the
        // sender's perspective `peer` is the reader (the other party).
        presence.sendToUser(peerId, { type: 'read-receipt', peer: meta.userId })
        return
      }

      case 'react': {
        // Toggle an emoji reaction on a message. Only the two parties of that
        // message may react; relay the change to both so chips update live.
        const messageId = Number(frame.messageId)
        const emoji = String(frame.emoji || '').slice(0, 16)
        if (!messageId || !emoji) return
        const target = getMessageById(messageId)
        if (!target) return
        if (target.senderUserId !== meta.userId && target.recipientUserId !== meta.userId) return
        const on = frame.on !== false
        setReaction(messageId, meta.userId, emoji, on)
        const reaction = {
          type: 'reaction', messageId, userId: meta.userId, emoji, on,
          senderUserId: target.senderUserId, recipientUserId: target.recipientUserId
        }
        presence.sendToUser(target.senderUserId, reaction)
        if (target.recipientUserId !== target.senderUserId) presence.sendToUser(target.recipientUserId, reaction)
        return
      }

      case 'message': {
        if (!allowSend(peer.id)) {
          send(peer, { type: 'error', message: 'rate limited' })
          return
        }
        const to = Number(frame.to)
        const body = (frame.body ?? '').toString().trim()
        if (!to || !body) return
        if (to === meta.userId) return // no self-DM

        // Recipient must be staff. If they're connected they already passed the
        // gate; otherwise verify (cached). Only reject on a definitive "no".
        if (!presence.isConnected(to)) {
          const staff = await isStaffUser(to, meta.token)
          if (staff === false) {
            send(peer, { type: 'error', message: 'recipient not allowed' })
            return
          }
        }

        // Optional structured payload (e.g. a shared ERP entity card). The human
        // `body` is still required above, so push text / search / fallbacks all work.
        const shareMeta = (frame.meta && typeof frame.meta === 'object') ? frame.meta : null

        const createdAt = Date.now()
        const trimmed = body.slice(0, 4000)
        const saved = insertMessage({
          senderUserId: meta.userId,
          recipientUserId: to,
          body: trimmed,
          createdAt,
          clientMsgId: frame.clientMsgId ?? null,
          meta: shareMeta
        })
        const msg = saved ?? {
          id: null,
          senderUserId: meta.userId,
          recipientUserId: to,
          body: trimmed,
          createdAt,
          readAt: null,
          clientMsgId: frame.clientMsgId ?? null,
          meta: shareMeta
        }

        const out = { type: 'message', message: msg }
        presence.sendToUser(to, out)          // recipient's tabs (live)
        presence.sendToUser(meta.userId, out) // sender's tabs (echo → reconcile optimistic)

        // Native FCM to ALL the recipient's devices, regardless of WS presence. Presence is per-USER,
        // so a 2nd device that's backgrounded/killed gets NOTHING from the live socket while another
        // device keeps the user "online" — the push is what reaches it. Foreground devices receive it
        // silently (no pushNotificationReceived handler → no tray; the live socket already showed it),
        // so there is no double-notify.
        sendFcmToUser(to, {
          title: frame.fromName || 'Neue Nachricht',
          body: trimmed,
          data: { senderUserId: String(meta.userId), url: `/chat?chat=${meta.userId}` },
        }).catch((e: any) => console.error('[ChatWS] FCM failed:', e?.message))

        // Web push (browsers/PWA) stays offline-only — an active browser tab already showed it live.
        if (!presence.isConnected(to)) {
          const payload = buildChatMessagePayload(frame.fromName || '', trimmed, meta.userId)
          sendPushToUser(to, payload).catch((e: any) =>
            console.error('[ChatWS] offline push failed:', e?.message))
        }
        return
      }

      default:
        return
    }
  },

  close(peer) {
    presence.removePeer(peer.id)
    sendTimes.delete(peer.id)
  },

  error(peer) {
    try { presence.removePeer(peer.id) } catch {}
    try { sendTimes.delete(peer.id) } catch {}
  }
})
