<template>
  <div class="app-shell" :class="'lang-' + language">
    <input ref="imageInput" type="file" class="hidden-file-input" accept="image/*" capture="environment" @change="onImageSelected" />
    <input ref="scoreProofInput" type="file" class="hidden-file-input" accept="image/*" capture="environment" @change="onScoreProofSelected" />

    <AppHeader
      :t="t"
      :competition-title="competitionTitle"
      :mode="mode"
      :language="language"
      :server-time="state.serverTime ? formatServerTime(state.serverTime) : ''"
      @change-mode="mode = $event"
      @change-language="setLanguage"
    />

    <main class="page-content">
      <section v-if="loading" class="loading-state">
        <div class="spinner" />
        <p>{{ t('labels.loading') }}</p>
      </section>

      <template v-else>
        <Transition name="mode-fade" mode="out-in">
          <div :key="'mode-' + mode">
            <ViewModePanel
              v-if="mode === 'view'"
              :t="t"
              :view-tabs="viewTabs"
              :view-tab="viewTab"
              :group-summaries="groupSummaries"
              :players-sorted="playersSorted"
              :row-class="rowClass"
              :player-image="playerImage"
              :display-name="displayName"
              :secondary-name="secondaryName"
              :live-mode="liveMode"
              :selected-live-group="selectedLiveGroup"
              :live-selectable-groups="liveSelectableGroups"
              :live-group-code="liveGroupCode"
              :live-members="liveMembers"
              :live-tick="liveTick"
              :preliminary-rows="preliminaryRows"
              :finalists="finalists"
              :is-finalist="isFinalist"
              :prelim-tie="prelimTie"
              :final-group1="finalGroup1"
              :final-group2="finalGroup2"
              :final-rows="finalRows"
              :final-tie="finalTie"
              :score-for="scoreFor"
              :podium-ordered="podiumOrdered"
              :podium-image="podiumImage"
              :podium-name="podiumName"
              :podium-has-result="podiumHasResult"
              :podium-score-display="podiumScoreDisplay"
              :can-view-proofs="canViewProofs"
              :has-score-proof="hasScoreProof"
              :score-proof-for="scoreProofFor"
              @change-tab="viewTab = $event"
              @change-live-mode="setLiveMode"
              @change-live-group="setSelectedLiveGroup"
              @open-proof="onOpenProofFromPayload"
            />

            <template v-else>
              <AdminLoginPanel
                v-if="!adminToken"
                :t="t"
                :username="login.username"
                :password="login.password"
                :error="loginError"
                @update:username="login.username = $event"
                @update:password="login.password = $event"
                @submit="adminLogin"
              />

              <AdminPanel
                v-else
                :t="t"
                :admin-tabs="adminTabs"
                :admin-tab="adminTab"
                :view-proof-in-view="canViewProofs"
                :group-setup-input="groupSetupInput"
                :admin-group-cards="adminGroupCards"
                :new-player="newPlayer"
                :assignable-groups="assignableGroups"
                :players-sorted="playersSorted"
                :player-filter="playerFilter"
                :player-sort="playerSort"
                :player-image="playerImage"
                :group-option-label="groupOptionLabel"
                :normalized-group-code="normalizedGroupCode"
                :preliminary-rows="preliminaryRows"
                :prelim-tie-rows="prelimTieRows"
                :final-group1="finalGroup1"
                :final-group2="finalGroup2"
                :final-tie-rows="finalTieRows"
                :final-rows="finalRows"
                :prelim-tie="prelimTie"
                :final-tie="finalTie"
                :score-filters="scoreFilters"
                :display-name="displayName"
                :secondary-name="secondaryName"
                :score-input-value="scoreInputValue"
                :on-score-focus="onScoreFocus"
                :on-score-input="onScoreInput"
                :on-score-commit="onScoreCommit"
                :has-score-proof="hasScoreProof"
                :score-proof-for="scoreProofFor"
                :open-score-proof-uploader="openScoreProofUploader"
                :remove-score-proof="removeScoreProof"
                :open-proof-preview="openProofPreview"
                :request-score-advice="requestScoreAdvice"
                @refresh="fetchState()"
                @logout="adminLogout"
                @toggle-view-proof="updateViewProofSetting"
                @change-admin-tab="adminTab = $event"
                @update:group-setup-input="groupSetupInput = $event"
                @save-group-setup="saveGroupSetup"
                @auto-group-even="autoGroupEvenly"
                @update:new-player="updateNewPlayer"
                @create-player="createPlayer"
                @convert-new-name="convertNewPlayerName"
                @update:player-filter="playerFilter = $event"
                @update:player-sort="playerSort = $event"
                @open-image-uploader="openImageUploader"
                @update-player-field="onUpdatePlayerFieldPayload"
                @update-player-group="onUpdatePlayerGroupPayload"
                @convert-row-name="convertRowPlayerName"
                @remove-player-image="removePlayerImage"
                @delete-player="deletePlayer"
                @request-reset-stage="openResetStageModal"
                @update-score-filter="updateScoreFilter"
              />
            </template>
          </div>
        </Transition>
      </template>
    </main>

    <ProofModal :open="proofModal.open" :image="proofModal.image" :title="proofModal.title" @close="closeProofModal" />
    <ResetStageModal :open="resetModal.open" :t="t" @cancel="closeResetStageModal" @confirm="confirmResetStage" />
    <ImageCropModal
      :open="avatarCropModal.open"
      :source-image="avatarCropModal.sourceImage"
      :t="t"
      @close="closeAvatarCropModal"
      @confirm="onAvatarCropConfirm"
    />
    <ScoreAdviceModal
      :open="scoreAdviceModal.open"
      :t="t"
      :advice="scoreAdviceModal.advice"
      :current-score="scoreAdviceModal.currentScore"
      :image="scoreAdviceModal.image"
      :loading="scoreAdviceModal.loading"
      :error="scoreAdviceModal.error"
      @close="closeScoreAdviceModal"
      @refresh="refreshScoreAdvice"
      @apply="applySuggestedScore"
    />

    <div class="toast" :class="[toast.kind, { show: toast.show }]">{{ toast.message }}</div>
  </div>
</template>

<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import AppHeader from './components/AppHeader.vue'
import ViewModePanel from './components/ViewModePanel.vue'
import AdminLoginPanel from './components/AdminLoginPanel.vue'
import AdminPanel from './components/AdminPanel.vue'
import ProofModal from './components/ProofModal.vue'
import ResetStageModal from './components/ResetStageModal.vue'
import ImageCropModal from './components/ImageCropModal.vue'
import ScoreAdviceModal from './components/ScoreAdviceModal.vue'
import { I18N, createInitialState } from './constants/i18n'
import { DEFAULT_PRIMARY_GROUPS, groupKey, loadPrimaryGroups, normalizedGroupCode, parseGroupList } from './utils/groups'
import { convertNameAuto } from './utils/nameTransliteration'

const API_BASE = import.meta.env.VITE_API_BASE || '/api'
const FALLBACK_SYNC_MS = 15000
const STREAM_RETRY_MS = 1500

const loading = ref(true)
const mode = ref('view')
const viewTab = ref('groups')
const adminTab = ref('players')
const language = ref(localStorage.getItem('shooting_lang') === 'en' ? 'en' : 'ar')
const state = ref(createInitialState())
const primaryGroups = ref(loadPrimaryGroups(localStorage))

const adminToken = ref(localStorage.getItem('shooting_admin_token') || '')
const login = reactive({ username: 'datwyler', password: 'datwyler' })
const loginError = ref('')

const newPlayer = reactive({ nameAr: '', nameEn: '', groupCode: '' })
const imageInput = ref(null)
const scoreProofInput = ref(null)
const uploadingPlayerId = ref(null)
const uploadingScoreProof = reactive({ stage: '', playerId: null })
const groupSetupInput = ref(primaryGroups.value.join(', '))
const playerFilter = ref('')
const playerSort = ref('id')
const scoreFilters = reactive({
  preliminary: '',
  prelim_tiebreak: '',
  final: '',
  final_tiebreak: '',
})

const liveGroupIndex = ref(0)
const liveMode = ref('rotate')
const selectedLiveGroup = ref('')
const liveTick = ref(0)
let liveTimer = null
let syncTimer = null
let stream = null
let streamRetryTimer = null
let streamConnected = false
let fetchingState = false

const proofModal = reactive({ open: false, image: '', title: '' })
const resetModal = reactive({ open: false, stage: '' })
const avatarCropModal = reactive({ open: false, sourceImage: '' })
const scoreAdviceModal = reactive({
  open: false,
  loading: false,
  error: '',
  image: '',
  advice: null,
  currentScore: 0,
  stage: '',
  playerId: 0,
})
const scoreAdviceCache = reactive({})

const scoreDrafts = reactive({})

const toast = reactive({ show: false, kind: 'success', message: '' })
let toastTimer = null

const competitionTitle = computed(() => {
  const fallback = t('titleFallback')
  const title = language.value === 'ar' ? state.value.competition?.titleAr : state.value.competition?.titleEn
  return title || fallback
})

const playersSorted = computed(() => [...(state.value.players || [])].sort((a, b) => a.id - b.id))
const preliminaryRows = computed(() => state.value.derived?.preliminaryRanking?.rows || [])
const finalists = computed(() => state.value.derived?.finalists || [])
const finalGroup1 = computed(() => state.value.derived?.finalGroups?.group1 || [])
const finalGroup2 = computed(() => state.value.derived?.finalGroups?.group2 || [])
const finalRows = computed(() => state.value.derived?.finalRanking?.rows || [])
const podiumRows = computed(() => state.value.derived?.podium || [])
const prelimTie = computed(() => state.value.derived?.preliminaryRanking?.tieBreak || { required: false, resolved: true, slots: 0, playerIds: [] })
const finalTie = computed(() => state.value.derived?.finalRanking?.tieBreak || { required: false, resolved: true, slots: 0, playerIds: [] })
const canViewProofs = computed(() => Boolean(state.value.settings?.viewProofInView))

const viewTabs = computed(() => [
  { id: 'groups', label: t('tabs.groups') },
  { id: 'live', label: t('tabs.live') },
  { id: 'overall', label: t('tabs.overall') },
  { id: 'final', label: t('tabs.final') },
  { id: 'podium', label: t('tabs.podium') },
])

const adminTabs = computed(() => [
  { id: 'players', label: t('tabs.players') },
  { id: 'preliminary', label: t('tabs.preliminary') },
  { id: 'prelimTie', label: t('tabs.prelimTie') },
  { id: 'final', label: t('tabs.final') },
  { id: 'finalTie', label: t('tabs.finalTie') },
])

const groupsBoard = computed(() => {
  const map = new Map()
  for (const player of playersSorted.value) {
    const code = (player.groupCode || '').trim() || t('labels.unassigned')
    if (!map.has(code)) map.set(code, [])
    map.get(code).push(player)
  }
  return [...map.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([code, players]) => ({ code, players }))
})

const groupSummaries = computed(() => groupsBoard.value.map((group) => ({ code: group.code, key: groupKey(group.code), count: group.players.length })))

const adminGroupStats = computed(() => {
  const counts = { unassigned: 0 }
  for (const code of primaryGroups.value) {
    counts[code] = 0
  }
  const other = new Map()
  for (const player of playersSorted.value) {
    const code = normalizedGroupCode(player.groupCode)
    if (!code) {
      counts.unassigned += 1
    } else if (primaryGroups.value.includes(code)) {
      counts[code] += 1
    } else {
      other.set(code, (other.get(code) || 0) + 1)
    }
  }
  return { counts, other: [...other.entries()].sort((a, b) => a[0].localeCompare(b[0])) }
})

const assignableGroups = computed(() => {
  const extra = adminGroupStats.value.other.map(([code]) => code)
  return [...primaryGroups.value, ...extra]
})

const adminGroupCards = computed(() => {
  const cards = primaryGroups.value.map((code) => ({
    key: code.toLowerCase(),
    label: `${t('labels.group')} ${code}`,
    count: adminGroupStats.value.counts[code],
  }))
  cards.push({
    key: 'u',
    label: t('labels.unassigned'),
    count: adminGroupStats.value.counts.unassigned,
  })
  return cards
})

const activeGroupCodes = computed(() => {
  const seen = new Set()
  for (const player of playersSorted.value) {
    const code = normalizedGroupCode(player.groupCode)
    if (code) seen.add(code)
  }
  return [...seen].sort((a, b) => a.localeCompare(b))
})

const liveSelectableGroups = computed(() => assignableGroups.value)
const liveRotationGroups = computed(() => activeGroupCodes.value)

const liveGroupCode = computed(() => {
  if (liveMode.value === 'fixed') {
    return selectedLiveGroup.value || liveSelectableGroups.value[0] || ''
  }
  return liveRotationGroups.value[liveGroupIndex.value] || ''
})
const liveMembers = computed(() => {
  const code = normalizedGroupCode(liveGroupCode.value)
  if (!code) return []
  return playersSorted.value.filter((player) => normalizedGroupCode(player.groupCode) === code)
})
const podiumOrdered = computed(() => [podiumRows.value[1] || null, podiumRows.value[0] || null, podiumRows.value[2] || null])

const prelimTieRows = computed(() => {
  const ids = new Set(prelimTie.value.playerIds || [])
  return preliminaryRows.value.filter((row) => ids.has(row.playerId))
})

const finalTieRows = computed(() => {
  const ids = new Set(finalTie.value.playerIds || [])
  return finalRows.value.filter((row) => ids.has(row.playerId))
})

watch(language, (value) => {
  localStorage.setItem('shooting_lang', value)
  document.documentElement.lang = value
  document.documentElement.dir = value === 'ar' ? 'rtl' : 'ltr'
})

watch(
  () => `${mode.value}|${viewTab.value}|${liveMode.value}|${liveRotationGroups.value.join('|')}`,
  () => {
    if (mode.value === 'view' && viewTab.value === 'live' && liveMode.value === 'rotate') {
      startLiveRotation()
      return
    }
    stopLiveRotation()
  },
)

watch(
  () => liveSelectableGroups.value.join('|'),
  () => {
    if (liveSelectableGroups.value.length === 0) {
      selectedLiveGroup.value = ''
      return
    }
    if (!liveSelectableGroups.value.includes(selectedLiveGroup.value)) {
      selectedLiveGroup.value = liveSelectableGroups.value[0]
    }
  },
  { immediate: true },
)

watch(
  () => liveRotationGroups.value.join('|'),
  () => {
    if (liveRotationGroups.value.length === 0) {
      liveGroupIndex.value = 0
      return
    }
    if (liveGroupIndex.value >= liveRotationGroups.value.length) {
      liveGroupIndex.value = 0
    }
  },
  { immediate: true },
)

onMounted(async () => {
  document.documentElement.lang = language.value
  document.documentElement.dir = language.value === 'ar' ? 'rtl' : 'ltr'
  await fetchState()
  startRealtimeSync()
})

onBeforeUnmount(() => {
  stopLiveRotation()
  stopRealtimeSync()
  if (toastTimer) clearTimeout(toastTimer)
})

function t(path) {
  const source = I18N[language.value]
  return path.split('.').reduce((value, key) => (value && value[key] !== undefined ? value[key] : path), source)
}

function setLanguage(next) {
  if (next === 'ar' || next === 'en') language.value = next
}

function formatServerTime(iso) {
  try {
    return new Intl.DateTimeFormat(language.value === 'ar' ? 'ar-AE' : 'en-US', {
      dateStyle: 'medium',
      timeStyle: 'short',
    }).format(new Date(iso))
  } catch {
    return iso
  }
}

function groupOptionLabel(code) {
  const normalized = normalizedGroupCode(code)
  if (!normalized) {
    return `${t('labels.unassigned')} (${adminGroupStats.value.counts.unassigned})`
  }
  if (primaryGroups.value.includes(normalized)) {
    return `${normalized} (${adminGroupStats.value.counts[normalized]})`
  }
  const count = adminGroupStats.value.other.find(([key]) => key === normalized)?.[1] || 0
  return `${normalized} (${count})`
}

function saveGroupSetup() {
  const parsed = parseGroupList(groupSetupInput.value)
  primaryGroups.value = parsed.length > 0 ? parsed : DEFAULT_PRIMARY_GROUPS
  groupSetupInput.value = primaryGroups.value.join(', ')
  localStorage.setItem('shooting_group_list', primaryGroups.value.join(','))
  if (!primaryGroups.value.includes(normalizedGroupCode(newPlayer.groupCode))) {
    newPlayer.groupCode = ''
  }
  showToast(t('messages.saved'))
}

function updateNewPlayer({ key, value }) {
  if (!Object.prototype.hasOwnProperty.call(newPlayer, key)) return
  newPlayer[key] = value
}

function convertNewPlayerName(direction) {
  if (direction === 'ar_to_en') {
    const source = String(newPlayer.nameAr || '').trim()
    if (!source) {
      showToast(t('messages.noNameToConvert'), 'error')
      return
    }
    newPlayer.nameEn = convertNameAuto('ar_to_en', source)
    return
  }
  if (direction === 'en_to_ar') {
    const source = String(newPlayer.nameEn || '').trim()
    if (!source) {
      showToast(t('messages.noNameToConvert'), 'error')
      return
    }
    newPlayer.nameAr = convertNameAuto('en_to_ar', source)
  }
}

function updateScoreFilter({ stage, value }) {
  if (!Object.prototype.hasOwnProperty.call(scoreFilters, stage)) return
  scoreFilters[stage] = value
}

async function autoGroupEvenly() {
  if (primaryGroups.value.length === 0) {
    showToast(t('messages.noGroupsConfigured'), 'error')
    return
  }
  try {
    state.value = await api('/admin/players/auto-group', {
      method: 'POST',
      admin: true,
      body: { groups: primaryGroups.value },
    })
    showToast(t('messages.saved'))
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

function setLiveMode(nextMode) {
  if (nextMode !== 'rotate' && nextMode !== 'fixed') return
  liveMode.value = nextMode
  if (nextMode === 'fixed' && !selectedLiveGroup.value && liveSelectableGroups.value.length > 0) {
    selectedLiveGroup.value = liveSelectableGroups.value[0]
  }
}

function setSelectedLiveGroup(nextValue) {
  const next = normalizedGroupCode(nextValue || '')
  selectedLiveGroup.value = next
}

function rowClass(code) {
  const key = groupKey(code)
  if (key === 'u') return 'row-group-empty'
  return 'row-group-' + key
}

function displayName(item) {
  return language.value === 'ar' ? item.nameAr : item.nameEn
}

function secondaryName(item) {
  return language.value === 'ar' ? item.nameEn : item.nameAr
}

function playerImage(player) {
  if (player?.imageData) return player.imageData
  const name = player?.nameAr || player?.nameEn || 'Player'
  return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=F0F2F8&color=1B1F40&bold=true&rounded=true&size=180`
}

function showToast(message, kind = 'success') {
  toast.message = message
  toast.kind = kind
  toast.show = true
  if (toastTimer) clearTimeout(toastTimer)
  toastTimer = setTimeout(() => {
    toast.show = false
  }, 2500)
}

function clearAdminSession(notify = false) {
  adminToken.value = ''
  localStorage.removeItem('shooting_admin_token')
  if (notify) showToast(t('messages.unauthorized'), 'error')
}

async function api(path, options = {}) {
  const response = await fetch(`${API_BASE}${path}`, {
    method: options.method || 'GET',
    headers: {
      'Content-Type': 'application/json',
      ...(options.admin && adminToken.value ? { Authorization: `Bearer ${adminToken.value}` } : {}),
    },
    body: options.body ? JSON.stringify(options.body) : undefined,
  })

  let payload = null
  try {
    payload = await response.json()
  } catch {
    payload = null
  }

  if (!response.ok) {
    if (options.admin && response.status === 401) {
      clearAdminSession(true)
    }
    throw new Error(payload?.message || `${response.status} ${response.statusText}`)
  }

  return payload
}

async function fetchState(showLoader = true) {
  if (fetchingState) return
  fetchingState = true
  if (showLoader) loading.value = true
  try {
    const wantsAdminState = Boolean(adminToken.value)
    state.value = await api(wantsAdminState ? '/admin/state' : '/state', { admin: wantsAdminState })
  } catch (error) {
    if (adminToken.value === '') {
      try {
        state.value = await api('/state')
      } catch {}
    }
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  } finally {
    fetchingState = false
    if (showLoader) loading.value = false
  }
}

async function adminLogin() {
  loginError.value = ''
  try {
    const payload = await api('/admin/login', {
      method: 'POST',
      body: { username: login.username, password: login.password },
    })
    adminToken.value = payload.token
    localStorage.setItem('shooting_admin_token', payload.token)
    await fetchState(false)
    showToast(t('messages.saved'))
  } catch (error) {
    loginError.value = error.message
  }
}

async function adminLogout() {
  try {
    if (adminToken.value) {
      await api('/admin/logout', { method: 'POST', admin: true })
    }
  } catch {}
  clearAdminSession(false)
  await fetchState(false)
}

async function updateViewProofSetting(enabled) {
  try {
    state.value = await api('/admin/settings', {
      method: 'PUT',
      admin: true,
      body: { viewProofInView: Boolean(enabled) },
    })
    showToast(t('messages.saved'))
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

async function createPlayer() {
  if (!newPlayer.nameAr || !newPlayer.nameEn) {
    showToast(t('messages.mustProvideNames'), 'error')
    return
  }

  try {
    state.value = await api('/admin/players', {
      method: 'POST',
      admin: true,
      body: {
        nameAr: newPlayer.nameAr,
        nameEn: newPlayer.nameEn,
        groupCode: normalizedGroupCode(newPlayer.groupCode),
      },
    })
    newPlayer.nameAr = ''
    newPlayer.nameEn = ''
    newPlayer.groupCode = ''
    showToast(t('messages.saved'))
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

async function updatePlayer(playerId, body) {
  try {
    state.value = await api(`/admin/players/${playerId}`, {
      method: 'PUT',
      admin: true,
      body,
    })
    showToast(t('messages.saved'))
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

async function updatePlayerField(player, field, event) {
  const value = String(event.target.value || '').trim()
  if (value === player[field]) return

  if ((field === 'nameAr' || field === 'nameEn') && value === '') {
    event.target.value = player[field]
    showToast(t('messages.mustProvideNames'), 'error')
    return
  }

  await updatePlayer(player.id, { [field]: value })
}

function onUpdatePlayerFieldPayload({ player, field, event }) {
  updatePlayerField(player, field, event)
}

async function updatePlayerGroup(player, event) {
  const next = normalizedGroupCode(event.target.value)
  const current = normalizedGroupCode(player.groupCode)
  if (next === current) return
  await updatePlayer(player.id, { groupCode: next })
}

function onUpdatePlayerGroupPayload({ player, event }) {
  updatePlayerGroup(player, event)
}

async function convertRowPlayerName({ player, direction }) {
  if (!player || !direction) return

  if (direction === 'ar_to_en') {
    const source = String(player.nameAr || '').trim()
    if (!source) {
      showToast(t('messages.noNameToConvert'), 'error')
      return
    }
    await updatePlayer(player.id, { nameEn: convertNameAuto('ar_to_en', source) })
    return
  }

  if (direction === 'en_to_ar') {
    const source = String(player.nameEn || '').trim()
    if (!source) {
      showToast(t('messages.noNameToConvert'), 'error')
      return
    }
    await updatePlayer(player.id, { nameAr: convertNameAuto('en_to_ar', source) })
  }
}

async function deletePlayer(playerId) {
  const ok = window.confirm(t('messages.confirmDelete'))
  if (!ok) return

  try {
    state.value = await api(`/admin/players/${playerId}`, {
      method: 'DELETE',
      admin: true,
    })
    showToast(t('messages.saved'))
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

function openImageUploader(playerId) {
  if (mode.value !== 'admin' || !adminToken.value) return
  uploadingPlayerId.value = playerId
  imageInput.value?.click()
}

async function onImageSelected(event) {
  const file = event.target.files?.[0]
  event.target.value = ''
  if (!file || !uploadingPlayerId.value) return

  try {
    avatarCropModal.sourceImage = await compressImage(file, 1800, 0.9)
    avatarCropModal.open = true
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

function closeAvatarCropModal() {
  avatarCropModal.open = false
  avatarCropModal.sourceImage = ''
  uploadingPlayerId.value = null
}

async function onAvatarCropConfirm(imageData) {
  if (!uploadingPlayerId.value || !imageData) {
    closeAvatarCropModal()
    return
  }
  try {
    await updatePlayer(uploadingPlayerId.value, { imageData })
  } finally {
    closeAvatarCropModal()
  }
}

async function removePlayerImage(playerId) {
  await updatePlayer(playerId, { clearImage: true })
}

function scoreProofFor(stage, playerId) {
  return state.value.scoreProofs?.[stage]?.[String(playerId)] || ''
}

function hasScoreProof(stage, playerId) {
  return scoreProofFor(stage, playerId) !== ''
}

function openScoreProofUploader(stage, playerId) {
  if (mode.value !== 'admin' || !adminToken.value) return
  uploadingScoreProof.stage = stage
  uploadingScoreProof.playerId = playerId
  scoreProofInput.value?.click()
}

function onOpenProofFromPayload(payload) {
  if (!payload || !payload.stage || !payload.playerId) return
  openProofPreview(payload.stage, payload.playerId)
}

function openProofPreview(stage, playerId) {
  const image = scoreProofFor(stage, playerId)
  if (!image) return
  proofModal.open = true
  proofModal.image = image
  proofModal.title = `${t('table.verification')} · ${stage}`
}

function closeProofModal() {
  proofModal.open = false
  proofModal.image = ''
  proofModal.title = ''
}

async function onScoreProofSelected(event) {
  const file = event.target.files?.[0]
  event.target.value = ''
  if (!file || !uploadingScoreProof.stage || !uploadingScoreProof.playerId) return

  try {
    const imageData = await compressImage(file, 1280, 0.86)
    state.value = await api(`/admin/scores/${uploadingScoreProof.stage}/${uploadingScoreProof.playerId}/proof`, {
      method: 'PUT',
      admin: true,
      body: { imageData },
    })
    delete scoreAdviceCache[scoreAdviceKey(uploadingScoreProof.stage, uploadingScoreProof.playerId)]
    showToast(t('messages.saved'))
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  } finally {
    uploadingScoreProof.stage = ''
    uploadingScoreProof.playerId = null
  }
}

async function removeScoreProof(stage, playerId) {
  try {
    state.value = await api(`/admin/scores/${stage}/${playerId}/proof`, {
      method: 'DELETE',
      admin: true,
    })
    delete scoreAdviceCache[scoreAdviceKey(stage, playerId)]
    showToast(t('messages.saved'))
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

async function requestScoreAdvice(stage, playerId) {
  await openScoreAdviceModal(stage, playerId)
}

async function fetchScoreAdvice(stage, playerId) {
  const image = scoreProofFor(stage, playerId)
  if (!image) {
    throw new Error(t('messages.noProofForAi'))
  }

  const advice = await api(`/admin/scores/${stage}/${playerId}/advice`, {
    method: 'POST',
    admin: true,
  })
  scoreAdviceCache[scoreAdviceKey(stage, playerId)] = advice
  return { advice, image }
}

async function openScoreAdviceModal(stage, playerId) {
  const image = scoreProofFor(stage, playerId)
  if (!image) {
    showToast(t('messages.noProofForAi'), 'error')
    return
  }
  const current = Number(scoreFor(stage, playerId) || 0)
  scoreAdviceModal.open = true
  scoreAdviceModal.loading = true
  scoreAdviceModal.error = ''
  scoreAdviceModal.image = image
  scoreAdviceModal.advice = scoreAdviceCache[scoreAdviceKey(stage, playerId)] || null
  scoreAdviceModal.currentScore = current
  scoreAdviceModal.stage = stage
  scoreAdviceModal.playerId = playerId

  try {
    const result = await fetchScoreAdvice(stage, playerId)
    scoreAdviceModal.image = result.image
    scoreAdviceModal.advice = result.advice
  } catch (error) {
    scoreAdviceModal.error = `${t('messages.errorPrefix')}: ${error.message}`
  } finally {
    scoreAdviceModal.loading = false
  }
}

function scoreAdviceKey(stage, playerId) {
  return `${stage}:${playerId}`
}

function closeScoreAdviceModal() {
  scoreAdviceModal.open = false
  scoreAdviceModal.loading = false
  scoreAdviceModal.error = ''
  scoreAdviceModal.image = ''
  scoreAdviceModal.advice = null
  scoreAdviceModal.currentScore = 0
  scoreAdviceModal.stage = ''
  scoreAdviceModal.playerId = 0
}

async function refreshScoreAdvice() {
  if (!scoreAdviceModal.stage || !scoreAdviceModal.playerId) return
  await openScoreAdviceModal(scoreAdviceModal.stage, scoreAdviceModal.playerId)
}

async function applySuggestedScore() {
  if (!scoreAdviceModal.advice) return
  const stage = scoreAdviceModal.stage
  const playerId = scoreAdviceModal.playerId
  const score = Number(scoreAdviceModal.advice.advisedScore || 0)
  await updateScoreValue(stage, playerId, score)
  scoreAdviceModal.currentScore = score
  closeScoreAdviceModal()
}

function compressImage(file, maxSize, quality) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onerror = () => reject(new Error('Failed to read image'))
    reader.onload = () => {
      const image = new Image()
      image.onerror = () => reject(new Error('Invalid image file'))
      image.onload = () => {
        let width = image.width
        let height = image.height

        if (width > height && width > maxSize) {
          height = Math.round((height * maxSize) / width)
          width = maxSize
        } else if (height > maxSize) {
          width = Math.round((width * maxSize) / height)
          height = maxSize
        }

        const canvas = document.createElement('canvas')
        canvas.width = width
        canvas.height = height
        const ctx = canvas.getContext('2d')
        if (!ctx) {
          reject(new Error('Failed to process image'))
          return
        }

        ctx.drawImage(image, 0, 0, width, height)
        resolve(canvas.toDataURL('image/jpeg', quality))
      }
      image.src = reader.result
    }
    reader.readAsDataURL(file)
  })
}

function scoreFor(stage, playerId) {
  return state.value.scores?.[stage]?.[String(playerId)] ?? 0
}

function scoreDraftKey(stage, playerId) {
  return `${stage}:${playerId}`
}

function scoreInputValue(stage, playerId) {
  const draft = scoreDrafts[scoreDraftKey(stage, playerId)]
  if (draft) return draft.value
  return String(scoreFor(stage, playerId))
}

function onScoreFocus(stage, playerId) {
  const key = scoreDraftKey(stage, playerId)
  if (!scoreDrafts[key]) {
    scoreDrafts[key] = { value: String(scoreFor(stage, playerId)), committing: false }
  }
}

function onScoreInput(stage, playerId, event) {
  const key = scoreDraftKey(stage, playerId)
  const raw = String(event?.target?.value ?? '')
  const cleaned = raw.replace(/[^\d]/g, '').slice(0, 4)
  const current = scoreDrafts[key] || { value: String(scoreFor(stage, playerId)), committing: false }
  scoreDrafts[key] = { ...current, value: cleaned }
  if (event?.target && event.target.value !== cleaned) event.target.value = cleaned
}

async function onScoreCommit(stage, playerId) {
  const key = scoreDraftKey(stage, playerId)
  const draft = scoreDrafts[key]
  if (!draft || draft.committing) return

  let score = Number(draft.value === '' ? 0 : draft.value)
  if (!Number.isFinite(score)) score = 0
  score = Math.trunc(score)

  if (score < 0 || score > 9999) {
    showToast(t('messages.invalidScore'), 'error')
    return
  }

  if (score === scoreFor(stage, playerId)) {
    delete scoreDrafts[key]
    return
  }

  scoreDrafts[key] = { ...draft, committing: true }
  await updateScoreValue(stage, playerId, score, key)
}

async function updateScoreValue(stage, playerId, score, draftKey = '') {
  if (score < 0 || score > 9999) {
    showToast(t('messages.invalidScore'), 'error')
    return
  }

  try {
    state.value = await api(`/admin/scores/${stage}/${playerId}`, {
      method: 'PUT',
      admin: true,
      body: { score },
    })
    if (draftKey) {
      delete scoreDrafts[draftKey]
    }
  } catch (error) {
    if (draftKey && scoreDrafts[draftKey]) {
      scoreDrafts[draftKey] = { ...scoreDrafts[draftKey], committing: false }
    }
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

function openResetStageModal(stage) {
  resetModal.open = true
  resetModal.stage = stage
}

function closeResetStageModal() {
  resetModal.open = false
  resetModal.stage = ''
}

async function confirmResetStage(resetProofs) {
  const stage = resetModal.stage
  if (!stage) return

  try {
    state.value = await api(`/admin/scores/${stage}/reset`, {
      method: 'POST',
      admin: true,
      body: { resetProofs: Boolean(resetProofs) },
    })
    for (const key of Object.keys(scoreDrafts)) {
      if (key.startsWith(`${stage}:`)) delete scoreDrafts[key]
    }
    if (resetProofs) {
      for (const key of Object.keys(scoreAdviceCache)) {
        if (key.startsWith(`${stage}:`)) delete scoreAdviceCache[key]
      }
    }
    closeResetStageModal()
    showToast(t('messages.saved'))
  } catch (error) {
    showToast(`${t('messages.errorPrefix')}: ${error.message}`, 'error')
  }
}

function isFinalist(playerId) {
  return finalists.value.some((row) => row.playerId === playerId)
}

function startLiveRotation() {
  stopLiveRotation()
  if (liveRotationGroups.value.length === 0) return

  liveGroupIndex.value = 0
  liveTick.value += 1

  if (liveRotationGroups.value.length <= 1) return

  liveTimer = setInterval(() => {
    liveGroupIndex.value = (liveGroupIndex.value + 1) % liveRotationGroups.value.length
    liveTick.value += 1
  }, 5000)
}

function stopLiveRotation() {
  if (liveTimer) {
    clearInterval(liveTimer)
    liveTimer = null
  }
}

function buildStreamURL() {
  const base = API_BASE.endsWith('/') ? API_BASE.slice(0, -1) : API_BASE
  return `${base}/events`
}

function connectStateStream() {
  if (stream) {
    stream.close()
    stream = null
  }
  if (streamRetryTimer) {
    clearTimeout(streamRetryTimer)
    streamRetryTimer = null
  }

  stream = new EventSource(buildStreamURL())
  stream.onopen = () => {
    streamConnected = true
  }
  stream.addEventListener('state', () => {
    fetchState(false)
  })
  stream.onerror = () => {
    streamConnected = false
    if (stream) {
      stream.close()
      stream = null
    }
    if (!streamRetryTimer) {
      streamRetryTimer = setTimeout(() => {
        streamRetryTimer = null
        connectStateStream()
      }, STREAM_RETRY_MS)
    }
  }
}

function startRealtimeSync() {
  stopRealtimeSync()
  connectStateStream()
  syncTimer = setInterval(() => {
    if (!streamConnected) fetchState(false)
  }, FALLBACK_SYNC_MS)
}

function stopRealtimeSync() {
  streamConnected = false
  if (streamRetryTimer) {
    clearTimeout(streamRetryTimer)
    streamRetryTimer = null
  }
  if (stream) {
    stream.close()
    stream = null
  }
  if (syncTimer) {
    clearInterval(syncTimer)
    syncTimer = null
  }
}

function podiumName(entry) {
  if (!entry || entry.score === 0) return t('labels.waiting')
  return displayName(entry)
}

function podiumImage(entry) {
  if (!entry || entry.score === 0) {
    return 'https://ui-avatars.com/api/?name=?&background=F0F2F8&color=1B1F40&rounded=true&size=200'
  }
  return playerImage(entry)
}

function podiumHasResult(entry) {
  return Boolean(entry && entry.score > 0)
}

function podiumScoreDisplay(entry) {
  if (!podiumHasResult(entry)) return '-'
  return podiumScoreText(entry)
}

function podiumScoreText(entry) {
  if (!entry || entry.score === 0) return '—'
  const scoreText = `${t('table.score')}: ${entry.score}`
  if (!finalTie.value.required) return scoreText
  return `${scoreText} · ${t('table.tieScore')}: ${entry.tieBreak}`
}
</script>
