<template>
  <MaintenanceTile
    icon="mdi-brain"
    title="Concept heads (the learning suggester)"
    blurb="Train the per-concept heads that turn your tags into suggestions — they learn from your library and sharpen every time you accept or reject."
    :open="running"
  >
    <p class="fc-muted text-body-2 mb-3">
      A <strong>head</strong> is a tiny classifier trained on the SigLIP
      embeddings already stored on your images — your positives plus your
      negatives (rejections). One is built per general/character concept with at
      least <strong>{{ minPositives }}</strong> tagged images. Retrain after a
      tagging session to fold in your latest accepts/rejects; scoring is live, so
      the rail reflects a retrain on the next image you open.
    </p>

    <!-- Summary stats -->
    <div class="fc-stats mb-3">
      <div class="fc-stat">
        <div class="fc-stat__n">{{ headCount }}</div>
        <div class="fc-stat__l">heads</div>
      </div>
      <div class="fc-stat">
        <div class="fc-stat__n">{{ graduatedCount }}</div>
        <div class="fc-stat__l" title="Heads precise enough to auto-apply without review">auto-apply ready</div>
      </div>
      <div class="fc-stat">
        <div class="fc-stat__n fc-stat__n--time">{{ lastTrained }}</div>
        <div class="fc-stat__l">last trained</div>
      </div>
    </div>

    <v-btn
      v-if="!running"
      color="accent" variant="flat" rounded="pill"
      prepend-icon="mdi-play" :loading="busy" @click="onTrain"
    >{{ headCount > 0 ? 'Retrain heads' : 'Train heads' }}</v-btn>

    <div v-if="running" class="mt-3">
      <v-progress-linear indeterminate color="accent" />
      <div class="text-body-2 mt-2 fc-muted">Training… (started {{ startedAgo }})</div>
    </div>

    <v-alert
      v-if="lastError"
      type="error" variant="tonal" density="compact" class="mt-3"
    >Training failed: {{ lastError }}</v-alert>

    <!-- Empty state -->
    <div v-if="!running && headCount === 0" class="fc-empty mt-4">
      <v-icon size="32" color="accent">mdi-brain</v-icon>
      <p class="fc-muted text-body-2 mt-2 mb-0">
        No heads yet. Tag a handful of images for the concepts you care about,
        then train — each concept with ≥ {{ minPositives }} tags becomes a head.
      </p>
    </div>

    <!-- Per-concept table -->
    <div v-if="heads.length" class="mt-4">
      <div class="fc-muted text-caption mb-2">
        {{ heads.length }} concept{{ heads.length === 1 ? '' : 's' }}, strongest first
        (AP = average precision; auto-apply ⚡ = precise enough to fire without review)
      </div>
      <div class="fc-table-wrap">
        <table class="fc-table">
          <thead>
            <tr>
              <th class="fc-l">Concept</th>
              <th>Cat</th>
              <th class="fc-r" title="Tagged positives the head trained on">+tags</th>
              <th class="fc-r">AP</th>
              <th class="fc-r" title="Precision at the suggest operating point">P</th>
              <th class="fc-r" title="Recall at the suggest operating point">R</th>
              <th class="fc-c">⚡</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="h in heads" :key="h.tag_id">
              <td class="fc-l">{{ h.name }}</td>
              <td><span class="fc-cat">{{ h.category }}</span></td>
              <td class="fc-r fc-mono">{{ h.n_pos }}</td>
              <td class="fc-r fc-mono" :class="apClass(h.ap)">{{ pct(h.ap) }}</td>
              <td class="fc-r fc-mono">{{ pct(h.precision) }}</td>
              <td class="fc-r fc-mono">{{ pct(h.recall) }}</td>
              <td class="fc-c">
                <v-icon v-if="h.auto_apply" size="16" color="success"
                  title="Auto-apply ready">mdi-lightning-bolt</v-icon>
                <span v-else class="fc-muted">—</span>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <!-- Earned auto-apply -->
    <div class="fc-auto mt-6">
      <SettingToggleRow
        v-model="autoEnabled" :loading="settingBusy"
        icon="mdi-lightning-bolt" label="Auto-apply" @change="onToggleAuto"
      />
      <p class="fc-muted text-body-2 mb-3">
        Graduated heads (⚡, with ≥ {{ autoMinPosInput }} examples) apply their tag
        on their own where they clear {{ Math.round((autoPrecisionInput || 0) * 100) }}%
        precision. Every auto-tag is reversible; removing one teaches the head it
        misfired.
      </p>

      <div class="d-flex mb-3" style="gap: 12px;">
        <SettingNumberField
          v-model="autoPrecisionInput" label="Precision target"
          :min="0.5" :max="0.999" :step="0.01" :disabled="settingBusy"
          @change="onSaveSettings"
        />
        <SettingNumberField
          v-model="autoMinPosInput" label="Min examples to fire"
          :min="1" :disabled="settingBusy" @change="onSaveSettings"
        />
      </div>

      <div class="d-flex" style="gap: 8px;">
        <v-btn
          size="small" variant="tonal" color="accent" rounded="pill"
          prepend-icon="mdi-eye-outline" :loading="autoBusy" @click="onPreview"
        >Preview</v-btn>
        <v-btn
          size="small" variant="flat" color="accent" rounded="pill"
          prepend-icon="mdi-lightning-bolt"
          :loading="autoBusy || autoRunning" :disabled="!autoEnabled"
          @click="onApplyNow"
        >Apply now</v-btn>
      </div>

      <div v-if="autoRunning" class="mt-3">
        <v-progress-linear indeterminate color="accent" />
        <div class="text-body-2 mt-2 fc-muted">Sweeping the library…</div>
      </div>

      <div v-if="lastSweep && !autoRunning" class="mt-3">
        <div class="fc-muted text-caption mb-1">
          {{ lastSweep.dry_run ? 'Preview' : 'Applied' }} ·
          {{ formatTime(lastSweep.finished_at) }} ·
          {{ lastSweep.dry_run ? 'would apply' : 'applied' }}
          <strong>{{ sweepTotal(lastSweep) }}</strong>
          tag{{ sweepTotal(lastSweep) === 1 ? '' : 's' }}
        </div>
        <div v-if="sweepConcepts(lastSweep).length" class="fc-chips">
          <span v-for="c in sweepConcepts(lastSweep)" :key="c.tag_id" class="fc-chip">
            {{ c.name }} <strong>{{ c.applied }}</strong>
          </span>
        </div>
      </div>
    </div>

    <!-- Presentation chrome auto-hide (#141) -->
    <div class="fc-auto mt-6">
      <SettingToggleRow
        v-model="presentationEnabled" :loading="settingBusy"
        icon="mdi-image-off-outline" label="Hide presentation chrome"
        @change="onTogglePresentation"
      />
      <p class="fc-muted text-body-2 mb-3">
        Auto-hide <code>banner</code> chrome from the gallery once a head has
        learned it (≥ {{ minPositives }} examples) and clears
        {{ Math.round((presentationThresholdInput || 0) * 100) }}% confidence.
        (<code>wip</code> and <code>editor screenshot</code> are handled by the
        process auto-tagger below.) If a hidden image also looks like real content
        (≥ {{ Math.round((presentationConflictInput || 0) * 100) }}% on a content
        tag), it's flagged for review instead of buried. Every auto-hide is reversible.
      </p>
      <div class="d-flex mb-3" style="gap: 12px;">
        <SettingNumberField
          v-model="presentationThresholdInput" label="Hide confidence"
          :min="0.5" :max="0.999" :step="0.01" :disabled="settingBusy"
          @change="onSavePresentation"
        />
        <SettingNumberField
          v-model="presentationConflictInput" label="Flag if content ≥"
          :min="0" :max="1" :step="0.05" :disabled="settingBusy"
          @change="onSavePresentation"
        />
      </div>
    </div>

    <!-- Process auto-tagging (#1464): wip / editor screenshot -->
    <div class="fc-auto mt-6">
      <SettingToggleRow
        v-model="processEnabled" :loading="settingBusy"
        icon="mdi-progress-wrench" label="Auto-tag work-in-progress"
        @change="onToggleProcess"
      />
      <p class="fc-muted text-body-2 mb-3">
        Auto-tag <code>wip</code> and <code>editor screenshot</code> process art
        once a head has learned them (≥ {{ minPositives }} examples) and clears
        {{ Math.round((processThresholdInput || 0) * 100) }}% confidence. These stay
        <strong>visible</strong> in the gallery — the tag just keeps them out of
        training and the Explore rabbit-hole. Off by default. If a tagged image also
        looks like real content (≥ {{ Math.round((processConflictInput || 0) * 100) }}%
        on a content tag), it's flagged for review. Learns only from your titles +
        manual tags, never its own guesses — so it can't run away. Every tag reversible.
      </p>
      <div class="d-flex mb-3" style="gap: 12px;">
        <SettingNumberField
          v-model="processThresholdInput" label="Tag confidence"
          :min="0.5" :max="0.999" :step="0.01" :disabled="settingBusy"
          @change="onSaveProcess"
        />
        <SettingNumberField
          v-model="processConflictInput" label="Flag if content ≥"
          :min="0" :max="1" :step="0.05" :disabled="settingBusy"
          @change="onSaveProcess"
        />
      </div>
    </div>

    <!-- Performance / tuning -->
    <div v-if="metricsConcepts.length" class="mt-5">
      <div class="fc-section-h mb-1">How auto-apply is landing</div>
      <div class="fc-muted text-caption mb-2">
        Misfire = an auto-tag you removed; missed = a tag you added by hand that a
        head should have caught. Tune the precision target from the misfire rate.
      </div>
      <div class="fc-table-wrap">
        <table class="fc-table">
          <thead>
            <tr>
              <th class="fc-l">Concept</th>
              <th class="fc-r" title="Tags currently auto-applied">applied</th>
              <th class="fc-r" title="Auto-tags you removed">misfires</th>
              <th class="fc-r" title="Removed / (applied + removed)">rate</th>
              <th class="fc-r" title="Tags you added by hand that a head exists for">missed</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="c in metricsConcepts" :key="c.tag_id">
              <td class="fc-l">{{ c.name }}</td>
              <td class="fc-r fc-mono">{{ c.n_auto_applied }}</td>
              <td class="fc-r fc-mono">{{ c.n_misfires }}</td>
              <td class="fc-r fc-mono" :class="rateClass(c.misfire_rate)">
                {{ pct(c.misfire_rate) }}
              </td>
              <td class="fc-r fc-mono">{{ c.n_underfires }}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </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 SettingNumberField from '../common/SettingNumberField.vue'
import SettingToggleRow from '../common/SettingToggleRow.vue'
import { useSettingSave } from '../../composables/useSettingSave.js'
import { useHeadsStore } from '../../stores/heads.js'
import { useMLStore } from '../../stores/ml.js'

const store = useHeadsStore()
const mlSettings = useMLStore()
const summary = ref(null)
const busy = ref(false)
let pollTimer = null

// --- Auto-apply state ---
const autoEnabled = ref(false)
const autoPrecisionInput = ref(0.97)
const autoMinPosInput = ref(30)
// Shared settings-save flow (busy + toast + revert); `settingBusy` gates the
// toggles/fields, `save` returns ok/false for the optimistic-switch revert.
const { busy: settingBusy, save } = useSettingSave(mlSettings.patchSettings)
const autoBusy = ref(false)
const autoStatus = ref(null)
const metricsData = ref(null)
let autoTimer = null

// --- Presentation chrome auto-hide state (#141) ---
const presentationEnabled = ref(true)
const presentationThresholdInput = ref(0.90)
const presentationConflictInput = ref(0.50)
const processEnabled = ref(false)
const processThresholdInput = ref(0.90)
const processConflictInput = ref(0.50)

const autoRunning = computed(() => autoStatus.value?.running_id != null)
const lastSweep = computed(() =>
  (autoStatus.value?.runs || []).find(r => r.status !== 'running') || null)
const metricsConcepts = computed(() => metricsData.value?.concepts ?? [])

const headCount = computed(() => summary.value?.head_count ?? 0)
const graduatedCount = computed(() => summary.value?.graduated_count ?? 0)
const heads = computed(() => summary.value?.heads ?? [])
const running = computed(() => summary.value?.running_id != null)
const minPositives = computed(() => mlSettings.settings?.head_min_positives ?? 8)
const lastTrained = computed(() =>
  summary.value?.last_trained_at ? relTime(summary.value.last_trained_at) : 'never')
// Surface the most recent terminal run's error (if it ended in error).
const lastError = computed(() => {
  const r = (summary.value?.runs || []).find(x => x.status !== 'running')
  return r && r.status === 'error' ? r.error : null
})
const startedAgo = computed(() => {
  const r = (summary.value?.runs || []).find(x => x.status === 'running')
  return r?.started_at ? formatTime(r.started_at) : ''
})

onMounted(async () => {
  // Settings power the copy + the auto-apply tuning inputs.
  try {
    await mlSettings.loadSettings()
    const s = mlSettings.settings || {}
    autoEnabled.value = !!s.head_auto_apply_enabled
    autoPrecisionInput.value = s.head_auto_apply_precision ?? 0.97
    autoMinPosInput.value = s.head_auto_apply_min_positives ?? 30
    presentationEnabled.value = s.presentation_auto_apply_enabled ?? true
    presentationThresholdInput.value = s.presentation_auto_apply_threshold ?? 0.90
    presentationConflictInput.value = s.presentation_conflict_threshold ?? 0.50
    processEnabled.value = s.process_auto_apply_enabled ?? false
    processThresholdInput.value = s.process_auto_apply_threshold ?? 0.90
    processConflictInput.value = s.process_conflict_threshold ?? 0.50
  } catch { /* non-fatal */ }
  await refresh()
  if (running.value) startPoll()
  await refreshAuto()
  if (autoRunning.value) startAutoPoll()
  refreshMetrics()
})
onUnmounted(() => { stopPoll(); stopAutoPoll() })

async function refresh() {
  try {
    summary.value = await store.status()
  } catch { /* non-fatal — the card still offers a fresh train */ }
}

function startPoll() {
  stopPoll()
  pollTimer = setInterval(async () => {
    await refresh()
    if (!running.value) stopPoll()
  }, 5000)
}
function stopPoll() {
  if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
}

async function onTrain() {
  busy.value = true
  try {
    await store.train()
    await refresh()
    startPoll()
  } catch (e) {
    const msg = e.body?.running_id ? 'Training is already running.' : e.message
    toast({ text: `Could not start training: ${msg}`, type: 'error' })
  } finally {
    busy.value = false
  }
}

// --- Auto-apply ---
async function refreshAuto() {
  try { autoStatus.value = await store.autoApplyStatus() } catch { /* non-fatal */ }
}
async function refreshMetrics() {
  try { metricsData.value = await store.metrics() } catch { /* non-fatal */ }
}
function startAutoPoll() {
  stopAutoPoll()
  autoTimer = setInterval(async () => {
    const was = autoRunning.value
    await refreshAuto()
    // Sweep just finished → refresh the counts + landing metrics.
    if (was && !autoRunning.value) { refreshMetrics(); refresh() }
    if (!autoRunning.value) stopAutoPoll()
  }, 4000)
}
function stopAutoPoll() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null } }

async function onToggleAuto(val) {
  const ok = await save({ head_auto_apply_enabled: !!val },
    { successMessage: val ? 'Auto-apply on' : 'Auto-apply off', errorPrefix: 'Could not update' })
  if (!ok) autoEnabled.value = !val   // revert the switch
}
async function onSaveSettings() {
  await save({
    head_auto_apply_precision: Number(autoPrecisionInput.value),
    head_auto_apply_min_positives: Number(autoMinPosInput.value),
  })
}

async function onTogglePresentation(val) {
  const ok = await save({ presentation_auto_apply_enabled: !!val },
    { successMessage: val ? 'Chrome auto-hide on' : 'Chrome auto-hide off', errorPrefix: 'Could not update' })
  if (!ok) presentationEnabled.value = !val   // revert the switch
}
async function onSavePresentation() {
  await save({
    presentation_auto_apply_threshold: Number(presentationThresholdInput.value),
    presentation_conflict_threshold: Number(presentationConflictInput.value),
  })
}

async function onToggleProcess(val) {
  const ok = await save({ process_auto_apply_enabled: !!val },
    { successMessage: val ? 'WIP auto-tag on' : 'WIP auto-tag off', errorPrefix: 'Could not update' })
  if (!ok) processEnabled.value = !val   // revert the switch
}
async function onSaveProcess() {
  await save({
    process_auto_apply_threshold: Number(processThresholdInput.value),
    process_conflict_threshold: Number(processConflictInput.value),
  })
}
function onPreview() { startSweep(true) }
function onApplyNow() { startSweep(false) }
async function startSweep(dryRun) {
  autoBusy.value = true
  try {
    await store.autoApply(dryRun)
    await refreshAuto()
    startAutoPoll()
  } catch (e) {
    const code = e.body?.error
    const msg = code === 'auto_apply_already_running' ? 'A sweep is already running.'
      : code === 'auto_apply_disabled' ? 'Enable auto-apply first.'
      : e.message
    toast({ text: `Could not start sweep: ${msg}`, type: 'error' })
  } finally {
    autoBusy.value = false
  }
}
function sweepConcepts(run) {
  return (run?.report?.concepts || [])
    .filter(c => c.applied > 0)
    .sort((a, b) => b.applied - a.applied)
}
function sweepTotal(run) { return run?.n_applied ?? 0 }
function rateClass(x) {
  if (x == null) return ''
  if (x <= 0.03) return 'fc-good'
  if (x <= 0.1) return 'fc-ok'
  return 'fc-weak'
}

function pct(x) { return x == null ? '—' : `${Math.round(x * 100)}%` }
function apClass(ap) {
  if (ap == null) return ''
  if (ap >= 0.85) return 'fc-good'
  if (ap >= 0.7) return 'fc-ok'
  return 'fc-weak'
}
function formatTime(iso) {
  if (!iso) return ''
  try { return new Date(iso).toLocaleString() } catch { return iso }
}
function relTime(iso) {
  try {
    const d = (Date.now() - new Date(iso).getTime()) / 1000
    if (d < 60) return 'just now'
    if (d < 3600) return `${Math.floor(d / 60)}m ago`
    if (d < 86400) return `${Math.floor(d / 3600)}h ago`
    return `${Math.floor(d / 86400)}d ago`
  } catch { return iso }
}
</script>

<style scoped>
.fc-auto {
  border-top: 1px solid rgb(var(--v-theme-surface-light)); padding-top: 16px;
}
.fc-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.fc-chip {
  font-size: 12px; padding: 2px 8px; border-radius: 999px;
  background: rgb(var(--v-theme-surface-light));
  color: rgb(var(--v-theme-on-surface-variant));
}
.fc-chip strong { color: rgb(var(--v-theme-on-surface)); }

.fc-stats { display: flex; gap: 28px; }
.fc-stat__n {
  font-size: 22px; font-weight: 700; line-height: 1.1;
  color: rgb(var(--v-theme-on-surface));
  font-family: 'JetBrains Mono', monospace;
}
.fc-stat__n--time { font-size: 15px; font-weight: 600; }
.fc-stat__l {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: rgb(var(--v-theme-on-surface-variant));
}

.fc-empty {
  text-align: center; padding: 18px 12px;
  border: 1px dashed rgb(var(--v-theme-surface-light)); border-radius: 8px;
}

.fc-table-wrap {
  max-height: 360px; overflow-y: auto;
  border: 1px solid rgb(var(--v-theme-surface-light)); border-radius: 8px;
}
.fc-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.fc-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: rgb(var(--v-theme-surface));
  text-align: right; padding: 6px 10px; font-weight: 600;
  color: rgb(var(--v-theme-on-surface-variant));
  border-bottom: 1px solid rgb(var(--v-theme-surface-light));
  white-space: nowrap;
}
.fc-table td {
  padding: 5px 10px; text-align: right;
  border-bottom: 1px solid rgba(var(--v-theme-surface-light), 0.5);
}
.fc-table tbody tr:hover { background: rgb(var(--v-theme-surface-light)); }
.fc-l { text-align: left !important; }
.fc-r { text-align: right; }
.fc-c { text-align: center !important; }
.fc-mono { font-family: 'JetBrains Mono', monospace; }
.fc-cat {
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.03em;
  color: rgb(var(--v-theme-on-surface-variant));
  background: rgb(var(--v-theme-surface-light));
  padding: 1px 6px; border-radius: 999px;
}
.fc-ok { color: rgb(var(--v-theme-on-surface)); }
</style>
