// useWebRTC.ts
import { useState, useEffect, useRef, useCallback } from 'react'

export type CallState = 'idle' | 'connecting' | 'connected' | 'failed' | 'ended'

interface UseWebRTCOptions {
	callId: string
	wsUrl: string
	onHangup?: () => void
}

interface UseWebRTCReturn {
	callState: CallState
	localStream: MediaStream | null
	remoteStream: MediaStream | null
	isMuted: boolean
	isCameraOff: boolean
	toggleMute: () => void
	toggleCamera: () => void
	hangup: () => void
	errorMessage: string | null
}

const ICE_SERVERS: RTCIceServer[] = [
	{ urls: 'stun:stun.l.google.com:19302' },
	{ urls: 'stun:stun1.l.google.com:19302' },
	{ urls: 'stun:stun2.l.google.com:19302' },
	{ urls: 'stun:stun3.l.google.com:19302' },
	{ urls: 'stun:stun4.l.google.com:19302' },
]

export function useWebRTC({ callId, wsUrl, onHangup }: UseWebRTCOptions): UseWebRTCReturn {
	const [callState, setCallState] = useState<CallState>('idle')
	const [localStream, setLocalStream] = useState<MediaStream | null>(null)
	const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null)
	const [isMuted, setIsMuted] = useState(false)
	const [isCameraOff, setIsCameraOff] = useState(false)
	const [errorMessage, setErrorMessage] = useState<string | null>(null)

	// useRef でPCとWSを保持（非同期ハンドラ内でも最新値を参照できる）
	const pcRef = useRef<RTCPeerConnection | null>(null)
	const wsRef = useRef<WebSocket | null>(null)
	const localStreamRef = useRef<MediaStream | null>(null)
	// remoteDescriptionが設定される前に届いたICE候補をバッファリング
	const pendingCandidatesRef = useRef<RTCIceCandidateInit[]>([])
	const callStateRef = useRef<CallState>('idle')

	const updateCallState = useCallback((state: CallState) => {
		callStateRef.current = state
		setCallState(state)
	}, [])

	const cleanup = useCallback(() => {
		localStreamRef.current?.getTracks().forEach(track => track.stop())
		localStreamRef.current = null
		setLocalStream(null)
		if (pcRef.current) {
			pcRef.current.close()
			pcRef.current = null
		}
		if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
			wsRef.current.close()
		}
		wsRef.current = null
		pendingCandidatesRef.current = []
	}, [])

	useEffect(() => {
		if (!callId || !wsUrl) return

		updateCallState('idle')
		const socket = new WebSocket(wsUrl)
		wsRef.current = socket

		socket.onopen = () => {
			console.log('WebSocket connected')
			navigator.mediaDevices.getUserMedia({ video: true, audio: true })
				.then(stream => {
					localStreamRef.current = stream
					setLocalStream(stream)

					const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS })
					pcRef.current = pc

					stream.getTracks().forEach(track => pc.addTrack(track, stream))

					pc.ontrack = (event) => {
						if (event.streams?.[0]) setRemoteStream(event.streams[0])
					}

					pc.oniceconnectionstatechange = () => {
						console.log('ICE connection state:', pc.iceConnectionState)
						if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
							updateCallState('connected')
						} else if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') {
							updateCallState('failed')
							setErrorMessage('接続が切断されました')
						}
					}

					// Trickle ICE: 候補が見つかる都度送信
					pc.onicecandidate = (event) => {
						if (socket.readyState !== WebSocket.OPEN) return
						socket.send(JSON.stringify({
							type: 'ice',
							callId,
							candidate: event.candidate ? event.candidate.toJSON() : null,
						}))
					}

					pc.createOffer()
						.then(offer => pc.setLocalDescription(offer))
						.then(() => {
							if (socket.readyState !== WebSocket.OPEN || !pc.localDescription) return
							socket.send(JSON.stringify({ type: 'offer', callId, sdp: pc.localDescription.sdp }))
							updateCallState('connecting')
						})
						.catch(err => {
							console.error('SDP offer error:', err)
							updateCallState('failed')
							setErrorMessage('接続の開始に失敗しました')
						})
				})
				.catch(err => {
					console.error('getUserMedia error:', err)
					updateCallState('failed')
					setErrorMessage('カメラ・マイクへのアクセスが拒否されました')
				})
		}

		socket.onmessage = async (event: MessageEvent) => {
			let msg: { type: string; sdp?: string; candidate?: RTCIceCandidateInit | null; message?: string }
			try { msg = JSON.parse(event.data) } catch { return }
			console.log('WebSocket message:', msg.type)

			const pc = pcRef.current
			switch (msg.type) {
				case 'answer':
					if (!pc || !msg.sdp) break
					await pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp }).catch(console.error)
					for (const c of pendingCandidatesRef.current) {
						await pc.addIceCandidate(c).catch(console.error)
					}
					pendingCandidatesRef.current = []
					break
				case 'ice':
					if (!pc || msg.candidate == null) break
					if (pc.remoteDescription) {
						await pc.addIceCandidate(msg.candidate).catch(console.error)
					} else {
						pendingCandidatesRef.current.push(msg.candidate)
					}
					break
				case 'hangup':
					cleanup()
					updateCallState('ended')
					onHangup?.()
					break
				case 'error':
					updateCallState('failed')
					setErrorMessage(msg.message ?? '接続エラーが発生しました')
					break
			}
		}

		socket.onerror = () => {
			if (callStateRef.current !== 'ended') {
				updateCallState('failed')
				setErrorMessage('通信エラーが発生しました')
			}
		}

		socket.onclose = () => {
			if (callStateRef.current !== 'ended' && callStateRef.current !== 'failed') {
				updateCallState('failed')
				setErrorMessage('接続が切断されました')
			}
		}

		return () => { cleanup() }
	// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [wsUrl, callId])

	const hangup = useCallback(() => {
		if (wsRef.current?.readyState === WebSocket.OPEN) {
			wsRef.current.send(JSON.stringify({ type: 'hangup', callId }))
		}
		cleanup()
		updateCallState('ended')
	}, [callId, cleanup, updateCallState])

	const toggleMute = useCallback(() => {
		const track = localStreamRef.current?.getAudioTracks()[0]
		if (!track) return
		track.enabled = !track.enabled
		setIsMuted(!track.enabled)
	}, [])

	const toggleCamera = useCallback(() => {
		const track = localStreamRef.current?.getVideoTracks()[0]
		if (!track) return
		track.enabled = !track.enabled
		setIsCameraOff(!track.enabled)
	}, [])

	return { callState, localStream, remoteStream, isMuted, isCameraOff, toggleMute, toggleCamera, hangup, errorMessage }
}
