'use client'

import {
    createContext,
    useCallback,
    useContext,
    useEffect,
    useRef,
    useState,
    type ReactNode,
} from 'react'
import type { UserNotificationWsPayload } from '@/lib/notifications/userNotificationTypes'
import {
    noticeWsRequest,
    rejectAllNoticeWsRpc,
    settleNoticeWsRpc,
} from '@/lib/notices/noticeWsRpc'
import {
    buildNoticeWsUrl,
    dispatchUserNotificationsList,
    dispatchUserNotificationsUpdated,
    parseNoticeWsMessage,
    sendNoticeSubscribe,
    SESSION_CHANGED_EVENT,
    type UserNotificationListItem,
} from '@/lib/notices/wsNotice'

type NoticeWsContextValue = {
    userId: string | null
    wsReady: boolean
    listNotifications: () => Promise<UserNotificationListItem[]>
    deleteNotification: (notificationId: string) => Promise<boolean>
}

const NoticeWsContext = createContext<NoticeWsContextValue>({
    userId: null,
    wsReady: false,
    listNotifications: async () => [],
    deleteNotification: async () => false,
})

export function NoticeWsProvider({
    children,
    initialUserId,
}: {
    children: ReactNode
    /** 伺服器已解析的 userId；`null` 表示未登入（略過首次 API 請求） */
    initialUserId?: string | null
}) {
    const [userId, setUserId] = useState<string | null>(() => {
        if (initialUserId === undefined) return null
        const id = initialUserId?.trim()
        return id || null
    })
    const [wsReady, setWsReady] = useState(false)
    const wsRef = useRef<WebSocket | null>(null)
    const reconnectAttemptsRef = useRef(0)
    const reconnectTimeoutRef = useRef<number | null>(null)
    const cancelledRef = useRef(false)

    const loadUser = useCallback(async () => {
        try {
            const res = await fetch('/api/profile/user-info', { credentials: 'include' })
            if (res.status === 401) {
                setUserId(null)
                return
            }
            const data = await res.json()
            const id =
                data?.ok && data?.id != null
                    ? String(data.id).trim()
                    : data?.ok && data?.user?.id != null
                      ? String(data.user.id).trim()
                      : ''
            if (id) {
                setUserId(id)
            } else {
                setUserId(null)
            }
        } catch {
            setUserId(null)
        }
    }, [])

    useEffect(() => {
        cancelledRef.current = false
        if (initialUserId === undefined) {
            void loadUser()
        }
        return () => {
            cancelledRef.current = true
        }
    }, [loadUser, initialUserId])

    useEffect(() => {
        const onSessionChange = () => {
            void loadUser()
        }
        window.addEventListener(SESSION_CHANGED_EVENT, onSessionChange)
        return () => window.removeEventListener(SESSION_CHANGED_EVENT, onSessionChange)
    }, [loadUser])

    const handleNotification = useCallback((payload: UserNotificationWsPayload) => {
        dispatchUserNotificationsUpdated(payload)
    }, [])

    const handleWsMessage = useCallback(
        (raw: string) => {
            const msg = parseNoticeWsMessage(raw)
            if (!msg) return
            if (settleNoticeWsRpc(msg)) return

            if (msg.type === 'user_notification' && msg.data) {
                handleNotification(msg.data as UserNotificationWsPayload)
                return
            }

            if (msg.type === 'user_notifications_list' && msg.data?.notifications) {
                dispatchUserNotificationsList(msg.data.notifications)
                return
            }

            if (msg.type === 'notification_deleted' && msg.data?.notificationId) {
                dispatchUserNotificationsUpdated()
            }
        },
        [handleNotification]
    )

    const connect = useCallback(() => {
        if (cancelledRef.current || typeof window === 'undefined' || !userId) return

        try {
            const ws = new WebSocket(buildNoticeWsUrl())
            wsRef.current = ws
            setWsReady(false)

            ws.onopen = () => {
                reconnectAttemptsRef.current = 0
                setWsReady(true)
                sendNoticeSubscribe(ws, userId)
            }

            ws.onmessage = (event) => {
                try {
                    handleWsMessage(event.data as string)
                } catch {
                    // ignore malformed frames
                }
            }

            ws.onclose = () => {
                wsRef.current = null
                setWsReady(false)
                rejectAllNoticeWsRpc('WebSocket closed')
                if (cancelledRef.current || !userId) return
                const delay = Math.min(30_000, 1000 * 2 ** reconnectAttemptsRef.current)
                reconnectAttemptsRef.current += 1
                reconnectTimeoutRef.current = window.setTimeout(connect, delay)
            }

            ws.onerror = () => {
                ws.close()
            }
        } catch {
            setWsReady(false)
        }
    }, [userId, handleWsMessage])

    useEffect(() => {
        if (!userId) {
            if (wsRef.current) {
                wsRef.current.close()
                wsRef.current = null
            }
            setWsReady(false)
            rejectAllNoticeWsRpc('Signed out')
            return
        }

        cancelledRef.current = false
        connect()

        return () => {
            cancelledRef.current = true
            if (reconnectTimeoutRef.current != null) {
                window.clearTimeout(reconnectTimeoutRef.current)
                reconnectTimeoutRef.current = null
            }
            if (wsRef.current) {
                wsRef.current.close()
                wsRef.current = null
            }
            setWsReady(false)
            rejectAllNoticeWsRpc('WebSocket closed')
        }
    }, [userId, connect])

    const listNotifications = useCallback(async (): Promise<UserNotificationListItem[]> => {
        const ws = wsRef.current
        if (!ws || ws.readyState !== WebSocket.OPEN) {
            return []
        }
        const msg = await noticeWsRequest(ws, 'list_notifications', {})
        if (msg.type === 'user_notifications_list' && msg.data?.notifications) {
            return msg.data.notifications
        }
        return []
    }, [])

    const deleteNotification = useCallback(async (notificationId: string): Promise<boolean> => {
        const ws = wsRef.current
        const id = String(notificationId ?? '').trim()
        if (!ws || ws.readyState !== WebSocket.OPEN || !id) {
            return false
        }
        const msg = await noticeWsRequest(ws, 'delete_notification', { notificationId: id })
        return msg.type === 'notification_deleted' && Boolean(msg.data?.ok)
    }, [])

    return (
        <NoticeWsContext.Provider
            value={{ userId, wsReady, listNotifications, deleteNotification }}
        >
            {children}
        </NoticeWsContext.Provider>
    )
}

export function useNoticeWs() {
    return useContext(NoticeWsContext)
}
