/**
 * WebSocket connection manager for mizan/channels
 *
 * Supports both pub/sub channels AND RPC calls over the same connection.
 */

import type {
    ConnectionStatus,
    OutgoingMessage,
    IncomingPayload,
    SubscribeOptions,
} from './types'

type MessageHandler = (payload: IncomingPayload) => void
type StatusHandler = (status: ConnectionStatus) => void

/** RPC request message */
export interface RPCRequest<T = unknown> {
    action: 'rpc'
    id: string
    fn: string
    args: T
}

/** RPC response - success */
export interface RPCSuccessResponse<T = unknown> {
    id: string
    ok: true
    data: T
}

/** RPC response - error */
export interface RPCErrorResponse {
    id: string
    ok: false
    error: {
        code: string
        message: string
        details?: Record<string, unknown>
    }
}

export type RPCResponse<T = unknown> = RPCSuccessResponse<T> | RPCErrorResponse

/** RPC error thrown on failure */
export class RPCError extends Error {
    code: string
    details?: Record<string, unknown>

    constructor(code: string, message: string, details?: Record<string, unknown>) {
        super(message)
        this.name = 'RPCError'
        this.code = code
        this.details = details
    }
}

export interface ChannelConnectionOptions {
    /** WebSocket URL (default: /ws/) */
    url?: string

    /** Reconnect on disconnect (default: true) */
    reconnect?: boolean

    /** Reconnection delay in ms (default: 1000) */
    reconnectDelay?: number

    /** Maximum reconnection attempts (default: 10) */
    maxReconnectAttempts?: number
}

export class ChannelConnection {
    private ws: WebSocket | null = null
    private url: string
    private reconnect: boolean
    private reconnectDelay: number
    private maxReconnectAttempts: number
    private reconnectAttempts = 0
    private reconnectTimer: ReturnType<typeof setTimeout> | null = null

    private messageHandlers: Set<MessageHandler> = new Set()
    private statusHandlers: Set<StatusHandler> = new Set()

    private _status: ConnectionStatus = 'disconnected'
    private pendingMessages: OutgoingMessage[] = []

    // RPC state
    private rpcIdCounter = 0
    private pendingRPCs: Map<string, {
        resolve: (data: unknown) => void
        reject: (error: RPCError) => void
    }> = new Map()

    constructor(options: ChannelConnectionOptions = {}) {
        // Build WebSocket URL
        const baseUrl = options.url || '/ws/'
        if (typeof window !== 'undefined') {
            const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
            this.url = baseUrl.startsWith('ws')
                ? baseUrl
                : `${protocol}//${window.location.host}${baseUrl}`
        } else {
            this.url = baseUrl
        }

        this.reconnect = options.reconnect ?? true
        this.reconnectDelay = options.reconnectDelay ?? 1000
        this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10
    }

    get status(): ConnectionStatus {
        return this._status
    }

    private setStatus(status: ConnectionStatus) {
        this._status = status
        this.statusHandlers.forEach(handler => handler(status))
    }

    connect(): void {
        if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) {
            return
        }

        this.setStatus('connecting')

        try {
            this.ws = new WebSocket(this.url)

            this.ws.onopen = () => {
                this.reconnectAttempts = 0
                this.setStatus('connected')

                // Send any pending messages
                this.pendingMessages.forEach(msg => this.send(msg))
                this.pendingMessages = []
            }

            this.ws.onclose = (event) => {
                this.setStatus('disconnected')

                // Attempt reconnection if enabled
                if (this.reconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
                    this.scheduleReconnect()
                }
            }

            this.ws.onerror = () => {
                // WebSocket errors don't provide useful details (browser security)
                // The onclose handler will fire next and trigger reconnection
                console.warn('[ChannelConnection] WebSocket error (will reconnect)')
            }

            this.ws.onmessage = (event) => {
                try {
                    const payload = JSON.parse(event.data)

                    // Check if this is an RPC response (has 'id' and 'ok' fields)
                    if ('id' in payload && 'ok' in payload) {
                        this.handleRPCResponse(payload as RPCResponse)
                        return
                    }

                    // Otherwise, it's a channel message
                    this.messageHandlers.forEach(handler => handler(payload as IncomingPayload))
                } catch (e) {
                    console.error('[ChannelConnection] Failed to parse message:', e)
                }
            }
        } catch (error) {
            console.error('[ChannelConnection] Failed to connect:', error)
            this.setStatus('disconnected')

            if (this.reconnect) {
                this.scheduleReconnect()
            }
        }
    }

    disconnect(): void {
        this.reconnect = false
        this.clearReconnectTimer()

        if (this.ws) {
            this.ws.close()
            this.ws = null
        }

        this.setStatus('disconnected')
    }

    send(message: OutgoingMessage): void {
        if (this.ws?.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(message))
        } else {
            // Queue message to send when connected
            this.pendingMessages.push(message)

            // Ensure we're trying to connect
            if (this._status === 'disconnected') {
                this.connect()
            }
        }
    }

    onMessage(handler: MessageHandler): () => void {
        this.messageHandlers.add(handler)
        return () => this.messageHandlers.delete(handler)
    }

    onStatusChange(handler: StatusHandler): () => void {
        this.statusHandlers.add(handler)
        return () => this.statusHandlers.delete(handler)
    }

    private scheduleReconnect(): void {
        this.clearReconnectTimer()

        const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts)
        this.reconnectAttempts++

        console.log(`[ChannelConnection] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`)

        this.reconnectTimer = setTimeout(() => {
            this.connect()
        }, delay)
    }

    private clearReconnectTimer(): void {
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer)
            this.reconnectTimer = null
        }
    }

    // =========================================================================
    // RPC Methods
    // =========================================================================

    /**
     * Call a server function via RPC.
     *
     * @param fn - Function name (as registered on backend)
     * @param args - Function arguments
     * @returns Promise resolving to function output
     * @throws RPCError on failure
     */
    rpc<TInput, TOutput>(fn: string, args: TInput): Promise<TOutput> {
        return new Promise((resolve, reject) => {
            const id = `rpc_${++this.rpcIdCounter}_${Date.now()}`

            // Store pending RPC
            this.pendingRPCs.set(id, {
                resolve: resolve as (data: unknown) => void,
                reject,
            })

            // Send RPC request
            const request: RPCRequest<TInput> = {
                action: 'rpc',
                id,
                fn,
                args,
            }

            this.send(request as unknown as OutgoingMessage)
        })
    }

    private handleRPCResponse(response: RPCResponse): void {
        const pending = this.pendingRPCs.get(response.id)
        if (!pending) {
            console.warn(`[ChannelConnection] Received RPC response for unknown id: ${response.id}`)
            return
        }

        this.pendingRPCs.delete(response.id)

        if (response.ok) {
            pending.resolve(response.data)
        } else {
            pending.reject(new RPCError(
                response.error.code,
                response.error.message,
                response.error.details,
            ))
        }
    }
}

// Singleton connection instance
let defaultConnection: ChannelConnection | null = null

export function getDefaultConnection(options?: ChannelConnectionOptions): ChannelConnection {
    if (!defaultConnection) {
        defaultConnection = new ChannelConnection(options)
    }
    return defaultConnection
}
