'use client'

/**
 * React hooks for mizan/channels
 *
 * Includes pub/sub channel hooks AND RPC hooks.
 */

import { useCallback, useEffect, useRef, useState } from 'react'
import { useChannelContext } from './context'
import { RPCError } from './connection'
import type {
    ConnectionStatus,
    ChannelSubscription,
    IncomingPayload,
} from './types'

export interface UseChannelOptions<TServerMessage> {
    /** Called when subscribed successfully */
    onSubscribed?: () => void

    /** Called when a message is received */
    onMessage?: (message: TServerMessage) => void

    /** Called on error */
    onError?: (error: string) => void

    /** Called when unsubscribed */
    onUnsubscribed?: () => void

    /** Maximum messages to keep in history (default: 100) */
    maxMessages?: number
}

/**
 * Subscribe to a channel and receive typed messages.
 *
 * @param channelName - The registered channel name
 * @param params - Channel parameters (if required)
 * @param options - Subscription options
 */
export function useChannel<
    TParams = undefined,
    TServerMessage = unknown,
    TReactMessage = unknown,
>(
    channelName: string,
    params?: TParams,
    options: UseChannelOptions<TServerMessage> = {},
): ChannelSubscription<TParams, TServerMessage, TReactMessage> {
    const { connection, status: connectionStatus } = useChannelContext()

    const [messages, setMessages] = useState<TServerMessage[]>([])
    const [subscribed, setSubscribed] = useState(false)

    const optionsRef = useRef(options)
    optionsRef.current = options

    const maxMessages = options.maxMessages ?? 100

    // Stable params reference for effect dependencies
    const paramsJson = JSON.stringify(params ?? {})
    const paramsRef = useRef(params)
    paramsRef.current = params

    // Subscribe on mount / when params change
    useEffect(() => {
        if (connectionStatus !== 'connected') {
            return
        }

        const currentParams = paramsRef.current ?? {}

        // Subscribe
        connection.send({
            action: 'subscribe',
            channel: channelName,
            params: currentParams as Record<string, unknown>,
        })

        // Handle incoming messages
        const unsubscribeMessages = connection.onMessage((payload: IncomingPayload) => {
            // Check for subscription confirmation
            if ('subscribed' in payload && payload.channel === channelName) {
                setSubscribed(true)
                optionsRef.current.onSubscribed?.()
                return
            }

            // Check for unsubscription confirmation
            if ('unsubscribed' in payload && payload.channel === channelName) {
                setSubscribed(false)
                optionsRef.current.onUnsubscribed?.()
                return
            }

            // Check for errors
            if ('error' in payload) {
                if (!payload.channel || payload.channel === channelName) {
                    optionsRef.current.onError?.(payload.error)
                }
                return
            }

            // Handle data messages
            if ('type' in payload && 'data' in payload) {
                const message = payload.data as TServerMessage
                setMessages(prev => {
                    const next = [...prev, message]
                    // Trim to max messages
                    if (next.length > maxMessages) {
                        return next.slice(-maxMessages)
                    }
                    return next
                })
                optionsRef.current.onMessage?.(message)
            }
        })

        // Cleanup: unsubscribe
        return () => {
            unsubscribeMessages()

            connection.send({
                action: 'unsubscribe',
                channel: channelName,
                params: currentParams as Record<string, unknown>,
            })
        }
    }, [connection, connectionStatus, channelName, paramsJson, maxMessages])

    // Send function
    const send = useCallback((message: TReactMessage) => {
        if (!subscribed) {
            console.warn(`[useChannel] Cannot send: not subscribed to ${channelName}`)
            return
        }

        connection.send({
            action: 'message',
            channel: channelName,
            params: (paramsRef.current ?? {}) as Record<string, unknown>,
            data: message,
        })
    }, [connection, channelName, subscribed])

    // Unsubscribe function
    const unsubscribe = useCallback(() => {
        connection.send({
            action: 'unsubscribe',
            channel: channelName,
            params: (paramsRef.current ?? {}) as Record<string, unknown>,
        })
    }, [connection, channelName])

    // Clear messages
    const clearMessages = useCallback(() => {
        setMessages([])
    }, [])

    // Derive status
    const status: ConnectionStatus = !subscribed
        ? connectionStatus === 'connected' ? 'connecting' : connectionStatus
        : 'connected'

    return {
        status,
        messages,
        send: send as ChannelSubscription<TParams, TServerMessage, TReactMessage>['send'],
        unsubscribe,
        clearMessages,
    }
}

/**
 * Get only the latest message from a channel (useful for presence, typing indicators)
 */
export function useChannelLatest<
    TParams = undefined,
    TServerMessage = unknown,
    TReactMessage = unknown,
>(
    channelName: string,
    params?: TParams,
    options: UseChannelOptions<TServerMessage> = {},
): Omit<ChannelSubscription<TParams, TServerMessage, TReactMessage>, 'messages'> & { latest: TServerMessage | null } {
    const [latest, setLatest] = useState<TServerMessage | null>(null)

    const channel = useChannel<TParams, TServerMessage, TReactMessage>(
        channelName,
        params,
        {
            ...options,
            onMessage: (msg) => {
                setLatest(msg)
                options.onMessage?.(msg)
            },
            maxMessages: 1,
        },
    )

    // Explicitly exclude messages to match the documented API
    const { messages: _, ...rest } = channel

    return {
        ...rest,
        latest,
    }
}


// =============================================================================
// RPC Hooks
// =============================================================================

export interface RPCClient {
    /**
     * Call a server function.
     *
     * @param fn - Function name
     * @param args - Function arguments
     * @returns Promise resolving to function output
     * @throws RPCError on failure
     */
    call<TInput, TOutput>(fn: string, args: TInput): Promise<TOutput>

    /** Connection status */
    status: ConnectionStatus
}

/**
 * Get an RPC client for calling server functions.
 *
 * Usage:
 *   const rpc = useRPC()
 *   const result = await rpc.call('update_profile', { name: 'New Name' })
 *
 * The generated code wraps this with typed functions:
 *   const { updateProfile } = useDjango()
 *   const result = await updateProfile({ name: 'New Name' })
 */
export function useRPC(): RPCClient {
    const { connection, status } = useChannelContext()

    const call = useCallback(<TInput, TOutput>(fn: string, args: TInput): Promise<TOutput> => {
        return connection.rpc<TInput, TOutput>(fn, args)
    }, [connection])

    return {
        call,
        status,
    }
}

// Re-export RPCError for convenience
export { RPCError }
