<template>
  <div class="pb-6">
    <div class="mb-6">
      <h1 class="text-3xl font-bold text-white mb-2">{{ t('systemUpdate.title') }}</h1>
      <p class="text-white/70">{{ t('systemUpdate.subtitle') }}</p>
    </div>

    <!-- Status message -->
    <div
      v-if="statusMessage"
      class="mb-4 p-3 rounded-lg text-sm"
      :class="statusIsError ? 'bg-red-500/20 text-red-300' : 'bg-green-500/20 text-green-300'"
    >
      {{ statusMessage }}
    </div>

    <!-- Current Version -->
    <div class="glass-card p-6 mb-6">
      <h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.currentSystem') }}</h2>
      <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
        <div class="p-4 bg-white/5 rounded-lg">
          <p class="text-xs text-white/60 mb-1">{{ t('common.version') }}</p>
          <p class="text-xl font-bold text-white">v{{ currentVersion }}</p>
        </div>
        <div class="p-4 bg-white/5 rounded-lg">
          <p class="text-xs text-white/60 mb-1">{{ t('systemUpdate.lastChecked') }}</p>
          <p class="text-sm font-medium text-white">{{ lastCheckDisplay }}</p>
        </div>
        <div class="p-4 bg-white/5 rounded-lg">
          <p class="text-xs text-white/60 mb-1">{{ t('common.status') }}</p>
          <div class="flex items-center gap-2">
            <div class="w-2 h-2 rounded-full" :class="statusDotColor"></div>
            <p class="text-sm font-medium" :class="statusTextColor">{{ statusLabel }}</p>
          </div>
        </div>
      </div>
    </div>

    <!-- Available Update -->
    <div v-if="updateInfo" class="glass-card p-6 mb-6 border border-orange-400/30">
      <div class="flex items-start justify-between mb-4">
        <div>
          <h2 class="text-lg font-semibold text-white">{{ t('systemUpdate.updateAvailable') }}</h2>
          <p class="text-sm text-white/60">Version {{ updateInfo.version }} &mdash; {{ updateInfo.release_date }}</p>
        </div>
        <span class="px-3 py-1 bg-orange-500/20 text-orange-400 text-xs font-medium rounded-full">{{ t('systemUpdate.new') }}</span>
      </div>

      <!-- Changelog -->
      <div v-if="updateInfo.changelog.length" class="mb-4">
        <h3 class="text-sm font-medium text-white/80 mb-2">{{ t('systemUpdate.changelog') }}</h3>
        <ul class="space-y-1">
          <li v-for="(entry, i) in updateInfo.changelog" :key="i" class="text-sm text-white/60 flex gap-2">
            <span class="text-orange-400 shrink-0">&bull;</span>
            <span>{{ entry }}</span>
          </li>
        </ul>
      </div>

      <!-- Components -->
      <div v-if="updateInfo.components > 0" class="mb-4 p-3 bg-white/5 rounded-lg">
        <p class="text-xs text-white/60">{{ t('systemUpdate.componentsToUpdate', { count: updateInfo.components }) }}</p>
      </div>

      <!-- Actions -->
      <div class="flex gap-3">
        <!-- Git path: one-shot pull+rebuild+restart -->
        <button
          v-if="updateMethod === 'git' && !applying"
          @click="requestGitApply"
          class="glass-button rounded-lg px-6 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30"
        >
          {{ t('systemUpdate.pullAndRebuild') }}
        </button>
        <!-- Manifest path: download then apply -->
        <button
          v-if="updateMethod !== 'git' && !downloading && !applying && !downloaded"
          @click="downloadUpdate"
          class="glass-button rounded-lg px-6 py-2 text-sm font-medium"
        >
          {{ t('systemUpdate.downloadUpdate') }}
        </button>
        <button
          v-if="updateMethod !== 'git' && downloaded && !applying"
          @click="requestApply"
          class="glass-button rounded-lg px-6 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30"
        >
          {{ t('systemUpdate.applyUpdate') }}
        </button>
      </div>
      <p v-if="updateMethod === 'git'" class="text-xs text-white/40 mt-3">
        {{ t('systemUpdate.gitMethodHint') }}
      </p>
    </div>

    <!-- No update available -->
    <div v-else-if="!loading" class="glass-card p-6 mb-6">
      <div class="flex items-center gap-3 mb-4">
        <svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
        </svg>
        <h2 class="text-lg font-semibold text-white">{{ t('systemUpdate.upToDate') }}</h2>
      </div>
      <p class="text-sm text-white/60">{{ t('systemUpdate.upToDateMessage') }}</p>
    </div>

    <!-- Download Progress -->
    <div v-if="downloading" class="glass-card p-6 mb-6">
      <h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.downloading') }}</h2>
      <div class="w-full h-3 bg-white/10 rounded-full overflow-hidden mb-2">
        <div
          class="h-full rounded-full transition-all duration-500"
          :class="downloadStalled ? 'bg-amber-400' : 'bg-orange-400'"
          :style="{ width: downloadPercentFormatted + '%' }"
        ></div>
      </div>
      <div class="flex items-center justify-between gap-3 flex-wrap">
        <div class="flex items-center gap-2 min-w-0">
          <div v-if="downloadFinishing && !downloadStalled" class="w-3 h-3 border-2 border-orange-400 border-t-transparent rounded-full animate-spin shrink-0"></div>
          <p class="text-xs" :class="downloadStalled ? 'text-amber-300' : 'text-white/60'">
            {{ downloadStalled
               ? t('systemUpdate.downloadStalled')
               : downloadFinishing
                 ? t('systemUpdate.finishingDownload')
                 : t('systemUpdate.percentComplete', { percent: downloadPercentFormatted }) }}
          </p>
        </div>
        <button
          @click="requestCancelDownload"
          :disabled="cancelingDownload"
          class="glass-button rounded-lg px-4 py-1.5 text-xs font-medium disabled:opacity-40 shrink-0"
          :class="downloadStalled ? 'bg-amber-500/20 border-amber-400/40 text-amber-200' : ''"
        >
          {{ cancelingDownload ? t('systemUpdate.cancelingDownload') : t('systemUpdate.cancelDownload') }}
        </button>
      </div>
    </div>

    <!-- Applying -->
    <div v-if="applying" class="glass-card p-6 mb-6">
      <h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.applying') }}</h2>
      <div class="flex items-center gap-3">
        <div class="w-5 h-5 border-2 border-orange-400 border-t-transparent rounded-full animate-spin"></div>
        <p class="text-sm text-white/70">{{ t('systemUpdate.applyWarning') }}</p>
      </div>
    </div>

    <!-- Update Schedule -->
    <div class="glass-card p-6 mb-6">
      <h2 class="text-lg font-semibold text-white mb-2">{{ t('systemUpdate.updateSchedule') }}</h2>
      <p class="text-sm text-white/60 mb-4">{{ t('systemUpdate.subtitle') }}</p>
      <div class="space-y-3">
        <label
          v-for="opt in scheduleOptions"
          :key="opt.value"
          class="flex items-start gap-3 p-3 bg-white/5 rounded-lg cursor-pointer hover:bg-white/10 transition-colors"
          :class="{ 'ring-1 ring-orange-400/50 bg-orange-500/10': schedule === opt.value }"
        >
          <input
            type="radio"
            name="update-schedule"
            :value="opt.value"
            :checked="schedule === opt.value"
            @change="setSchedule(opt.value)"
            class="mt-1 accent-orange-400"
          />
          <div>
            <p class="text-sm font-medium text-white">{{ opt.label }}</p>
            <p class="text-xs text-white/50">{{ opt.description }}</p>
          </div>
        </label>
      </div>
    </div>

    <!-- Mirrors -->
    <div class="glass-card p-6 mb-6">
      <div class="flex items-start justify-between gap-4 mb-2">
        <h2 class="text-lg font-semibold text-white">Update mirrors</h2>
        <button
          type="button"
          class="text-xs px-3 py-1.5 rounded-md bg-white/5 hover:bg-white/10 text-white/70 hover:text-white transition-colors"
          @click="addingMirror = !addingMirror"
        >{{ addingMirror ? 'Cancel' : '+ Add mirror' }}</button>
      </div>
      <p class="text-sm text-white/60 mb-4">
        Servers this node checks for updates. The primary is tried first; if it's slow or unreachable, the next one in the list is tried automatically. Downloads always come from the mirror that served the manifest — switching primary switches where files come from.
      </p>
      <ul v-if="mirrors.length" class="space-y-2 mb-3">
        <li v-for="(m, i) in mirrors" :key="m.url" class="p-3 bg-white/5 rounded-lg flex items-start gap-3">
          <div class="flex-1 min-w-0">
            <div class="flex items-center gap-2 mb-0.5">
              <p class="text-sm font-medium text-white truncate">{{ m.label || `Mirror ${i + 1}` }}</p>
              <span v-if="i === 0" class="text-[10px] font-mono px-2 py-0.5 rounded bg-green-500/20 text-green-300">PRIMARY</span>
            </div>
            <p class="text-xs text-white/50 font-mono break-all">{{ m.url }}</p>
          </div>
          <div class="shrink-0 flex flex-col gap-1">
            <button
              v-if="i !== 0"
              type="button"
              class="text-xs px-2 py-1 rounded-md text-white/70 hover:bg-white/10 hover:text-white transition-colors"
              title="Make this the primary mirror"
              @click="setPrimaryMirror(m.url)"
            >Set primary</button>
            <button
              v-if="mirrors.length > 1"
              type="button"
              class="text-xs px-2 py-1 rounded-md text-red-300 hover:bg-red-400/10 transition-colors"
              @click="removeMirror(m.url)"
            >Remove</button>
          </div>
        </li>
      </ul>
      <form v-if="addingMirror" class="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-3" @submit.prevent="submitMirror">
        <input
          v-model="mirrorDraft.url"
          type="text"
          placeholder="https://host/.../manifest.json"
          class="sm:col-span-2 px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none"
        />
        <input
          v-model="mirrorDraft.label"
          type="text"
          placeholder="Label (optional)"
          class="px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none"
        />
        <button
          type="submit"
          class="sm:col-span-3 min-h-[40px] glass-button rounded-lg text-sm font-medium disabled:opacity-60"
          :disabled="mirrorSaving || !mirrorDraft.url.trim()"
        >{{ mirrorSaving ? 'Adding…' : 'Add mirror' }}</button>
      </form>
    </div>

    <!-- Actions row -->
    <div class="glass-card p-6">
      <h2 class="text-lg font-semibold text-white mb-4">{{ t('systemUpdate.actions') }}</h2>
      <div class="flex flex-wrap gap-3">
        <button
          @click="checkForUpdates"
          :disabled="loading"
          class="glass-button rounded-lg px-5 py-2 text-sm font-medium disabled:opacity-40"
        >
          {{ loading ? t('systemUpdate.checking') : t('systemUpdate.checkForUpdates') }}
        </button>
        <button
          v-if="rollbackAvailable"
          @click="requestRollback"
          class="glass-button rounded-lg px-5 py-2 text-sm font-medium bg-red-500/10 border-red-400/20"
        >
          {{ t('systemUpdate.rollback') }}
        </button>
        <RouterLink to="/dashboard/settings" class="glass-button rounded-lg px-5 py-2 text-sm font-medium text-center">
          {{ t('systemUpdate.backToSettings') }}
        </RouterLink>
      </div>
    </div>

    <!-- Install progress overlay — covers the UI while the backend
         swaps files, restarts, and comes back up on the new version.
         Auto-reloads the page as soon as /health reports the target
         version. Styled to match the screensaver (ASCII logo, full-
         screen black). -->
    <Teleport to="body">
      <Transition name="fade">
        <div
          v-if="installing"
          class="fixed inset-0 z-[3000] bg-black flex flex-col items-center justify-center overflow-hidden"
        >
          <!-- Centered ASCII logo — same asset used by the screensaver -->
          <div class="install-overlay-ascii">
            <BitcoinFaceAscii />
          </div>

          <!-- Status text + progress bar underneath -->
          <div class="mt-8 w-[min(520px,80vw)] text-center">
            <h2 class="text-xl font-semibold text-white mb-1">
              {{ installStage === 'applying' ? t('systemUpdate.overlayApplying')
               : installStage === 'restarting' ? t('systemUpdate.overlayRestarting')
               : installStage === 'reconnecting' ? t('systemUpdate.overlayReconnecting')
               : installStage === 'ready' ? t('systemUpdate.overlayReady')
               : t('systemUpdate.overlayStalled') }}
            </h2>
            <p v-if="installTargetVersion" class="text-sm text-white/60 mb-4">
              {{ t('systemUpdate.overlayTarget', { version: installTargetVersion }) }}
            </p>

            <!-- Animated bar: indeterminate stripe while working; full
                 orange when ready; steady at 50% (paused look) when
                 stalled so it reads as "something needs the user". -->
            <div class="w-full h-2 bg-white/10 rounded-full overflow-hidden mb-3 relative">
              <div
                v-if="installStage === 'ready'"
                class="absolute inset-0 bg-green-400"
              ></div>
              <div
                v-else-if="installStage === 'stalled'"
                class="absolute inset-y-0 left-0 w-1/2 bg-orange-400/60"
              ></div>
              <div
                v-else
                class="absolute inset-y-0 w-1/3 bg-orange-400 rounded-full install-overlay-bar-anim"
              ></div>
            </div>

            <p class="text-xs text-white/40">{{ installElapsedLabel }}</p>

            <button
              v-if="installStage === 'stalled'"
              @click="reloadNow"
              class="mt-5 glass-button rounded-lg px-5 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30"
            >
              {{ t('systemUpdate.overlayReloadNow') }}
            </button>
          </div>
        </div>
      </Transition>
    </Teleport>

    <!-- Confirmation modal -->
    <Transition name="fade">
      <div v-if="confirmAction" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="cancelConfirm">
        <div class="glass-card p-6 max-w-sm w-full mx-4">
          <h3 class="text-lg font-semibold text-white mb-3">
            {{ confirmAction === 'rollback'
              ? t('systemUpdate.rollbackTitle')
              : confirmAction === 'git-apply'
                ? t('systemUpdate.gitApplyTitle')
                : confirmAction === 'cancel-download'
                  ? t('systemUpdate.cancelDownloadTitle')
                  : t('systemUpdate.applyTitle') }}
          </h3>
          <p class="text-sm text-white/70 mb-6">
            {{ confirmAction === 'rollback'
              ? t('systemUpdate.rollbackMessage')
              : confirmAction === 'git-apply'
                ? t('systemUpdate.gitApplyMessage')
                : confirmAction === 'cancel-download'
                  ? t('systemUpdate.cancelDownloadConfirm')
                  : t('systemUpdate.applyMessage') }}
          </p>
          <div class="flex gap-3 justify-end">
            <button @click="cancelConfirm" class="glass-button rounded-lg px-4 py-2 text-sm font-medium">
              {{ t('common.cancel') }}
            </button>
            <button
              @click="executeConfirm"
              class="glass-button rounded-lg px-4 py-2 text-sm font-medium"
              :class="(confirmAction === 'rollback' || confirmAction === 'cancel-download') ? 'bg-red-500/20 border-red-400/30' : 'bg-orange-500/20 border-orange-400/30'"
            >
              {{ confirmAction === 'rollback'
                ? t('systemUpdate.rollbackButton')
                : confirmAction === 'git-apply'
                  ? t('systemUpdate.pullAndRebuild')
                  : confirmAction === 'cancel-download'
                    ? t('systemUpdate.cancelDownloadButton')
                    : t('systemUpdate.applyNow') }}
            </button>
          </div>
        </div>
      </div>
    </Transition>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterLink } from 'vue-router'
import { rpcClient } from '@/api/rpc-client'
import BitcoinFaceAscii from '@/views/discover/BitcoinFaceAscii.vue'

interface UpdateDetail {
  version: string
  release_date: string
  changelog: string[]
  components: number
}

type ScheduleValue = 'manual' | 'daily_check' | 'auto_apply'

const { t } = useI18n()

const scheduleOptions = computed<{ value: ScheduleValue; label: string; description: string }[]>(() => [
  { value: 'manual', label: t('systemUpdate.manualOnly'), description: t('systemUpdate.manualOnlyDesc') },
  { value: 'daily_check', label: t('systemUpdate.dailyCheck'), description: t('systemUpdate.dailyCheckDesc') },
  { value: 'auto_apply', label: t('systemUpdate.autoApply'), description: t('systemUpdate.autoApplyDesc') },
])

const schedule = ref<ScheduleValue>('daily_check')
const loading = ref(false)
const downloading = ref(false)
const downloaded = ref(false)
const applying = ref(false)
const cancelingDownload = ref(false)
const downloadStalled = ref(false)
const confirmAction = ref<'apply' | 'git-apply' | 'rollback' | 'cancel-download' | null>(null)
const currentVersion = ref('0.0.0')
const lastCheck = ref<string | null>(null)
const updateInfo = ref<UpdateDetail | null>(null)
const updateMethod = ref<'git' | 'manifest' | null>(null)
const rollbackAvailable = ref(false)
const updateInProgress = ref(false)
const statusMessage = ref('')
const statusIsError = ref(false)
const downloadPercent = ref(0)
const downloadPercentFormatted = computed(() => downloadPercent.value.toFixed(2))

// Mirrors — servers this node tries for the manifest, in priority
// order. First entry is the primary. Add/remove/set-primary are wired
// to update.*-mirror RPCs; downloads automatically go to the mirror
// that served the manifest.
interface UpdateMirror { url: string; label: string }
const mirrors = ref<UpdateMirror[]>([])
const addingMirror = ref(false)
const mirrorSaving = ref(false)
const mirrorDraft = reactive({ url: '', label: '' })

async function loadMirrors() {
  try {
    const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({ method: 'update.list-mirrors' })
    mirrors.value = res.mirrors
  } catch (e) {
    if (import.meta.env.DEV) console.warn('update.list-mirrors failed', e)
  }
}

async function submitMirror() {
  const url = mirrorDraft.url.trim()
  if (!url) return
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
    showStatus('Mirror URL must start with http:// or https://', true)
    return
  }
  mirrorSaving.value = true
  try {
    const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({
      method: 'update.add-mirror',
      params: { url, label: mirrorDraft.label.trim() },
    })
    mirrors.value = res.mirrors
    mirrorDraft.url = ''
    mirrorDraft.label = ''
    addingMirror.value = false
    showStatus('Mirror added.')
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e)
    showStatus(`Add mirror failed: ${msg}`, true)
  } finally {
    mirrorSaving.value = false
  }
}

async function removeMirror(url: string) {
  try {
    const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({
      method: 'update.remove-mirror',
      params: { url },
    })
    mirrors.value = res.mirrors
    showStatus('Mirror removed.')
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e)
    showStatus(`Remove failed: ${msg}`, true)
  }
}

async function setPrimaryMirror(url: string) {
  try {
    const res = await rpcClient.call<{ mirrors: UpdateMirror[] }>({
      method: 'update.set-primary-mirror',
      params: { url },
    })
    mirrors.value = res.mirrors
    showStatus('Primary mirror updated. Next update check will try it first.')
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e)
    showStatus(`Set primary failed: ${msg}`, true)
  }
}

// Poll the backend for the real bytes_downloaded / total_bytes so the
// progress bar tracks actual download state (and survives route
// changes). Returns true if a download is currently in progress.
async function pollDownloadProgress(): Promise<boolean> {
  try {
    const res = await rpcClient.call<{
      download_progress?: {
        bytes_downloaded: number
        total_bytes: number
        active: boolean
        stalled?: boolean
      } | null
    }>({ method: 'update.status' })
    const p = res.download_progress
    if (p && p.total_bytes > 0) {
      downloadPercent.value = Math.min(100, (p.bytes_downloaded / p.total_bytes) * 100)
      downloadStalled.value = !!p.stalled
      return p.active
    }
    downloadStalled.value = false
    return false
  } catch {
    return false
  }
}
// Shown next to the progress bar when the fake increment has maxed out
// at 95% but the real RPC hasn't returned yet — lets the user know the
// UI hasn't frozen while SHA verification and disk writes finish.
const downloadFinishing = computed(() => downloading.value && downloadPercent.value >= 95)

// Install overlay state — drives the full-screen progress modal shown
// while the backend swaps files, restarts, and comes back up on the
// new version. The overlay polls /health and auto-reloads the browser
// as soon as the backend reports the target version, so the user
// doesn't need to manually refresh.
type InstallStage = 'applying' | 'restarting' | 'reconnecting' | 'ready' | 'stalled'
const installing = ref(false)
const installStage = ref<InstallStage>('applying')
const installTargetVersion = ref<string | null>(null)
const installStartedAt = ref<number>(0)
const installElapsedSec = ref(0)
let installPollTimer: ReturnType<typeof setInterval> | null = null
let installElapsedTimer: ReturnType<typeof setInterval> | null = null
const installElapsedLabel = computed(() => {
  const s = installElapsedSec.value
  if (s < 60) return `Elapsed: ${s}s`
  return `Elapsed: ${Math.floor(s / 60)}m${s % 60 < 10 ? '0' : ''}${s % 60}s`
})
function startInstallOverlay(targetVersion: string) {
  installing.value = true
  installStage.value = 'applying'
  installTargetVersion.value = targetVersion
  installStartedAt.value = Date.now()
  installElapsedSec.value = 0
  // Tick an elapsed counter once per second for the UI.
  installElapsedTimer = setInterval(() => {
    installElapsedSec.value = Math.floor((Date.now() - installStartedAt.value) / 1000)
    // Stop polling after 3 min — surface the manual reload button.
    if (installElapsedSec.value >= 180 && installStage.value !== 'ready') {
      installStage.value = 'stalled'
    }
  }, 1000)
  // Start polling /health after a short delay — the backend restarts 2s
  // after replying to update.apply, so an immediate poll would see the
  // old backend and conclude nothing happened.
  setTimeout(() => {
    installStage.value = 'restarting'
    installPollTimer = setInterval(pollHealth, 1500)
  }, 2500)
}
async function pollHealth() {
  if (installStage.value === 'ready' || installStage.value === 'stalled') return
  try {
    const res = await fetch('/health', { signal: AbortSignal.timeout(2000) })
    if (!res.ok) throw new Error(`health ${res.status}`)
    const data = await res.json() as { version?: string }
    if (data.version && data.version === installTargetVersion.value) {
      installStage.value = 'ready'
      if (installPollTimer) { clearInterval(installPollTimer); installPollTimer = null }
      // Brief pause so the user sees the "Ready" state before the reload.
      setTimeout(() => { window.location.reload() }, 1200)
    } else {
      // Backend is up but still reporting the old version — frontend
      // and backend are mid-swap. Signal to the user.
      installStage.value = 'reconnecting'
    }
  } catch {
    // Fetch fails while the server is mid-restart. Stay in 'restarting'.
  }
}
function reloadNow() { window.location.reload() }
// Cleanup if the component is torn down mid-install (unlikely but safe).
import { onBeforeUnmount } from 'vue'
onBeforeUnmount(() => {
  if (installPollTimer) clearInterval(installPollTimer)
  if (installElapsedTimer) clearInterval(installElapsedTimer)
})

const lastCheckDisplay = computed(() => {
  if (!lastCheck.value) return t('common.never')
  try {
    const d = new Date(lastCheck.value)
    return d.toLocaleString()
  } catch {
    return lastCheck.value
  }
})

const statusLabel = computed(() => {
  if (applying.value) return t('systemUpdate.applying')
  if (downloading.value) return t('systemUpdate.downloading')
  if (updateInProgress.value) return t('systemUpdate.applying')
  if (updateInfo.value) return t('systemUpdate.updateAvailable')
  if (rollbackAvailable.value) return t('systemUpdate.rollback')
  return t('systemUpdate.upToDate')
})

const statusDotColor = computed(() => {
  if (applying.value || downloading.value) return 'bg-orange-400 animate-pulse'
  if (updateInfo.value || updateInProgress.value) return 'bg-orange-400'
  return 'bg-green-400'
})

const statusTextColor = computed(() => {
  if (applying.value || downloading.value || updateInfo.value || updateInProgress.value) return 'text-orange-400'
  return 'text-green-400'
})

function showStatus(msg: string, isError = false) {
  statusMessage.value = msg
  statusIsError.value = isError
  setTimeout(() => { statusMessage.value = '' }, 8000)
}

async function loadStatus() {
  try {
    const res = await rpcClient.call<{
      current_version: string
      last_check: string | null
      update_available: boolean
      update_in_progress: boolean
      rollback_available: boolean
    }>({ method: 'update.status' })
    currentVersion.value = res.current_version
    lastCheck.value = res.last_check
    updateInProgress.value = res.update_in_progress
    rollbackAvailable.value = res.rollback_available

    if (res.update_in_progress) {
      downloaded.value = true
    }
  } catch (e) {
    if (import.meta.env.DEV) console.warn('Failed to load update status', e)
  }
}

async function checkForUpdates() {
  loading.value = true
  statusMessage.value = ''
  try {
    const res = await rpcClient.call<{
      current_version: string
      last_check: string | null
      update_available: boolean
      update: UpdateDetail | null
      update_method?: string
    }>({ method: 'update.check' })
    currentVersion.value = res.current_version
    lastCheck.value = res.last_check
    updateInfo.value = res.update
    updateMethod.value = res.update_method === 'git' ? 'git' : 'manifest'
    if (!res.update_available) {
      showStatus(t('systemUpdate.upToDateMessage'))
    }
  } catch (e) {
    showStatus(t('systemUpdate.checkFailed'), true)
    if (import.meta.env.DEV) console.warn('Update check failed', e)
  } finally {
    loading.value = false
  }
}

async function downloadUpdate() {
  downloading.value = true
  downloadPercent.value = 0
  statusMessage.value = ''

  // Poll the backend's real byte counter every second instead of
  // faking progress. The backend exposes bytes_downloaded/total_bytes
  // via update.status, updated per chunk. This also means the bar
  // resumes correctly after navigating away and back — no more
  // "95% for some time" mystery.
  const progressInterval = setInterval(() => { void pollDownloadProgress() }, 1000)

  try {
    const res = await rpcClient.call<{
      total_bytes: number
      downloaded_bytes: number
      components_downloaded: number
    }>({ method: 'update.download', timeout: 3_900_000 })
    downloadPercent.value = 100
    downloaded.value = true
    const sizeMB = (res.downloaded_bytes / 1_048_576).toFixed(1)
    showStatus(t('systemUpdate.downloadSuccess', { count: res.components_downloaded, size: sizeMB }))
  } catch (e) {
    showStatus(t('systemUpdate.downloadFailed'), true)
    if (import.meta.env.DEV) console.warn('Download failed', e)
  } finally {
    clearInterval(progressInterval)
    downloading.value = false
  }
}

function requestApply() {
  confirmAction.value = 'apply'
}

function requestGitApply() {
  confirmAction.value = 'git-apply'
}

function requestRollback() {
  confirmAction.value = 'rollback'
}

function requestCancelDownload() {
  confirmAction.value = 'cancel-download'
}

function cancelConfirm() {
  confirmAction.value = null
}

async function executeConfirm() {
  const action = confirmAction.value
  confirmAction.value = null
  if (action === 'apply') {
    await applyUpdate()
  } else if (action === 'git-apply') {
    await applyUpdateGitWithOverlay()
  } else if (action === 'rollback') {
    await rollbackUpdate()
  } else if (action === 'cancel-download') {
    await cancelDownload()
  }
}

async function cancelDownload() {
  cancelingDownload.value = true
  try {
    await rpcClient.call({ method: 'update.cancel-download' })
    downloading.value = false
    downloaded.value = false
    downloadPercent.value = 0
    downloadStalled.value = false
    showStatus(t('systemUpdate.cancelDownloadSuccess'))
  } catch (e) {
    showStatus(t('systemUpdate.cancelDownloadFailed'), true)
    if (import.meta.env.DEV) console.warn('Cancel download failed', e)
  } finally {
    cancelingDownload.value = false
  }
}

async function applyUpdate() {
  applying.value = true
  statusMessage.value = ''
  const target = updateInfo.value?.version || null
  try {
    await rpcClient.call({ method: 'update.apply', timeout: 300_000 })
    // Apply succeeded. Backend scheduled a restart 2s after returning;
    // show the full-screen overlay while we wait for the new backend
    // to report the target version, then auto-reload.
    applying.value = false
    if (target) {
      startInstallOverlay(target)
    } else {
      // No target version known (legacy path) — fall back to the old
      // flash-and-reload behaviour.
      showStatus(t('systemUpdate.applySuccess'))
      setTimeout(() => window.location.reload(), 3000)
    }
  } catch (e) {
    showStatus(t('systemUpdate.applyFailed'), true)
    if (import.meta.env.DEV) console.warn('Apply failed', e)
    applying.value = false
  }
}

async function applyUpdateGitWithOverlay() {
  // Git-apply (dev path) also restarts the service — reuse the overlay
  // so the UX matches the manifest path. Target version isn't known up
  // front for git-apply; we just wait for a version change on /health.
  applying.value = true
  statusMessage.value = ''
  try {
    await rpcClient.call({ method: 'update.git-apply', timeout: 900_000 })
    applying.value = false
    startInstallOverlay(updateInfo.value?.version || currentVersion.value)
  } catch (e) {
    showStatus(t('systemUpdate.applyFailed'), true)
    if (import.meta.env.DEV) console.warn('Git apply failed', e)
    applying.value = false
  }
}

async function rollbackUpdate() {
  try {
    await rpcClient.call({ method: 'update.rollback' })
    showStatus(t('systemUpdate.rollbackSuccess'))
    rollbackAvailable.value = false
    await loadStatus()
  } catch (e) {
    showStatus(t('systemUpdate.rollbackFailed'), true)
    if (import.meta.env.DEV) console.warn('Rollback failed', e)
  }
}

async function loadSchedule() {
  try {
    const res = await rpcClient.call<{ schedule: ScheduleValue }>({ method: 'update.get-schedule' })
    schedule.value = res.schedule
  } catch {
    if (import.meta.env.DEV) console.warn('Failed to load update schedule')
  }
}

async function setSchedule(value: ScheduleValue) {
  schedule.value = value
  try {
    await rpcClient.call({ method: 'update.set-schedule', params: { schedule: value } })
    showStatus(`Schedule set to ${scheduleOptions.value.find(o => o.value === value)?.label}`)
  } catch (e) {
    showStatus('Failed to save schedule', true)
    if (import.meta.env.DEV) console.warn('Set schedule failed', e)
  }
}

onMounted(async () => {
  await Promise.all([loadStatus(), loadSchedule(), loadMirrors(), checkForUpdates()])
  // If a download was already running when the user navigated here
  // (or refreshed), pick up the progress bar where it is and keep
  // polling until the backend reports done. No RPC call to start the
  // download — the backend's already running it.
  const active = await pollDownloadProgress()
  if (active) {
    downloading.value = true
    const resumeInterval = setInterval(async () => {
      const stillActive = await pollDownloadProgress()
      if (!stillActive) {
        clearInterval(resumeInterval)
        downloading.value = false
        downloaded.value = true
      }
    }, 1000)
  }
})
</script>

<style scoped>
/* Centered ASCII logo — clamped so the overlay doesn't blow out on
   narrow viewports. :deep so the rule reaches BitcoinFaceAscii's
   inner <pre>. */
.install-overlay-ascii :deep(pre) {
  font-size: clamp(6px, 1.2vw, 12px);
  line-height: 1.1;
  color: rgba(255, 255, 255, 0.85);
  margin: 0;
}

/* Indeterminate progress stripe that slides left-to-right. */
.install-overlay-bar-anim {
  animation: installBarSlide 1.8s ease-in-out infinite;
}
@keyframes installBarSlide {
  0%   { transform: translateX(-100%); }
  50%  { transform: translateX(120%); }
  100% { transform: translateX(300%); }
}
</style>
