<template>
  <MaintenanceTile
    icon="mdi-expansion-card"
    title="GPU agent (CCIP + crops)"
    blurb="Connect a desktop-GPU agent to embed characters (CCIP) and crops. It pulls work over HTTP — your database and Redis stay private."
  >
    <p class="fc-muted text-body-2 mb-3">
      The agent is a container you run on the machine with the GPU. It
      authenticates with the token below, leases jobs from this server, computes
      on the GPU, and posts results back — all over HTTP. Start it when you want
      a burst; stop it to reclaim the card.
    </p>

    <!-- Token -->
    <div class="fc-section-h mb-1">Agent token</div>
    <div v-if="loading" class="fc-muted text-body-2">Loading…</div>
    <template v-else>
      <div v-if="tokenValue" class="fc-token">
        <code class="fc-token__val">{{ masked ? maskedToken : tokenValue }}</code>
        <v-btn
          size="x-small" variant="text" :icon="masked ? 'mdi-eye' : 'mdi-eye-off'"
          :title="masked ? 'Reveal' : 'Hide'" @click="masked = !masked"
        />
        <v-btn
          size="x-small" variant="text" icon="mdi-content-copy"
          title="Copy token" @click="onCopy"
        />
        <v-btn
          size="small" variant="text" color="accent" class="ml-auto"
          prepend-icon="mdi-refresh" :loading="rotating" @click="onRotate"
        >Rotate</v-btn>
      </div>
      <div v-else>
        <v-btn
          color="accent" variant="flat" rounded="pill" size="small"
          prepend-icon="mdi-key-plus" :loading="rotating" @click="onRotate"
        >Generate token</v-btn>
      </div>
      <p class="fc-muted text-caption mt-2 mb-0">
        Point the agent at <code>{{ baseUrl }}</code> with this token. Rotating
        invalidates the old token — update the agent after you rotate.
      </p>
    </template>

    <!-- Queue -->
    <div class="fc-section-h mt-5 mb-2">Work queue</div>
    <div class="fc-queue">
      <div class="fc-q"><div class="fc-q__n">{{ queue.pending }}</div><div class="fc-q__l">pending</div></div>
      <div class="fc-q"><div class="fc-q__n">{{ queue.leased }}</div><div class="fc-q__l">in flight</div></div>
      <div class="fc-q"><div class="fc-q__n fc-good">{{ queue.done }}</div><div class="fc-q__l">done</div></div>
      <div class="fc-q"><div class="fc-q__n" :class="queue.error ? 'fc-weak' : ''">{{ queue.error }}</div><div class="fc-q__l">errored</div></div>
    </div>

    <v-btn
      class="mt-3" color="accent" variant="tonal" rounded="pill" size="small"
      prepend-icon="mdi-restart-alert" :loading="retrying"
      :disabled="!queue.error" @click="onRetryErrors"
    >Retry errored jobs</v-btn>
    <p class="fc-muted text-caption mt-2 mb-0">
      Errored jobs park after 3 failed attempts. This requeues just those (their
      errors cleared, attempts reset) — use after updating the agent, without
      re-running the whole done library.
    </p>

    <v-btn
      class="mt-4" color="accent" variant="tonal" rounded="pill" size="small"
      prepend-icon="mdi-account-box-multiple" :loading="backfilling" @click="onBackfill"
    >Queue character embedding (CCIP)</v-btn>
    <p class="fc-muted text-caption mt-2 mb-0">
      Enqueues every image that doesn't have a CCIP embedding yet. Nothing
      processes until the agent is running.
    </p>

    <v-btn
      class="mt-3" color="accent" variant="tonal" rounded="pill" size="small"
      prepend-icon="mdi-crop" :loading="backfillingSiglip" @click="onBackfillSiglip"
    >Queue concept crops (SigLIP)</v-btn>
    <p class="fc-muted text-caption mt-2 mb-0">
      Enqueues every image that doesn't have concept-crop embeddings yet — the
      localized vectors that help small/local tags (glasses, etc.) surface. New
      images get these automatically; this catches the back-catalogue.
    </p>

    <v-btn
      class="mt-3" color="warning" variant="tonal" rounded="pill" size="small"
      prepend-icon="mdi-backup-restore" :loading="reprocessing" @click="onReprocess"
    >Re-process library (re-detect + re-crop)</v-btn>
    <p class="fc-muted text-caption mt-2 mb-0">
      Re-runs the FULL pipeline (figure detection + CCIP + concept/panel crops) on
      <b>every</b> image — use after changing crop detectors so the back-catalogue
      gets re-cropped, not just new images. Heavy: re-processes the whole library.
    </p>

    <!-- Match strictness -->
    <div class="fc-section-h mt-5 mb-1">Character-match strictness</div>
    <div v-if="ml.settings" class="d-flex align-center" style="gap:12px">
      <v-slider
        v-model="threshold" :min="0.70" :max="0.95" :step="0.01"
        color="accent" hide-details density="compact" class="flex-grow-1"
        :loading="savingThreshold" @end="onSaveThreshold"
      />
      <span class="fc-q__n" style="font-size:16px">{{ threshold.toFixed(2) }}</span>
    </div>
    <p class="fc-muted text-caption mt-1 mb-0">
      How close a figure must be (CCIP cosine) to suggest a character. Higher =
      stricter — fewer but more confident matches. 0.85 recommended; below ~0.80
      a heavily-tagged character starts matching everything.
    </p>

    <!-- Auto-apply -->
    <div v-if="ml.settings" class="d-flex align-center mt-5" style="gap:12px">
      <v-switch
        v-model="autoApply" color="accent" hide-details density="compact"
        :loading="savingAuto" label="Auto-apply confident matches"
        @update:model-value="onSaveAuto"
      />
      <v-text-field
        v-model.number="autoThreshold" type="number" min="0.80" max="0.99"
        step="0.01" density="compact" hide-details variant="outlined"
        style="max-width:96px" :disabled="!autoApply" label="at"
        @change="onSaveAuto"
      />
    </div>
    <p class="fc-muted text-caption mt-1 mb-0">
      When on, a very-confident character match tags the image on its own (daily,
      reversible) — so identity tags keep flowing without review. Stricter than
      the suggest cut; 0.92 recommended.
    </p>

    <!-- Embedding model -->
    <div v-if="ml.settings" class="fc-section-h mt-5 mb-1">Embedding model</div>
    <div v-if="ml.settings">
      <v-select
        v-model="selectedModel" :items="modelItems" item-title="label"
        item-value="name" label="Model" density="compact" hide-details
        variant="outlined"
      />
      <div class="d-flex mt-3" style="gap:8px">
        <v-btn
          size="small" variant="tonal" rounded="pill" :loading="savingModel"
          prepend-icon="mdi-content-save" @click="onSaveModel"
        >Save model</v-btn>
        <v-btn
          size="small" color="accent" variant="flat" rounded="pill"
          :loading="reembedding" prepend-icon="mdi-backup-restore" @click="onReembed"
        >Re-embed library (GPU)</v-btn>
      </div>
      <p class="fc-muted text-caption mt-2 mb-0">
        Switching the model is a DIFFERENT embedding space. After <b>Save model</b>,
        run <b>Re-embed library</b> (the GPU agent re-embeds whole images + concept
        crops), then <b>Retrain heads</b> — suggestions degrade until both finish.
        SigLIP 2 (512px) is a 1152-d drop-in over SigLIP 1; new installs default to
        it. Your existing library stays on its current model until you re-embed.
      </p>
    </div>
  </MaintenanceTile>
</template>

<script setup>
import { toast } from '../../utils/toast.js'
import { computed, onMounted, onUnmounted, ref } from 'vue'

import MaintenanceTile from '../common/MaintenanceTile.vue'
import { useGpuStore } from '../../stores/gpu.js'
import { useMLStore } from '../../stores/ml.js'
import { copyText } from '../../utils/clipboard.js'

const store = useGpuStore()
const ml = useMLStore()
const loading = ref(true)
const tokenValue = ref(null)
const masked = ref(true)
const rotating = ref(false)
const backfilling = ref(false)
const backfillingSiglip = ref(false)
const reprocessing = ref(false)
const retrying = ref(false)
const threshold = ref(0.85)
const savingThreshold = ref(false)
const autoApply = ref(true)
const autoThreshold = ref(0.92)
const savingAuto = ref(false)
const modelItems = ref([])
const selectedModel = ref(null)
const savingModel = ref(false)
const reembedding = ref(false)
const queue = ref({ pending: 0, leased: 0, done: 0, error: 0 })
let pollTimer = null

const baseUrl = computed(() => window.location.origin)
const maskedToken = computed(() => {
  const t = tokenValue.value || ''
  return t.length > 8 ? `${t.slice(0, 4)}••••••••${t.slice(-4)}` : '••••••••'
})

onMounted(async () => {
  try {
    tokenValue.value = (await store.token()).token
  } catch { /* non-fatal */ } finally {
    loading.value = false
  }
  await refreshQueue()
  pollTimer = setInterval(() => { if (!document.hidden) refreshQueue() }, 5000)
  try {
    await ml.loadSettings()
    if (ml.settings?.ccip_match_threshold != null) {
      threshold.value = ml.settings.ccip_match_threshold
    }
    if (ml.settings?.ccip_auto_apply_enabled != null) {
      autoApply.value = ml.settings.ccip_auto_apply_enabled
      autoThreshold.value = ml.settings.ccip_auto_apply_threshold
    }
    if (ml.settings?.embedder_model_name != null) {
      const items = await ml.embedderModels()
      // Make sure the current model is selectable even if it's not in the list.
      const cur = ml.settings.embedder_model_name
      if (!items.some((m) => m.name === cur)) {
        items.push({ name: cur, version: ml.settings.embedder_model_version, label: `${cur} (current)` })
      }
      modelItems.value = items
      selectedModel.value = cur
    }
  } catch { /* non-fatal */ }
})

async function onSaveModel() {
  const opt = modelItems.value.find((m) => m.name === selectedModel.value)
  if (!opt) return
  savingModel.value = true
  try {
    await ml.patchSettings({
      embedder_model_name: opt.name,
      embedder_model_version: opt.version,
    })
    toast({ text: 'Embedding model saved — now Re-embed library, then Retrain heads', type: 'success' })
  } catch (e) {
    toast({ text: `Could not save model: ${e.message}`, type: 'error' })
  } finally {
    savingModel.value = false
  }
}

async function onReembed() {
  reembedding.value = true
  try {
    await store.backfill('embed')
    await store.backfill('siglip')
    toast({ text: 'Queued whole-image + concept re-embed — run the agent, then Retrain heads', type: 'success' })
    await refreshQueue()
  } catch (e) {
    toast({ text: `Could not queue re-embed: ${e.message}`, type: 'error' })
  } finally {
    reembedding.value = false
  }
}

async function onSaveAuto() {
  savingAuto.value = true
  try {
    await ml.patchSettings({
      ccip_auto_apply_enabled: autoApply.value,
      ccip_auto_apply_threshold: autoThreshold.value,
    })
    toast({ text: 'Auto-apply settings saved', type: 'success' })
  } catch (e) {
    toast({ text: `Could not save: ${e.message}`, type: 'error' })
  } finally {
    savingAuto.value = false
  }
}
onUnmounted(() => { if (pollTimer) clearInterval(pollTimer) })

async function onSaveThreshold() {
  savingThreshold.value = true
  try {
    await ml.patchSettings({ ccip_match_threshold: threshold.value })
    toast({ text: `Match strictness set to ${threshold.value.toFixed(2)}`, type: 'success' })
  } catch (e) {
    toast({ text: `Could not save: ${e.message}`, type: 'error' })
  } finally {
    savingThreshold.value = false
  }
}

async function refreshQueue() {
  try { queue.value = await store.status() } catch { /* non-fatal */ }
}

async function onRotate() {
  rotating.value = true
  try {
    tokenValue.value = (await store.rotateToken()).token
    masked.value = false
    toast({ text: 'New agent token generated — update your agent', type: 'success' })
  } catch (e) {
    toast({ text: `Could not rotate token: ${e.message}`, type: 'error' })
  } finally {
    rotating.value = false
  }
}

async function onCopy() {
  try {
    await copyText(tokenValue.value || '')   // resolves on success, throws on fail
    toast({ text: 'Token copied', type: 'success' })
  } catch {
    toast({ text: 'Copy failed — select and copy manually', type: 'warning' })
  }
}

async function onBackfill() {
  backfilling.value = true
  try {
    await store.backfill('ccip')
    toast({ text: 'Queued CCIP embedding — run the agent to process it', type: 'success' })
    await refreshQueue()
  } catch (e) {
    toast({ text: `Could not queue backfill: ${e.message}`, type: 'error' })
  } finally {
    backfilling.value = false
  }
}

async function onBackfillSiglip() {
  backfillingSiglip.value = true
  try {
    await store.backfill('siglip')
    toast({ text: 'Queued concept crops — run the agent to process them', type: 'success' })
    await refreshQueue()
  } catch (e) {
    toast({ text: `Could not queue backfill: ${e.message}`, type: 'error' })
  } finally {
    backfillingSiglip.value = false
  }
}

async function onRetryErrors() {
  retrying.value = true
  try {
    const { requeued, pruned, defects_kept: kept } = await store.retryErrors()
    const extras = []
    if (pruned) extras.push(`${pruned} stale duplicate${pruned === 1 ? '' : 's'} pruned`)
    if (kept) extras.push(`${kept} known-bad file${kept === 1 ? '' : 's'} kept for recovery`)
    const extra = extras.length ? ` (${extras.join(', ')})` : ''
    toast({ text: `Requeued ${requeued} errored job${requeued === 1 ? '' : 's'}${extra} — run the agent to process them`, type: 'success' })
    await refreshQueue()
  } catch (e) {
    toast({ text: `Could not retry errored jobs: ${e.message}`, type: 'error' })
  } finally {
    retrying.value = false
  }
}

async function onReprocess() {
  if (!window.confirm('Re-process the ENTIRE library (re-detect + re-crop every image)? This is heavy and runs on the GPU agent.')) return
  reprocessing.value = true
  try {
    await store.reprocess('ccip')
    toast({ text: 'Library queued for re-processing — run the agent to drain it', type: 'success' })
    await refreshQueue()
  } catch (e) {
    toast({ text: `Could not start re-process: ${e.message}`, type: 'error' })
  } finally {
    reprocessing.value = false
  }
}
</script>

<style scoped>
.fc-token {
  display: flex; align-items: center; gap: 4px;
  background: rgb(var(--v-theme-surface-light)); border-radius: 6px;
  padding: 4px 6px 4px 10px;
}
.fc-token__val {
  font-family: 'JetBrains Mono', monospace; font-size: 13px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fc-queue { display: flex; gap: 24px; }
.fc-q__n {
  font-size: 20px; font-weight: 700; line-height: 1.1;
  font-family: 'JetBrains Mono', monospace;
}
.fc-q__l {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: rgb(var(--v-theme-on-surface-variant));
}
</style>
