import { ref, shallowRef, watch } from 'vue'
import {
  Room,
  RoomEvent,
  Track,
  ConnectionQuality,
  RemoteTrackPublication,
  VideoQuality,
  type Participant,
  type RemoteParticipant,
  type RemoteTrack,
} from 'livekit-client'
import type { ChatMessage, MeetingQuality } from '@/types/meeting'

// 브라우저가 접속할 LiveKit WebSocket URL (build arg/env 로 주입, 미설정 시 로컬 기본값)
const LIVEKIT_URL = import.meta.env.VITE_LIVEKIT_URL || 'ws://localhost:7880'

// 채팅 메시지 최대 길이(데이터 채널 패킷 과대 방지) — 입력/전송에서 공유한다.
export const MAX_CHAT_LENGTH = 2000

// 입장 옵션 — 프리조인에서 정한 초기 마이크/카메라 상태
export interface ConnectOptions {
  cam?: boolean
  mic?: boolean
}

// LiveKit ConnectionQuality → UI 막대 단계로 정규화
export function normalizeQuality(q: ConnectionQuality | undefined): MeetingQuality {
  switch (q) {
    case ConnectionQuality.Excellent:
    case ConnectionQuality.Good:
      return 'good'
    case ConnectionQuality.Poor:
      return 'medium'
    case ConnectionQuality.Lost:
      return 'poor'
    default:
      return 'good'
  }
}

// 참여자 metadata(JSON)에서 프로필 이미지 URL 파싱 — 토큰 발급 시 백엔드가 실어준다.
function parseMetaAvatar(metadata: string | undefined): string | null {
  if (!metadata) return null
  try {
    return (
      (JSON.parse(metadata) as { avatarUrl?: string | null }).avatarUrl ?? null
    )
  } catch {
    return null
  }
}

// 참여자 metadata 가 진행자(host)인지 — 서버 서명 토큰의 값이라 위조 불가.
function isHostMeta(metadata: string | undefined): boolean {
  if (!metadata) return false
  try {
    return (JSON.parse(metadata) as { role?: string }).role === 'host'
  } catch {
    return false
  }
}

// LiveKit 방 연결/생명주기 관리 Composable
// 미디어 전송 자체는 LiveKit SDK 가 담당하고, 여기서는 연결 상태와 참여자 목록만 반응형으로 노출한다.
export function useLiveKitRoom() {
  // Room 인스턴스는 깊은 반응형이 불필요(내부적으로 자체 이벤트 관리) → shallowRef
  const room = shallowRef<Room | null>(null)
  // 참여자 배열도 shallowRef — Participant 는 비공개 멤버가 있는 클래스라 깊은 언랩 시
  //   구조적 타입으로 바뀌어 타입 불일치가 난다. 매번 배열을 통째로 교체하므로 shallow 로 충분.
  const participants = shallowRef<Participant[]>([])
  const connected = ref(false)
  const connecting = ref(false)
  const micEnabled = ref(false)
  const camEnabled = ref(false)
  const errorMsg = ref<string | null>(null)
  // 크게 보기(stage)로 승격된 참여자 identity. 진행자가 데이터 채널로 브로드캐스트 → 전원 동기화.
  const featuredIdentity = ref<string | null>(null)
  // 채팅 메시지(인메모리) — 데이터 채널로 주고받음. 서버에 저장하지 않으므로 새로고침 시 사라진다.
  const messages = ref<ChatMessage[]>([])

  // 로컬 + 원격 참여자 목록 갱신. 마이크/발화/품질 등 참여자 상태 변동 시에도 호출해
  //   배열 참조를 새로 만들어(=동일 Participant 인스턴스 유지) 의존 computed 를 재평가시킨다.
  function refresh() {
    const r = room.value
    if (!r) {
      participants.value = []
      return
    }
    participants.value = [
      r.localParticipant,
      ...Array.from(r.remoteParticipants.values()),
    ]
  }

  // 스테이지(featured)로 보일 참여자 식별자 해석 — 명시 승격 > 진행자 > 첫 참여자.
  //   (MeetingRoom 의 레이아웃 featured 해석과 동일 규칙이라 화질=스테이지가 일치한다)
  function effectiveFeaturedId(): string | null {
    const r = room.value
    if (!r) return null
    const all = [
      r.localParticipant,
      ...Array.from(r.remoteParticipants.values()),
    ]
    if (
      featuredIdentity.value &&
      all.some((p) => p.identity === featuredIdentity.value)
    ) {
      return featuredIdentity.value
    }
    return (all.find((p) => isHostMeta(p.metadata)) ?? all[0])?.identity ?? null
  }

  // 수신 화질 적용 — 스테이지 유저만 HIGH, 나머지 원격 참여자는 모두 LOW 로 구독한다.
  //   각 클라이언트가 자기 시점에서 적용하므로 "남들이 보기에" 스테이지=고화질이 된다.
  //   스테이지 변경·참여자/트랙 변동 시마다 호출해 강등된 참여자도 확실히 LOW 로 내린다.
  //   (adaptiveStream 을 끈 상태라 setVideoQuality 가 곧 실효 화질 — 크기에 덮어써지지 않음)
  function applyVideoQuality() {
    const r = room.value
    if (!r) return
    const fid = effectiveFeaturedId()
    r.remoteParticipants.forEach((p) => {
      const pub = p.getTrackPublication(Track.Source.Camera)
      if (pub instanceof RemoteTrackPublication && pub.isSubscribed) {
        pub.setVideoQuality(
          p.identity === fid ? VideoQuality.HIGH : VideoQuality.LOW,
        )
      }
    })
  }

  // 참여자 변동(입장/퇴장/메타변경) 시 목록 갱신 + 화질 재적용(스테이지 폴백이 바뀔 수 있음)
  function onParticipantsChanged() {
    refresh()
    applyVideoQuality()
  }

  // 스테이지(featuredIdentity)가 바뀌면 즉시 화질 재적용 — 강등된 참여자를 LOW 로 내린다.
  watch(featuredIdentity, () => applyVideoQuality())

  // --- 원격 오디오는 화면 타일(페이지네이션)과 무관하게 항상 재생되어야 한다 ---
  // 타일에 attach 하면 현재 페이지에 없는 참여자(>50번째)는 무음이 되므로,
  //   숨은 컨테이너에 트랙별 audio 엘리먼트를 붙여 룸 레벨에서 전원 오디오를 재생한다.
  let audioContainer: HTMLElement | null = null
  const audioEls = new Map<string, HTMLMediaElement>()

  function attachRemoteAudio(track: RemoteTrack, pub: RemoteTrackPublication) {
    if (track.kind !== Track.Kind.Audio) return
    if (!audioContainer) {
      audioContainer = document.createElement('div')
      audioContainer.style.display = 'none'
      document.body.appendChild(audioContainer)
    }
    const el = track.attach() // 새 audio 엘리먼트 생성
    audioContainer.appendChild(el)
    audioEls.set(pub.trackSid, el)
  }
  function detachRemoteAudio(track: RemoteTrack, pub: RemoteTrackPublication) {
    if (track.kind !== Track.Kind.Audio) return
    track.detach()
    const el = audioEls.get(pub.trackSid)
    if (el) {
      el.remove()
      audioEls.delete(pub.trackSid)
    }
  }
  function cleanupAudio() {
    audioEls.forEach((el) => el.remove())
    audioEls.clear()
    audioContainer?.remove()
    audioContainer = null
  }

  // TrackSubscribed/Unsubscribed: 목록 갱신 + (오디오면) 룸 레벨 재생 관리
  function onTrackSubscribed(track: RemoteTrack, pub: RemoteTrackPublication) {
    refresh()
    attachRemoteAudio(track, pub)
    // 새로 구독된 영상에도 스테이지 여부에 맞는 화질을 적용한다(늦게 켠 카메라 등).
    applyVideoQuality()
  }
  function onTrackUnsubscribed(track: RemoteTrack, pub: RemoteTrackPublication) {
    refresh()
    detachRemoteAudio(track, pub)
  }

  function onDisconnected() {
    connected.value = false
    participants.value = []
    featuredIdentity.value = null
    messages.value = []
    cleanupAudio()
  }

  // 데이터 채널 수신 — 진행자의 승격(promote) + 채팅(chat) 메시지를 처리
  function onData(payload: Uint8Array, sender?: RemoteParticipant) {
    try {
      const msg = JSON.parse(new TextDecoder().decode(payload)) as {
        type?: string
        identity?: string
        text?: string
        ts?: number
      }
      if (msg.type === 'promote' && typeof msg.identity === 'string') {
        // 진행자가 보낸 승격만 적용 — 일반 참여자가 보낸 promote 는 무시(인가).
        if (isHostMeta(sender?.metadata)) featuredIdentity.value = msg.identity
      } else if (msg.type === 'chat' && typeof msg.text === 'string') {
        messages.value = [
          ...messages.value,
          {
            id: `${sender?.identity ?? 'remote'}-${msg.ts ?? performance.now()}`,
            senderId: sender?.identity ?? 'unknown',
            // 표시 이름은 서버 서명 토큰의 name/identity 만 신뢰(클라이언트가 보낸 name 은 사칭 위험이라 무시).
            senderName: sender?.name || sender?.identity || '참여자',
            senderAvatarUrl: parseMetaAvatar(sender?.metadata),
            text: msg.text,
            ts: msg.ts ?? Date.now(),
            self: false,
          },
        ]
      }
    } catch {
      // 알 수 없는 메시지는 무시
    }
  }

  // 특정 참여자를 크게 보기로 승격 — 진행자만 호출(UI에서 제한). 전원에게 브로드캐스트.
  async function promote(identity: string): Promise<void> {
    const r = room.value
    if (!r) return
    featuredIdentity.value = identity
    const data = new TextEncoder().encode(
      JSON.stringify({ type: 'promote', identity }),
    )
    await r.localParticipant.publishData(data, { reliable: true })
  }

  // 채팅 전송 — 데이터 채널로 브로드캐스트 + 본인 목록에 즉시 추가
  async function sendChat(text: string): Promise<void> {
    const r = room.value
    // 데이터 채널 패킷 권장 크기를 넘지 않도록 길이 제한(과도한 페이로드 방지).
    const body = text.trim().slice(0, MAX_CHAT_LENGTH)
    if (!r || !body) return
    const ts = Date.now()
    const me = r.localParticipant
    messages.value = [
      ...messages.value,
      {
        id: `${me.identity}-${ts}`,
        senderId: me.identity,
        senderName: me.name || me.identity,
        senderAvatarUrl: parseMetaAvatar(me.metadata),
        text: body,
        ts,
        self: true,
      },
    ]
    // name 은 보내지 않는다 — 수신 측은 서명된 sender.name 을 사용(사칭 방지).
    const data = new TextEncoder().encode(
      JSON.stringify({ type: 'chat', text: body, ts }),
    )
    await me.publishData(data, { reliable: true })
  }

  // 방 입장 — 토큰으로 연결 후 옵션에 맞춰 카메라/마이크 발행
  async function connect(token: string, opts: ConnectOptions = {}): Promise<void> {
    if (connecting.value || connected.value) return
    const wantCam = opts.cam ?? true
    const wantMic = opts.mic ?? true
    connecting.value = true
    errorMsg.value = null
    try {
      // adaptiveStream 은 끈다 — 화질을 "엘리먼트 크기"가 아니라 "스테이지 역할"로
      //   명시 제어하기 위함(스테이지=HIGH, 나머지=LOW 보장). 켜져 있으면 setVideoQuality 가
      //   타일 크기에 덮어써져(min) 스테이지가 HD 가 안 되거나 강등 반영이 비결정적이 된다.
      //   dynacast 는 유지 — 아무도 구독 안 하는 레이어는 발행자가 송출을 멈춘다.
      const r = new Room({ dynacast: true })
      // 참여자 목록(배열) 재생성이 필요한 이벤트만 구독한다.
      // 발화(ActiveSpeakers)·연결품질(ConnectionQuality)은 매우 잦게 발생하는데
      //   각 타일이 ParticipantEvent 로 자체 갱신하므로, 여기서 전체 배열을 재생성하면
      //   대규모(수십 명) 화면에서 불필요한 리렌더로 버벅임을 유발한다 → 제외.
      r.on(RoomEvent.ParticipantConnected, onParticipantsChanged)
        .on(RoomEvent.ParticipantDisconnected, onParticipantsChanged)
        .on(RoomEvent.TrackSubscribed, onTrackSubscribed)
        .on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
        .on(RoomEvent.TrackMuted, refresh)
        .on(RoomEvent.TrackUnmuted, refresh)
        .on(RoomEvent.LocalTrackPublished, refresh)
        .on(RoomEvent.LocalTrackUnpublished, refresh)
        .on(RoomEvent.ParticipantMetadataChanged, onParticipantsChanged)
        .on(RoomEvent.DataReceived, onData)
        .on(RoomEvent.Disconnected, onDisconnected)

      await r.connect(LIVEKIT_URL, token)
      room.value = r
      connected.value = true

      // 카메라/마이크 권한 요청 및 발행 (실패해도 연결은 유지)
      if (wantCam || wantMic) {
        try {
          if (wantCam && wantMic) {
            await r.localParticipant.enableCameraAndMicrophone()
          } else {
            if (wantMic) await r.localParticipant.setMicrophoneEnabled(true)
            if (wantCam) await r.localParticipant.setCameraEnabled(true)
          }
          camEnabled.value = wantCam
          micEnabled.value = wantMic
        } catch {
          errorMsg.value =
            '카메라/마이크를 사용할 수 없습니다. 권한을 확인해 주세요.'
        }
      } else {
        // 권한 거부 입장 — 영상·음성 없이 참관
        errorMsg.value =
          '카메라·마이크 없이 참여 중입니다. 아래 컨트롤에서 언제든 켤 수 있습니다.'
      }
      refresh()
    } catch (e) {
      // 연결 실패 원인을 콘솔에 남긴다(이전엔 errorMsg 로만 삼켜져 화면·콘솔 어디에도 안 보였다).
      console.error('[meeting] LiveKit 연결 실패:', e)
      errorMsg.value = e instanceof Error ? e.message : '연결에 실패했습니다.'
      await disconnect()
    } finally {
      connecting.value = false
    }
  }

  // 방 나가기
  async function disconnect(): Promise<void> {
    const r = room.value
    if (r) await r.disconnect()
    room.value = null
    connected.value = false
    camEnabled.value = false
    micEnabled.value = false
    participants.value = []
    featuredIdentity.value = null
    messages.value = []
    cleanupAudio()
  }

  async function toggleMic(): Promise<void> {
    const r = room.value
    if (!r) return
    const next = !micEnabled.value
    micEnabled.value = next
    try {
      await r.localParticipant.setMicrophoneEnabled(next)
    } catch {
      micEnabled.value = !next // 실패 시 상태 원복(표시와 실제 송출 불일치 방지)
    }
    refresh()
  }

  async function toggleCam(): Promise<void> {
    const r = room.value
    if (!r) return
    const next = !camEnabled.value
    camEnabled.value = next
    try {
      await r.localParticipant.setCameraEnabled(next)
    } catch {
      camEnabled.value = !next
    }
    refresh()
  }

  return {
    participants,
    connected,
    connecting,
    micEnabled,
    camEnabled,
    errorMsg,
    featuredIdentity,
    messages,
    connect,
    disconnect,
    toggleMic,
    toggleCam,
    promote,
    sendChat,
  }
}
