<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."
    :open="true"
  >
    <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-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>

    <!-- 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>
  </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 threshold = ref(0.85)
const savingThreshold = ref(false)
const autoApply = ref(true)
const autoThreshold = ref(0.92)
const savingAuto = 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
    }
  } catch { /* non-fatal */ }
})

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
  }
}
</script>

<style scoped>
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
.fc-section-h {
  font-size: 13px; font-weight: 700; letter-spacing: 0.03em;
  text-transform: uppercase; color: rgb(var(--v-theme-on-surface));
}
.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));
}
.fc-good { color: rgb(var(--v-theme-success)); }
.fc-weak { color: rgb(var(--v-theme-error)); }
</style>
