<template>
  <div class="app" :class="{ suspended: viewerSuspended }">
    <header class="app-header">
      <div>
        <p class="eyebrow">Music Jeopardy</p>
        <h1>Track the Beat, Claim the Points</h1>
      </div>
      <div v-if="step === 'game'" class="selector-pill">
        <span class="label">Selecting</span>
        <span class="value">{{ currentSelector?.name || '-' }}</span>
      </div>
      <div v-if="gameId" class="selector-pill session-pill">
        <span class="label">Game ID</span>
        <span class="value">{{ gameId }}</span>
      </div>
    </header>

    <main class="app-main">
      <section v-if="step === 'setup'" class="panel">
        <h2>Team Setup</h2>
        <p class="hint">Add teams, name them, and create their color gradients.</p>
        <div class="session-box">
          <h3>Live Session</h3>
          <p class="hint">
            Create a game ID as host, or join an existing game ID as viewer.
          </p>
          <div class="session-row">
            <input
              v-model.trim="gameIdInput"
              class="input"
              placeholder="Game ID"
              :disabled="sessionBusy || !!gameId"
            />
            <button class="ghost" @click="createSession()" :disabled="sessionBusy || !!gameId || !normalizedGameIdInput">
              Create
            </button>
            <button
              class="ghost"
              @click="joinSession"
              :disabled="sessionBusy || !!gameId || !normalizedGameIdInput"
            >
              Join
            </button>
            <button class="ghost" v-if="gameId" @click="leaveSession" :disabled="sessionBusy">
              Leave
            </button>
          </div>
          <p class="session-meta">
            <span>Status: {{ connectionLabel }}</span>
            <span v-if="syncError">Error: {{ syncError }}</span>
          </p>
        </div>

        <div class="teams-grid">
          <div v-for="team in teams" :key="team.id" class="team-card">
            <div class="team-card-header">
              <input
                v-model="team.name"
                class="input"
                placeholder="Team name"
                :disabled="!canControlGame"
              />
              <button class="ghost" @click="removeTeam(team.id)" :disabled="!canControlGame">Remove</button>
            </div>
            <div class="gradient-preview" :style="teamGradient(team)"></div>
            <div class="color-row">
              <label>
                Color A
                <input v-model="team.colorA" type="color" :disabled="!canControlGame" />
              </label>
              <label>
                Color B
                <input v-model="team.colorB" type="color" :disabled="!canControlGame" />
              </label>
            </div>
          </div>
          <button class="add-card" @click="addTeam" :disabled="!canControlGame">
            <span>+ Add Team</span>
          </button>
        </div>

        <div class="actions">
          <button class="primary" :disabled="!canProceedToSelect" @click="goToSelect">
            Next: Choose Game
          </button>
        </div>
      </section>

      <section v-if="step === 'select'" class="panel">
        <h2>Select a Game</h2>
        <p class="hint">
          Games are loaded from <code>public/Data</code>. Each subfolder becomes a
          playable game.
        </p>

        <div v-if="loadingGames" class="empty-state">
          <p>Loading games...</p>
        </div>

        <div v-else-if="loadError" class="empty-state">
          <p>Could not load games.</p>
          <p>{{ loadError }}</p>
        </div>

        <div v-else-if="games.length === 0" class="empty-state">
          <p>No games found yet.</p>
          <p>
            Add folders in <code>public/Data/&lt;GameName&gt;/&lt;Category&gt;</code>
            with <code>Songs</code> and <code>Answers</code> subfolders, then
            run <code>npm run generate:data</code>.
          </p>
        </div>

        <div class="games-grid" v-else>
          <button
            v-for="game in games"
            :key="game.name"
            class="game-card"
            @click="startGame(game)"
            :disabled="!canControlGame"
          >
            <h3>{{ game.name }}</h3>
            <p>{{ game.categories.length }} categories</p>
          </button>
        </div>

        <div class="actions">
          <button class="ghost" @click="step = 'setup'" :disabled="!canControlGame">Back</button>
        </div>
      </section>

      <section v-if="step === 'game'" class="game-layout">
        <aside class="scoreboard">
          <h2>Teams</h2>
          <div v-if="!canControlGame" class="viewer-team-select">
            <label for="viewer-team">Playing For</label>
            <select id="viewer-team" v-model="viewerTeamId" class="input">
              <option value="">Select team</option>
              <option v-for="team in teams" :key="`viewer-${team.id}`" :value="team.id">
                {{ team.name }}
              </option>
            </select>
          </div>
          <div class="team-list">
            <button
              v-for="team in teams"
              :key="team.id"
              class="team-score-card"
              :class="{
                active: team.id === currentSelectorId,
                viewerTeam: !canControlGame && !!viewerTeamId && team.id === viewerTeamId
              }"
              :style="teamGradient(team)"
              @click="awardPoints(team.id)"
              :disabled="!canAward || !canControlGame"
            >
              <div class="team-name">{{ team.name }}</div>
              <div class="team-score">{{ team.score }}</div>
              <div v-if="team.id === lastAwardedTeamId" class="award-tag">
                Last Award
              </div>
            </button>
          </div>
          <div class="controls">
            <button class="ghost" @click="resetGame" :disabled="!canControlGame">Reset Game</button>
          </div>
        </aside>

        <section class="board">
          <div v-if="isGuessSuspended && !!guessingTeamId" class="board-guessing-banner">
            {{ guessingTeamLabel }} is guessing
          </div>
          <div class="board-grid">
            <div
              v-for="(category, cIndex) in selectedGame?.categories || []"
              :key="category.name"
              class="category-column"
            >
              <div class="category-title">{{ category.name }}</div>
              <button
                v-for="(clue, qIndex) in category.clues"
                :key="clue.points"
                class="tile"
                :class="tileClass(cIndex, qIndex)"
                :style="tileStyle(cIndex, qIndex)"
                :disabled="tileDisabled(cIndex, qIndex) || !canControlGame"
                @click="handleTileClick(cIndex, qIndex)"
                @contextmenu.prevent="handleTileRightClick(cIndex, qIndex)"
              >
                <span v-if="tileStatus(cIndex, qIndex) === 'available'">
                  {{ clue.points }}
                </span>
                <span v-else-if="tileStatus(cIndex, qIndex) === 'playing'">
                  Playing
                </span>
                <span v-else-if="tileStatus(cIndex, qIndex) === 'paused'">
                  Paused
                </span>
                <span v-else-if="tileStatus(cIndex, qIndex) === 'guessed'">
                  Guessed
                </span>
                <span v-else-if="tileStatus(cIndex, qIndex) === 'won'">
                  Won
                </span>
                <span v-else>Skipped</span>
              </button>
            </div>
          </div>

          <div class="player-panel">
            <div class="player-header">
              <h3>Now Playing</h3>
              <p>
                Left click the active tile to pause/guess. Right click to resume
                or skip.
              </p>
            </div>
            <div
              class="player-body"
              :class="{
                playing: isPlaying && !isAnswerClip,
                answer: isPlaying && isAnswerClip,
                idle: !currentClipUrl
              }"
            >
              <div
                class="pulse-orb"
                :class="{
                  active: isPlaying,
                  playing: isPlaying && !isAnswerClip,
                  answer: isPlaying && isAnswerClip,
                  idle: !currentClipUrl
                }"
                :style="{
                  '--pulse': pulseLevel.toFixed(3),
                  '--ray': rayLevel.toFixed(3),
                  '--ray-hue': rayHue.toFixed(0)
                }"
              ></div>
              <div
                class="pulse-tentacles"
                :class="{
                  playing: isPlaying && !isAnswerClip,
                  answer: isPlaying && isAnswerClip,
                  idle: !currentClipUrl
                }"
                :style="{ '--pulse': pulseLevel.toFixed(3) }"
              >
                <span
                  v-for="(level, index) in tentacleLevels"
                  :key="index"
                  class="tentacle"
                  :style="{
                    '--level': level.toFixed(3),
                    '--index': index
                  }"
                ></span>
              </div>
              <audio
                ref="player"
                v-if="currentClipUrl"
                :src="currentClipUrl"
                class="hidden-audio"
                preload="auto"
                @play="handlePlayerPlay"
                @pause="handlePlayerPause"
                @ended="handlePlayerPause"
              ></audio>
              <div v-if="viewerGuessVisible || showEnableAudio" class="viewer-actions">
                <button v-if="showEnableAudio" class="primary enable-audio" @click="enableViewerAudio">
                  Tap To Enable Audio
                </button>
                <button class="primary viewer-guess" :disabled="!canViewerGuess" @click="requestGuessStop">
                  Stop Song And Guess
                </button>
              </div>
              <button
                v-if="canControlGame && getCurrentTileStatus() === 'paused' && !!guessingTeamId"
                class="primary host-reveal"
                @click="revealPausedGuess"
              >
                Reveal Answer
              </button>
            <div v-if="!currentClipUrl" class="player-empty"></div>
          </div>
          </div>
        </section>
      </section>

      <section v-if="step === 'end'" class="panel end-panel">
        <h2>Final Scores</h2>
        <div class="winner-banner">
          <span>Winner</span>
          <strong>{{ winnerNames }}</strong>
        </div>
        <div class="teams-grid">
          <div
            v-for="team in teams"
            :key="team.id"
            class="team-card"
            :class="{ winner: winnerIds.includes(team.id) }"
          >
            <div class="gradient-preview" :style="teamGradient(team)"></div>
            <h3>{{ team.name }}</h3>
            <p class="score">{{ team.score }} pts</p>
          </div>
        </div>
        <div class="actions">
          <button class="primary" @click="step = 'setup'">
            Start New Game
          </button>
        </div>
      </section>
    </main>
    <div v-if="viewerSuspended" class="guess-overlay">
      <p>{{ guessingTeamLabel }} is guessing</p>
    </div>
  </div>
</template>

<script lang="ts">
import { nextTick } from 'vue'
import { loadGameData, type Game } from './dataLoader'

type Step = 'setup' | 'select' | 'game' | 'end'

type Team = {
  id: string
  name: string
  colorA: string
  colorB: string
  score: number
}

type TileEntry = {
  status: 'available' | 'playing' | 'paused' | 'guessed' | 'won' | 'void'
  lastTeamId: string | null
}

type TileMap = Record<string, TileEntry>

type RealtimeState = {
  step: Step
  teams: Team[]
  selectedGameName: string | null
  tiles: TileMap
  currentTileKey: string | null
  currentClipUrl: string
  currentSelectorId: string | null
  lastAwardedTeamId: string | null
  isAnswerClip: boolean
  guessingTeamId: string | null
}

type RealtimeMessage = {
  type: 'session:init' | 'state:update' | 'guess:request'
  state?: RealtimeState | null
  teamId?: string
}

const REALTIME_BASE = '/realtime'

const makeTeam = (index: number): Team => ({
  id: `team-${Date.now()}-${index}`,
  name: `Team ${index + 1}`,
  colorA: '#3dd6ff',
  colorB: '#fbd72b',
  score: 0
})

const makeClientId = () => {
  const segment = Math.random().toString(36).slice(2, 10)
  return `client-${Date.now()}-${segment}`
}

const cloneTiles = (tiles: TileMap): TileMap => JSON.parse(JSON.stringify(tiles)) as TileMap

export default {
  data() {
    return {
      step: 'setup' as Step,
      teams: [makeTeam(0), makeTeam(1)] as Team[],
      games: [] as Game[],
      loadingGames: true,
      loadError: '',
      selectedGame: null as Game | null,
      tiles: {} as TileMap,
      currentTileKey: null as string | null,
      currentClipUrl: '',
      currentSelectorId: null as string | null,
      lastAwardedTeamId: null as string | null,
      isPlaying: false,
      pulseLevel: 0,
      audioContext: null as AudioContext | null,
      analyser: null as AnalyserNode | null,
      analyserData: null as Uint8Array | null,
      frequencyData: null as Uint8Array | null,
      rafId: 0,
      mediaSource: null as MediaElementAudioSourceNode | null,
      mediaElement: null as HTMLAudioElement | null,
      rayLevel: 0,
      rayHue: 200,
      isAnswerClip: false,
      tentacleLevels: Array.from({ length: 12 }, () => 0),
      gameId: '',
      gameIdInput: '',
      sessionBusy: false,
      socket: null as WebSocket | null,
      socketConnected: false,
      isHost: false,
      clientId: makeClientId(),
      syncError: '',
      isApplyingRemote: false,
      queuedRemoteState: null as RealtimeState | null,
      syncTimer: 0,
      audioUnlocked: true,
      latestRemoteState: null as RealtimeState | null,
      viewerTeamId: '',
      guessingTeamId: null as string | null,
      pauseTransitionLockUntil: 0
    }
  },
  async mounted() {
    try {
      this.loadingGames = true
      this.games = await loadGameData()
      if (this.queuedRemoteState) {
        await this.applyRemoteState(this.queuedRemoteState)
        this.queuedRemoteState = null
      }
      const fromUrl = new URL(window.location.href).searchParams.get('game')
      if (fromUrl) {
        this.gameIdInput = fromUrl.toUpperCase()
        await this.joinSession()
      }
    } catch (error) {
      this.loadError = error instanceof Error ? error.message : 'Failed to load games.'
    } finally {
      this.loadingGames = false
    }
  },
  computed: {
    normalizedGameIdInput() {
      return this.normalizeGameId(this.gameIdInput)
    },
    canProceed() {
      return this.teams.length > 0 && this.teams.every((team) => team.name.trim())
    },
    canProceedToSelect() {
      return this.canProceed && this.canControlGame && !!this.normalizedGameIdInput && !this.sessionBusy
    },
    showEnableAudio() {
      return !this.canControlGame && !!this.currentClipUrl && !this.audioUnlocked
    },
    canViewerGuess() {
      return (
        !this.canControlGame &&
        !!this.viewerTeamId &&
        this.getCurrentTileStatus() === 'playing'
      )
    },
    viewerGuessVisible() {
      return !this.canControlGame && this.getCurrentTileStatus() === 'playing'
    },
    canControlGame() {
      return !this.gameId || this.isHost
    },
    isGuessSuspended() {
      return this.step === 'game' && this.getCurrentTileStatus() === 'paused' && !!this.guessingTeamId
    },
    viewerSuspended() {
      return this.isGuessSuspended && !this.canControlGame
    },
    guessingTeamLabel() {
      if (this.guessingTeamId) {
        const team = this.teams.find((candidate) => candidate.id === this.guessingTeamId)
        if (team?.name?.trim()) return team.name.trim()
      }
      return 'A team'
    },
    currentSelector() {
      return this.teams.find((team) => team.id === this.currentSelectorId) || null
    },
    canAward() {
      return !!this.currentTileKey && this.tiles[this.currentTileKey]?.status === 'guessed'
    },
    winnerIds() {
      const maxScore = Math.max(...this.teams.map((team) => team.score))
      return this.teams.filter((team) => team.score === maxScore).map((team) => team.id)
    },
    winnerNames() {
      return this.teams
        .filter((team) => this.winnerIds.includes(team.id))
        .map((team) => team.name)
        .join(', ')
    },
    connectionLabel() {
      if (!this.gameId) return 'Local mode'
      if (this.sessionBusy) return 'Connecting'
      if (!this.socketConnected) return this.isHost ? 'Host disconnected' : 'Viewer disconnected'
      return this.isHost ? 'Connected as host' : 'Connected as viewer'
    }
  },
  watch: {
    currentClipUrl(newValue: string) {
      if (!newValue) {
        this.teardownAudio()
        return
      }
      nextTick(() => this.setupAudioGraph())
    }
  },
  methods: {
    normalizeGameId(value: string) {
      return value.toUpperCase().replace(/[^A-Z0-9]/g, '')
    },
    setGameInUrl(gameId: string) {
      const url = new URL(window.location.href)
      if (gameId) {
        url.searchParams.set('game', gameId)
      } else {
        url.searchParams.delete('game')
      }
      window.history.replaceState({}, '', url.toString())
    },
    closeSocket() {
      if (this.socket) {
        this.socket.close()
        this.socket = null
      }
      this.socketConnected = false
    },
    async createSession(preferredId?: string) {
      if (this.sessionBusy || this.gameId) return
      const requestedId = this.normalizeGameId(preferredId || this.gameIdInput)
      if (!requestedId) {
        this.syncError = 'Game ID is required'
        return
      }
      this.sessionBusy = true
      this.syncError = ''
      try {
        const response = await fetch(`${REALTIME_BASE}/sessions`, {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ gameId: requestedId })
        })
        if (!response.ok) {
          let errorMessage = 'Failed to create session'
          try {
            const payload = (await response.json()) as { error?: string }
            if (payload?.error) errorMessage = payload.error
          } catch {
            // ignore parse errors and keep default message
          }
          throw new Error(errorMessage)
        }
        const payload = (await response.json()) as { gameId: string }
        const gameId = this.normalizeGameId(payload.gameId)
        this.gameId = gameId
        this.gameIdInput = gameId
        this.isHost = true
        this.audioUnlocked = true
        this.setGameInUrl(gameId)
        await this.connectSession()
      } catch (error) {
        this.syncError = error instanceof Error ? error.message : 'Failed to create session'
      } finally {
        this.sessionBusy = false
      }
    },
    async joinSession() {
      if (this.sessionBusy || this.gameId) return
      const normalized = this.normalizeGameId(this.gameIdInput)
      if (!normalized) return
      this.sessionBusy = true
      this.syncError = ''
      try {
        const existsResponse = await fetch(`${REALTIME_BASE}/sessions/${normalized}`)
        if (!existsResponse.ok) throw new Error('Session not found')
        const sessionInfo = (await existsResponse.json()) as { hasState?: boolean }
        if (!sessionInfo.hasState) {
          throw new Error('Game is not open yet. Host must press Next: Choose Game first.')
        }
        this.gameId = normalized
        this.gameIdInput = normalized
        this.isHost = false
        this.audioUnlocked = false
        this.viewerTeamId = ''
        this.setGameInUrl(normalized)
        await this.connectSession()
      } catch (error) {
        this.syncError = error instanceof Error ? error.message : 'Failed to join session'
      } finally {
        this.sessionBusy = false
      }
    },
    leaveSession() {
      this.closeSocket()
      this.gameId = ''
      this.gameIdInput = ''
      this.isHost = false
      this.syncError = ''
      this.audioUnlocked = true
      this.latestRemoteState = null
      this.viewerTeamId = ''
      this.guessingTeamId = null
      this.pauseTransitionLockUntil = 0
      this.setGameInUrl('')
    },
    async connectSession() {
      this.closeSocket()
      if (!this.gameId) return
      const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
      const wsUrl = `${protocol}//${window.location.host}${REALTIME_BASE}/ws?gameId=${encodeURIComponent(
        this.gameId
      )}&clientId=${encodeURIComponent(this.clientId)}`
      const socket = new WebSocket(wsUrl)
      this.socket = socket

      socket.onmessage = async (event) => {
        let message: RealtimeMessage | null = null
        try {
          message = JSON.parse(event.data as string) as RealtimeMessage
        } catch {
          return
        }
        if (!message) return
        if (message.type === 'session:init' || message.type === 'state:update') {
          if (message.state) await this.applyRemoteState(message.state)
          return
        }
        if (message.type === 'guess:request') {
          await this.handleRemoteGuessRequest(message.teamId || '')
        }
      }

      socket.onclose = () => {
        if (this.socket === socket) {
          this.socketConnected = false
        }
      }

      await new Promise<void>((resolve, reject) => {
        socket.onopen = () => {
          if (this.socket !== socket) return
          this.socketConnected = true
          this.syncError = ''
          if (this.isHost) this.publishState()
          resolve()
        }
        socket.onerror = () => {
          if (this.socket !== socket) return
          this.socketConnected = false
          this.syncError = 'Realtime connection failed'
          reject(new Error('Realtime connection failed'))
        }
      }).catch(() => {})
    },
    buildRealtimeState(): RealtimeState {
      return {
        step: this.step,
        teams: this.teams.map((team) => ({ ...team })),
        selectedGameName: this.selectedGame?.name || null,
        tiles: cloneTiles(this.tiles),
        currentTileKey: this.currentTileKey,
        currentClipUrl: this.currentClipUrl,
        currentSelectorId: this.currentSelectorId,
        lastAwardedTeamId: this.lastAwardedTeamId,
        isAnswerClip: this.isAnswerClip,
        guessingTeamId: this.guessingTeamId
      }
    },
    publishState() {
      if (!this.isHost || !this.socketConnected || !this.socket) return
      if (this.isApplyingRemote) return
      if (this.step === 'setup') return
      const payload = {
        type: 'state:update',
        state: this.buildRealtimeState()
      }
      this.socket.send(JSON.stringify(payload))
    },
    queueStateSync() {
      if (!this.isHost || !this.socketConnected || !this.socket) return
      if (this.isApplyingRemote) return
      if (this.syncTimer) window.clearTimeout(this.syncTimer)
      this.syncTimer = window.setTimeout(() => {
        this.publishState()
      }, 80)
    },
    async applyRemoteState(state: RealtimeState) {
      if (this.isHost) return
      if (this.loadingGames) {
        this.queuedRemoteState = state
        return
      }
      this.latestRemoteState = state
      const previousClipUrl = this.currentClipUrl
      this.isApplyingRemote = true
      try {
        const selected = state.selectedGameName
          ? this.games.find((game) => game.name === state.selectedGameName) || null
          : null
        this.step = state.step
        this.teams = state.teams.map((team) => ({ ...team }))
        this.selectedGame = selected
        this.tiles = cloneTiles(state.tiles)
        this.currentTileKey = state.currentTileKey
        this.currentClipUrl = state.currentClipUrl
        this.currentSelectorId = state.currentSelectorId
        this.lastAwardedTeamId = state.lastAwardedTeamId
        this.isAnswerClip = state.isAnswerClip
        this.guessingTeamId = state.guessingTeamId || null
        if (this.viewerTeamId && !this.teams.some((team) => team.id === this.viewerTeamId)) {
          this.viewerTeamId = ''
        }
      } finally {
        this.isApplyingRemote = false
      }
      await nextTick()
      this.syncRemotePlayback(previousClipUrl !== this.currentClipUrl)
    },
    async enableViewerAudio() {
      if (this.canControlGame) return
      this.audioUnlocked = true
      this.ensureAudioContext()
      await nextTick()
      this.syncRemotePlayback(false)
    },
    requestGuessStop() {
      if (this.canControlGame) return
      if (!this.socketConnected || !this.socket) return
      if (!this.viewerTeamId) return
      if (this.getCurrentTileStatus() !== 'playing') return
      this.socket.send(
        JSON.stringify({
          type: 'guess:request',
          teamId: this.viewerTeamId
        })
      )
    },
    async handleRemoteGuessRequest(teamId: string) {
      if (!this.canControlGame || !this.isHost) return
      if (!teamId) return
      if (!this.currentTileKey || !this.selectedGame) return
      const status = this.getCurrentTileStatus()
      if (status !== 'playing') return
      const key = this.currentTileKey
      this.tiles[key].status = 'paused'
      this.lastAwardedTeamId = null
      this.guessingTeamId = teamId
      this.pauseTransitionLockUntil = Date.now() + 1200
      this.isAnswerClip = false
      const player = this.getPlayer()
      player?.pause()
      this.queueStateSync()
    },
    getCurrentTileStatus() {
      if (!this.currentTileKey) return 'available'
      return this.tiles[this.currentTileKey]?.status || 'available'
    },
    syncRemotePlayback(forceReload: boolean) {
      if (this.canControlGame) return
      const player = this.getPlayer()
      if (!player) return
      const tileStatus = this.getCurrentTileStatus()

      if (!this.currentClipUrl || tileStatus === 'won' || tileStatus === 'void') {
        player.pause()
        return
      }

      if (forceReload) {
        player.currentTime = 0
        player.load()
      }

      if (tileStatus === 'paused') {
        player.pause()
        return
      }

      if (tileStatus === 'playing' || tileStatus === 'guessed') {
        if (!this.audioUnlocked) return
        this.ensureAudioContext()
        player.play().catch(() => {})
      }
    },
    getPlayer() {
      return this.$refs.player as HTMLAudioElement | undefined
    },
    ensureAudioContext() {
      if (!this.audioContext) {
        this.audioContext = new AudioContext()
      }
      if (this.audioContext.state === 'suspended') {
        this.audioContext.resume().catch(() => {})
      }
    },
    setupAudioGraph() {
      const player = this.getPlayer()
      if (!player) return
      try {
        this.ensureAudioContext()
      } catch {
        return
      }

      if (this.analyser) {
        this.analyser.disconnect()
        this.analyser = null
      }

      const context = this.audioContext
      if (!context) return

      if (this.mediaElement !== player) {
        if (this.mediaSource) {
          this.mediaSource.disconnect()
        }
        try {
          this.mediaSource = context.createMediaElementSource(player)
        } catch {
          return
        }
        this.mediaElement = player
      }

      const analyser = context.createAnalyser()
      analyser.fftSize = 1024
      this.mediaSource?.connect(analyser)
      analyser.connect(context.destination)

      this.analyser = analyser
      this.analyserData = new Uint8Array(analyser.fftSize)
      this.frequencyData = new Uint8Array(analyser.frequencyBinCount)
      this.pulseLevel = 0
      this.rayLevel = 0
      this.tentacleLevels = this.tentacleLevels.map(() => 0)
    },
    teardownAudio() {
      if (this.rafId) {
        cancelAnimationFrame(this.rafId)
        this.rafId = 0
      }
      if (this.analyser) {
        this.analyser.disconnect()
        this.analyser = null
      }
      if (this.mediaSource) {
        this.mediaSource.disconnect()
        this.mediaSource = null
      }
      this.mediaElement = null
      this.analyserData = null
      this.frequencyData = null
      this.isPlaying = false
      this.pulseLevel = 0
      this.rayLevel = 0
      this.tentacleLevels = this.tentacleLevels.map(() => 0)
    },
    startPulse() {
      if (!this.analyser || !this.analyserData || !this.frequencyData) return
      const analyser = this.analyser
      const data = this.analyserData
      const freq = this.frequencyData

      const tick = () => {
        if (!this.isPlaying || !this.analyser || !this.analyserData || !this.frequencyData) {
          this.rafId = 0
          return
        }
        analyser.getByteTimeDomainData(data)
        analyser.getByteFrequencyData(freq)
        let sumSquares = 0
        for (let i = 0; i < data.length; i += 1) {
          const centered = (data[i] - 128) / 128
          sumSquares += centered * centered
        }
        const rms = Math.sqrt(sumSquares / data.length)
        const target = Math.min(1, rms * 3.6)
        this.pulseLevel = this.pulseLevel * 0.65 + target * 0.35

        const lowBandEnd = Math.floor(freq.length * 0.2)
        const highBandStart = Math.floor(freq.length * 0.55)
        let lowSum = 0
        for (let i = 0; i < lowBandEnd; i += 1) lowSum += freq[i]
        const lowAvg = lowSum / Math.max(1, lowBandEnd) / 255

        let highSum = 0
        for (let i = highBandStart; i < freq.length; i += 1) highSum += freq[i]
        const highAvg = highSum / Math.max(1, freq.length - highBandStart) / 255

        const weightedEnergy = lowAvg * 0.4 + highAvg * 0.7
        const compressed = Math.pow(Math.min(1, weightedEnergy * 1.1), 1.8)
        const rayTarget = Math.min(1, compressed)
        this.rayLevel = this.rayLevel * 0.78 + rayTarget * 0.22
        const hueTarget = 180 + highAvg * 120
        this.rayHue = this.rayHue * 0.8 + hueTarget * 0.2

        const bandCount = this.tentacleLevels.length
        const binSize = Math.max(1, Math.floor(freq.length / bandCount))
        for (let i = 0; i < bandCount; i += 1) {
          const start = i * binSize
          const end = i === bandCount - 1 ? freq.length : start + binSize
          let bandSum = 0
          for (let j = start; j < end; j += 1) bandSum += freq[j]
          const avg = bandSum / Math.max(1, end - start) / 255
          const amplified = Math.min(1, avg * 1.25)
          const shaped = Math.log1p(amplified * 9) / Math.log(10)
          this.tentacleLevels[i] =
            this.tentacleLevels[i] * 0.6 + shaped * 0.4
        }
        this.rafId = requestAnimationFrame(tick)
      }

      if (this.rafId) cancelAnimationFrame(this.rafId)
      this.rafId = requestAnimationFrame(tick)
    },
    handlePlayerPlay() {
      this.isPlaying = true
      this.startPulse()
      if (this.isHost) this.queueStateSync()
    },
    handlePlayerPause() {
      this.isPlaying = false
      if (this.rafId) {
        cancelAnimationFrame(this.rafId)
        this.rafId = 0
      }
      this.pulseLevel = 0
      if (this.isHost) this.queueStateSync()
    },
    togglePlayback() {
      const player = this.getPlayer()
      if (!player) return
      this.ensureAudioContext()
      if (player.paused) {
        player.play().catch(() => {})
      } else {
        player.pause()
      }
    },
    teamGradient(team: Team) {
      return {
        background: `linear-gradient(135deg, ${team.colorA}, ${team.colorB})`
      }
    },
    addTeam() {
      if (!this.canControlGame) return
      this.teams.push(makeTeam(this.teams.length))
      this.queueStateSync()
    },
    removeTeam(id: string) {
      if (!this.canControlGame) return
      if (this.teams.length === 1) return
      this.teams = this.teams.filter((team) => team.id !== id)
      this.queueStateSync()
    },
    async goToSelect() {
      if (!this.canControlGame) return
      const requestedId = this.normalizeGameId(this.gameIdInput)
      if (!requestedId) {
        this.syncError = 'Game ID is required'
        return
      }
      if (!this.gameId) {
        await this.createSession(requestedId)
        if (!this.gameId) return
      }
      this.step = 'select'
      this.queueStateSync()
    },
    startGame(game: Game) {
      if (!this.canControlGame) return
      this.selectedGame = game
      this.tiles = {}
      this.currentTileKey = null
      this.currentClipUrl = ''
      this.lastAwardedTeamId = null
      this.guessingTeamId = null
      this.pauseTransitionLockUntil = 0
      this.teams = this.teams.map((team) => ({ ...team, score: 0 }))
      const randomTeam = this.teams[Math.floor(Math.random() * this.teams.length)]
      this.currentSelectorId = randomTeam?.id || null
      this.step = 'game'
      this.queueStateSync()
    },
    resetGame() {
      if (!this.canControlGame) return
      this.step = 'select'
      this.selectedGame = null
      this.tiles = {}
      this.currentTileKey = null
      this.currentClipUrl = ''
      this.lastAwardedTeamId = null
      this.isAnswerClip = false
      this.guessingTeamId = null
      this.pauseTransitionLockUntil = 0
      this.teardownAudio()
      this.queueStateSync()
    },
    tileKey(cIndex: number, qIndex: number) {
      return `${cIndex}-${qIndex}`
    },
    tileStatus(cIndex: number, qIndex: number) {
      const key = this.tileKey(cIndex, qIndex)
      return this.tiles[key]?.status || 'available'
    },
    tileDisabled(cIndex: number, qIndex: number) {
      const status = this.tileStatus(cIndex, qIndex)
      if (status === 'won' || status === 'void') return true
      if (!this.currentTileKey) return false
      return this.currentTileKey !== this.tileKey(cIndex, qIndex)
    },
    tileClass(cIndex: number, qIndex: number) {
      const status = this.tileStatus(cIndex, qIndex)
      return {
        playing: status === 'playing',
        paused: status === 'paused',
        guessed: status === 'guessed',
        won: status === 'won',
        void: status === 'void'
      }
    },
    tileStyle(cIndex: number, qIndex: number) {
      const key = this.tileKey(cIndex, qIndex)
      const entry = this.tiles[key]
      if (entry?.status !== 'won' || !entry.lastTeamId) return {}
      const team = this.teams.find((t) => t.id === entry.lastTeamId)
      if (!team) return {}
      return this.teamGradient(team)
    },
    async handleTileClick(cIndex: number, qIndex: number) {
      if (!this.canControlGame) return
      const key = this.tileKey(cIndex, qIndex)
      const status = this.tileStatus(cIndex, qIndex)
      const clue = this.selectedGame?.categories[cIndex].clues[qIndex]

      if (!clue || !clue.song) return

      if (status === 'available') {
        this.tiles[key] = { status: 'playing', lastTeamId: null }
        this.currentTileKey = key
        this.currentClipUrl = encodeURI(clue.song)
        this.isAnswerClip = false
        this.guessingTeamId = null
        this.pauseTransitionLockUntil = 0
        await nextTick()
        const player = this.getPlayer()
        if (player) {
          this.ensureAudioContext()
          player.currentTime = 0
          player.load()
          player.play().catch(() => {})
        }
        this.queueStateSync()
        return
      }

      if (status === 'playing') {
        this.tiles[key].status = 'paused'
        this.guessingTeamId = null
        this.pauseTransitionLockUntil = 0
        const player = this.getPlayer()
        player?.pause()
        this.queueStateSync()
        return
      }

      if (status === 'paused') {
        if (this.guessingTeamId) return
        if (Date.now() < this.pauseTransitionLockUntil) return
        this.tiles[key].status = 'guessed'
        this.guessingTeamId = null
        this.pauseTransitionLockUntil = 0
        this.lastAwardedTeamId = null
        if (clue.answer) {
          this.currentClipUrl = encodeURI(clue.answer)
          this.isAnswerClip = true
          await nextTick()
          const player = this.getPlayer()
          if (player) {
            this.ensureAudioContext()
            player.currentTime = 0
            player.load()
            player.play().catch(() => {})
          }
        }
        this.queueStateSync()
        return
      }

      if (status === 'guessed') {
        if (!this.lastAwardedTeamId) return
        this.tiles[key].status = 'won'
        this.tiles[key].lastTeamId = this.lastAwardedTeamId
        this.currentSelectorId = this.lastAwardedTeamId
        this.currentTileKey = null
        this.currentClipUrl = ''
        this.lastAwardedTeamId = null
        this.guessingTeamId = null
        this.pauseTransitionLockUntil = 0
        this.checkEnd()
        this.queueStateSync()
      }
    },
    async revealPausedGuess() {
      if (!this.canControlGame) return
      if (!this.currentTileKey || !this.selectedGame) return
      if (this.getCurrentTileStatus() !== 'paused') return
      const [cIndex, qIndex] = this.currentTileKey.split('-').map(Number)
      const clue = this.selectedGame.categories[cIndex].clues[qIndex]
      this.tiles[this.currentTileKey].status = 'guessed'
      this.guessingTeamId = null
      this.pauseTransitionLockUntil = 0
      this.lastAwardedTeamId = null
      if (clue.answer) {
        this.currentClipUrl = encodeURI(clue.answer)
        this.isAnswerClip = true
        await nextTick()
        const player = this.getPlayer()
        if (player) {
          this.ensureAudioContext()
          player.currentTime = 0
          player.load()
          player.play().catch(() => {})
        }
      }
      this.queueStateSync()
    },
    handleTileRightClick(cIndex: number, qIndex: number) {
      if (!this.canControlGame) return
      const key = this.tileKey(cIndex, qIndex)
      const status = this.tileStatus(cIndex, qIndex)
      if (key !== this.currentTileKey) return

      if (status === 'paused') {
        this.tiles[key].status = 'playing'
        this.pauseTransitionLockUntil = 0
        this.guessingTeamId = null
        const player = this.getPlayer()
        player?.play().catch(() => {})
        this.queueStateSync()
        return
      }

      if (status === 'playing') {
        this.tiles[key].status = 'void'
        this.currentTileKey = null
        this.currentClipUrl = ''
        this.isAnswerClip = false
        this.guessingTeamId = null
        this.pauseTransitionLockUntil = 0
        const player = this.getPlayer()
        player?.pause()
        this.teardownAudio()
        this.checkEnd()
        this.queueStateSync()
      }
    },
    awardPoints(teamId: string) {
      if (!this.canControlGame) return
      if (!this.canAward) return
      const key = this.currentTileKey
      if (!key || !this.selectedGame) return
      const [cIndex, qIndex] = key.split('-').map(Number)
      const clue = this.selectedGame.categories[cIndex].clues[qIndex]
      this.teams = this.teams.map((team) =>
        team.id === teamId ? { ...team, score: team.score + clue.points } : team
      )
      this.lastAwardedTeamId = teamId
      if (this.tiles[key]) {
        this.tiles[key].lastTeamId = teamId
      }
      this.queueStateSync()
    },
    checkEnd() {
      if (!this.selectedGame) return
      const allTiles: TileEntry['status'][] = []
      this.selectedGame.categories.forEach((category, cIndex) => {
        category.clues.forEach((_, qIndex) => {
          allTiles.push(this.tileStatus(cIndex, qIndex))
        })
      })
      const finished = allTiles.every((status) => status === 'won' || status === 'void')
      if (finished) {
        this.step = 'end'
        this.guessingTeamId = null
      }
    }
  },
  unmounted() {
    this.teardownAudio()
    this.closeSocket()
    if (this.syncTimer) window.clearTimeout(this.syncTimer)
  }
}
</script>

