import { safeVideoPlay } from '@/lib/media/safeVideoPlay'
import { resolveWhepPlaybackUrl } from '@/lib/streams/cloudflareStreamUrls'

export type WhepPlaybackHandle = {
    /** 重連間可能為 null，請勿假設一定存在 */
    get peerConnection(): RTCPeerConnection | null
    get mediaStream(): MediaStream
    stop: () => Promise<void>
    reconnect: () => Promise<void>
}

export type StartWhepPlaybackOptions = {
    videoElement: HTMLVideoElement
    /** stream_token 或 webRTC/play URL */
    src: string
    /** 直接指定 WHEP URL 時優先使用 */
    whepUrl?: string | null
    onStream?: (stream: MediaStream) => void
    onFailed?: (error: unknown) => void
    /** ICE / 連線中斷（推流結束、網路斷線等） */
    onConnectionLost?: () => void
}

function waitForIceGathering(pc: RTCPeerConnection, timeoutMs = 15_000): Promise<void> {
    if (pc.iceGatheringState === 'complete') return Promise.resolve()

    return new Promise((resolve) => {
        const timer = window.setTimeout(() => {
            cleanup()
            resolve()
        }, timeoutMs)

        const onChange = () => {
            if (pc.iceGatheringState === 'complete') {
                cleanup()
                resolve()
            }
        }

        const cleanup = () => {
            window.clearTimeout(timer)
            pc.removeEventListener('icegatheringstatechange', onChange)
        }

        pc.addEventListener('icegatheringstatechange', onChange)
    })
}

function addTrackToStream(mediaStream: MediaStream, track: MediaStreamTrack) {
    const exists = mediaStream.getTracks().some((t) => t.id === track.id)
    if (exists) return
    track.enabled = true
    mediaStream.addTrack(track)
}

/** 避免 ICE 短暫 disconnected 與 track.onended 重複觸發 */
function createConnectionLostNotifier(onConnectionLost?: () => void) {
    let disconnectedTimer: ReturnType<typeof setTimeout> | null = null
    let cooldownUntil = 0

    const clearDisconnectedTimer = () => {
        if (disconnectedTimer != null) {
            clearTimeout(disconnectedTimer)
            disconnectedTimer = null
        }
    }

    const notify = () => {
        if (!onConnectionLost) return
        const now = Date.now()
        if (now < cooldownUntil) return
        cooldownUntil = now + 1500
        onConnectionLost()
    }

    const attachPeerConnection = (pc: RTCPeerConnection) => {
        pc.onconnectionstatechange = () => {
            const state = pc.connectionState
            if (state === 'connected' || state === 'connecting') {
                clearDisconnectedTimer()
                return
            }
            if (state === 'failed' || state === 'closed') {
                clearDisconnectedTimer()
                notify()
                return
            }
            if (state === 'disconnected' && disconnectedTimer == null) {
                disconnectedTimer = setTimeout(() => {
                    disconnectedTimer = null
                    if (
                        pc.connectionState === 'disconnected' ||
                        pc.connectionState === 'failed' ||
                        pc.connectionState === 'closed'
                    ) {
                        notify()
                    }
                }, 1_500)
            }
        }
    }

    const onTrackEnded = () => {
        clearDisconnectedTimer()
        notify()
    }

    return { attachPeerConnection, onTrackEnded, dispose: clearDisconnectedTimer }
}

async function connectCloudflareWhep(
    endpoint: string,
    mediaStream: MediaStream,
    videoElement: HTMLVideoElement,
    onStream?: (stream: MediaStream) => void,
    onConnectionLost?: () => void
): Promise<{ pc: RTCPeerConnection; resourceUrl: string | null; disposeConnectionHandlers: () => void }> {
    const pc = new RTCPeerConnection({
        bundlePolicy: 'max-bundle',
    })
    const connectionLost = createConnectionLostNotifier(onConnectionLost)
    connectionLost.attachPeerConnection(pc)

    videoElement.srcObject = mediaStream

    pc.ontrack = (event) => {
        const track = event.track
        if (!track) return
        addTrackToStream(mediaStream, track)
        onStream?.(mediaStream)
        track.onended = () => {
            connectionLost.onTrackEnded()
        }
    }

    pc.addTransceiver('video', { direction: 'recvonly' })
    pc.addTransceiver('audio', { direction: 'recvonly' })

    const offer = await pc.createOffer()
    await pc.setLocalDescription(offer)
    await waitForIceGathering(pc)

    const response = await fetch(endpoint, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/sdp',
            Accept: 'application/sdp',
        },
        body: pc.localDescription?.sdp ?? '',
    })

    if (!response.ok) {
        try {
            pc.close()
        } catch {
            // ignore
        }
        throw new Error(`WHEP playback failed (${response.status})`)
    }

    const answerSdp = await response.text()
    await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp })

    let resourceUrl: string | null = null
    const location = response.headers.get('Location')
    if (location) {
        try {
            resourceUrl = new URL(location, endpoint).href
        } catch {
            resourceUrl = location
        }
    }

    onStream?.(mediaStream)
    return { pc, resourceUrl, disposeConnectionHandlers: connectionLost.dispose }
}

function clearMediaStream(mediaStream: MediaStream) {
    for (const track of mediaStream.getTracks()) {
        try {
            mediaStream.removeTrack(track)
            track.stop()
        } catch {
            // ignore
        }
    }
}

/**
 * 連線 Cloudflare Stream WHEP（webRTC/play），同時接收音訊與視訊軌。
 */
export async function startWhepPlayback(options: StartWhepPlaybackOptions): Promise<WhepPlaybackHandle> {
    const explicitWhep = String(options.whepUrl ?? '').trim()
    const endpoint = resolveWhepPlaybackUrl(explicitWhep || options.src)
    if (!endpoint) {
        throw new Error('Invalid Cloudflare WHEP playback URL')
    }

    let mediaStream = new MediaStream()
    options.videoElement.srcObject = mediaStream

    let pc: RTCPeerConnection | null = null
    let resourceUrl: string | null = null
    let disposeConnectionHandlers: (() => void) | null = null
    let stopped = false

    const teardownPeer = () => {
        disposeConnectionHandlers?.()
        disposeConnectionHandlers = null
        if (pc) {
            try {
                pc.close()
            } catch {
                // ignore
            }
            pc = null
        }
    }

    const deleteResource = async () => {
        if (!resourceUrl) return
        const url = resourceUrl
        resourceUrl = null
        try {
            await fetch(url, { method: 'DELETE' })
        } catch {
            // ignore
        }
    }

    const connect = async () => {
        if (stopped) return

        await deleteResource()
        teardownPeer()
        clearMediaStream(mediaStream)

        mediaStream = new MediaStream()
        options.videoElement.srcObject = mediaStream

        try {
            const session = await connectCloudflareWhep(
                endpoint,
                mediaStream,
                options.videoElement,
                options.onStream,
                options.onConnectionLost
            )
            pc = session.pc
            resourceUrl = session.resourceUrl
            disposeConnectionHandlers = session.disposeConnectionHandlers
        } catch (err) {
            teardownPeer()
            clearMediaStream(mediaStream)
            options.onFailed?.(err)
            throw err
        }
    }

    await connect()

    if (!pc) {
        throw new Error('WHEP peer connection was not established')
    }

    const handle: WhepPlaybackHandle = {
        get peerConnection() {
            return pc
        },
        get mediaStream() {
            return mediaStream
        },
        stop: async () => {
            stopped = true
            clearMediaStream(mediaStream)
            teardownPeer()
            await deleteResource()
            try {
                options.videoElement.srcObject = null
            } catch {
                // ignore
            }
        },
        reconnect: async () => {
            if (stopped) return
            try {
                options.videoElement.pause()
            } catch {
                // ignore
            }
            await connect()
            safeVideoPlay(options.videoElement)
        },
    }

    return handle
}
