<template>
  <div>
    <div class="page-header">
      <h1>Toner Forecast</h1>
      <div class="actions">
        <select v-model.number="days" class="form-control" @change="load">
          <option :value="30">Last 30 days</option>
          <option :value="90">Last 90 days</option>
          <option :value="180">Last 180 days</option>
        </select>
        <router-link to="/reports/toner" class="btn btn-secondary">Toner Report</router-link>
      </div>
    </div>

    <div v-if="loading" class="loading">Reading history...</div>

    <!-- Zabbix off is not the same as nothing running out. Say which. -->
    <div v-else-if="!available" class="card empty-state">
      <p>{{ reason || 'Supply history is unavailable.' }}</p>
      <p class="muted">
        Levels and history both come from Zabbix. With it unreachable there is
        nothing to forecast from - this is not a report of "nothing is due".
      </p>
    </div>

    <template v-else>
      <!-- The answer first. Everything below it is the evidence. -->
      <div class="card order-card">
        <div class="order-head">
          <h2 class="section-title">
            Order now
            <span class="muted">- empty, or out within {{ horizondays }} days</span>
          </h2>
          <button v-if="orderlist.length" class="btn btn-secondary" @click="copyOrder">
            {{ copied ? 'Copied' : 'Copy list' }}
          </button>
        </div>

        <p v-if="!orderlist.length" class="muted nothing-due">
          Nothing is due in the next {{ horizondays }} days.
        </p>
        <ul v-else class="order-list">
          <li v-for="item in mapped" :key="orderKey(item)">
            <span class="quantity">{{ item.quantity }}x</span>
            <span class="partnumber">{{ item.partnumber }}</span>
            <span class="muted order-detail">
              {{ colorLabel(item.color) }}<template v-if="item.model">, {{ item.model }}</template>
            </span>
            <span class="muted order-printers">{{ printerNames(item) }}</span>
          </li>
        </ul>

        <!-- A cartridge nobody has mapped a part to still has to be ordered,
             so it cannot be hidden. It cannot be ordered from this page
             either, so it does not get a line each - it is one job: map them. -->
        <div v-if="unmapped.length" class="unmapped-block">
          <button class="unmapped-head" @click="toggle('unmapped')">
            <span class="band-caret">{{ isOpen('unmapped') ? '-' : '+' }}</span>
            {{ unmappedCount }} more due, with no part number on file
          </button>
          <ul v-if="isOpen('unmapped')" class="order-list">
            <li v-for="item in unmapped" :key="orderKey(item)">
              <span class="quantity">{{ item.quantity }}x</span>
              <span class="muted order-detail">
                {{ colorLabel(item.color) }}<template v-if="item.model">, {{ item.model }}</template>
              </span>
              <span class="muted order-printers">{{ printerNames(item) }}</span>
            </li>
          </ul>
        </div>
      </div>

      <!-- Bands, not a sortable table: the question is "which pile is this in",
           and a pile with nothing in it is worth seeing as empty. -->
      <div v-for="group in bands" :key="group.key" class="card band-card">
        <button class="band-head" @click="toggle(group.key)">
          <span class="band-caret">{{ isOpen(group.key) ? '-' : '+' }}</span>
          <span class="band-title">{{ group.title }}</span>
          <span class="band-count" :class="group.key">{{ group.items.length }}</span>
        </button>

        <div v-if="isOpen(group.key) && group.items.length" class="table-container">
          <table>
            <thead>
              <tr>
                <th class="colcolor"></th>
                <th>Printer</th>
                <th>Supply</th>
                <th>Part</th>
                <th class="collevel">Level</th>
                <th>Runs out</th>
                <th>Rate</th>
                <th title="Cartridge changes detected, over the days of history behind this row">Replacements</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="c in group.items" :key="c.printerid + c.name">
                <td class="colcolor">
                  <span class="chip" :style="chipStyle(c.color)"
                        :title="colorLabel(c.color)"></span>
                </td>
                <td>
                  <router-link :to="`/printers/${c.printerid}`">
                    {{ c.printername || c.assetnumber }}
                  </router-link>
                  <div class="muted small">{{ c.ipaddress }}</div>
                </td>
                <td>{{ c.name }}</td>
                <td>
                  <span v-if="firstPart(c)" class="partnumber">{{ firstPart(c) }}</span>
                  <span v-else class="muted">-</span>
                </td>
                <td class="collevel">
                  <!-- A bar reads at a glance where a percentage has to be
                       compared digit by digit against the row above. -->
                  <span class="bar" :title="levelText(c)">
                    <span class="bar-fill" :class="c.band"
                          :style="{ width: barWidth(c) }"></span>
                  </span>
                  <span class="level-text">{{ levelText(c) }}</span>
                </td>
                <td>
                  <span class="days" :class="c.band">{{ daysText(c) }}</span>
                </td>
                <td class="muted small">
                  <template v-if="c.burnrateperday != null">
                    {{ c.burnrateperday }}%/day<!--
                    --><span v-if="c.rateunstable" class="unstable"
                             title="This cartridge's usage has varied a lot between readings - a burst then a lull, or the reverse. The estimate is the best available, not a measurement.">~</span>
                  </template>
                  <template v-else>-</template>
                </td>
                <td class="muted small">
                  <!-- "2 / 90d" read as a date, a ratio, or a version to
                       everyone who saw it. Say the unit. -->
                  <template v-if="c.basisdays">
                    {{ c.replacements || 0 }} in {{ c.basisdays }}d
                  </template>
                  <template v-else>{{ c.replacements || 0 }}</template>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      <!-- Kept apart rather than sorted in as 0 or as 999: a cartridge with no
           estimate is neither urgent nor safe, and the reason is the point. -->
      <div v-if="unestimated.length" class="card band-card">
        <button class="band-head" @click="toggle('none')">
          <span class="band-caret">{{ isOpen('none') ? '-' : '+' }}</span>
          <span class="band-title">No estimate yet</span>
          <span class="band-count">{{ unestimated.length }}</span>
        </button>
        <div v-if="isOpen('none')" class="table-container">
          <table>
            <thead>
              <tr>
                <th class="colcolor"></th>
                <th>Printer</th><th>Supply</th><th class="collevel">Level</th><th>Why</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="c in unestimated" :key="c.printerid + c.name">
                <td class="colcolor">
                  <span class="chip" :style="chipStyle(c.color)"></span>
                </td>
                <td>
                  <router-link :to="`/printers/${c.printerid}`">
                    {{ c.printername || c.assetnumber }}
                  </router-link>
                </td>
                <td>{{ c.name }}</td>
                <td class="collevel">{{ levelText(c) }}</td>
                <td class="muted">{{ c.reason || '-' }}</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      <p class="muted footnote">
        {{ summary.replacements }} cartridges changed across the fleet in the
        last {{ days }} days.
      </p>
    </template>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { printersApi } from '@/api'
import { apiError } from '@/utils/apiError'

const loading = ref(true)
const available = ref(false)
const reason = ref('')
const days = ref(90)
const horizondays = ref(14)
const cartridges = ref([])
const unestimated = ref([])
const orderlist = ref([])
const summary = ref({ bands: {}, replacements: 0, toorder: 0 })
const copied = ref(false)

// Only what is already empty starts open. The order list above covers the same
// ground in a tenth of the height, so the bands are evidence, asked for.
const openBands = ref(new Set(['empty']))

const mapped = computed(() => orderlist.value.filter(item => item.partnumber))
const unmapped = computed(() => orderlist.value.filter(item => !item.partnumber))
const unmappedCount = computed(() =>
  unmapped.value.reduce((total, item) => total + item.quantity, 0))

const BAND_TITLES = {
  empty: 'Empty - order now',
  soon: 'Within 2 weeks',
  month: 'Within 30 days',
  later: 'Later',
}

const bands = computed(() =>
  Object.keys(BAND_TITLES).map(key => ({
    key,
    title: BAND_TITLES[key],
    items: cartridges.value.filter(c => c.band === key),
  })))

function isOpen(key) {
  return openBands.value.has(key)
}

function toggle(key) {
  const next = new Set(openBands.value)
  next.has(key) ? next.delete(key) : next.add(key)
  openBands.value = next
}

// Toner colours are the one place a literal colour is the data rather than a
// theme choice - a cyan chip has to be cyan in both light and dark mode.
const CHIP_COLORS = {
  black: '#2b2b2b', cyan: '#00b7eb', magenta: '#d6006e', yellow: '#f2c200',
}

function chipStyle(color) {
  const known = CHIP_COLORS[(color || '').toLowerCase()]
  return known
    ? { background: known }
    : { background: 'var(--bg)', border: '1px solid var(--border)' }
}

function colorLabel(color) {
  if (!color || color === 'none') return 'Supply'
  return color.charAt(0).toUpperCase() + color.slice(1)
}

function firstPart(cartridge) {
  const parts = cartridge.partnumbers || []
  return parts.length ? parts[0].partnumber : null
}

function levelText(cartridge) {
  return cartridge.currentlevel != null
    ? Math.round(cartridge.currentlevel) + '%'
    : '-'
}

function barWidth(cartridge) {
  const level = cartridge.currentlevel
  if (level == null) return '0%'
  return Math.max(0, Math.min(100, level)) + '%'
}

function daysText(cartridge) {
  if (cartridge.daysleft == null) return '-'
  if (cartridge.daysleft === 0) return 'empty'
  if (cartridge.daysleft === 1) return '1 day'
  return cartridge.daysleft + ' days'
}

function orderKey(item) {
  return [item.partnumber || 'unmapped', item.color, item.model].join('|')
}

// Enough to recognise which printers are meant, without a line of hostnames
// wrapping over the quantity that is the point of the row.
const NAMES_SHOWN = 3

function printerNames(item) {
  const names = (item.printers || []).map(p => p.printername).filter(Boolean)
  if (names.length <= NAMES_SHOWN) return names.join(', ')
  return `${names.slice(0, NAMES_SHOWN).join(', ')} +${names.length - NAMES_SHOWN} more`
}

// Purchasing wants it as text in a mail, not as a screenshot of a table.
async function copyOrder() {
  const lines = orderlist.value.map(item => {
    const part = item.partnumber || 'NO PART NUMBER ON FILE'
    const where = printerNames(item)
    return `${item.quantity}x ${part} (${colorLabel(item.color)}` +
      `${item.model ? ', ' + item.model : ''}) - ${where}`
  })
  try {
    await navigator.clipboard.writeText(lines.join('\n'))
    copied.value = true
    setTimeout(() => { copied.value = false }, 2000)
  } catch {
    copied.value = false
  }
}

async function load() {
  loading.value = true
  try {
    const response = await printersApi.supplyForecast(days.value)
    const data = response.data.data
    available.value = data.available
    reason.value = data.reason || ''
    cartridges.value = data.cartridges || []
    unestimated.value = data.unestimated || []
    orderlist.value = data.orderlist || []
    horizondays.value = data.horizondays || 14
    summary.value = data.summary || summary.value
  } catch (err) {
    available.value = false
    reason.value = apiError(err, 'Failed to read supply history')
  } finally {
    loading.value = false
  }
}

onMounted(load)
</script>

<style scoped>
.actions { display: flex; gap: 0.5rem; align-items: center; }
.actions .form-control { width: auto; }

.order-card { padding: 0 0 0.5rem; }
.order-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0 1rem;
}
.order-head .section-title { padding: 0.75rem 0; }
.nothing-due { padding: 0 1rem 0.75rem; }
.order-list { list-style: none; margin: 0; padding: 0 1rem; }
.order-list li {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.4rem 0;
  border-top: 1px solid var(--border);
}
.quantity { font-weight: 700; min-width: 2.5rem; }
.unmapped-block { border-top: 1px solid var(--border); }
.unmapped-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.5rem 1rem;
  background: none;
  border: none;
  color: var(--warning);
  font-size: 0.9rem;
  cursor: pointer;
  text-align: left;
}
.unmapped-block .order-list li { border-top: 1px dashed var(--border); }
.partnumber { font-family: monospace; font-weight: 700; }
.partnumber.unmapped { font-weight: 400; color: var(--warning); }
.order-detail { font-size: 0.9rem; }
.order-printers { font-size: 0.8rem; margin-left: auto; text-align: right; }

.band-card { padding: 0; margin-bottom: 1rem; }
.band-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.75rem 1rem;
  background: none;
  border: none;
  color: var(--text);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  text-align: left;
}
.band-caret { width: 1rem; color: var(--text-light); }
.band-title { flex: 1; }
.band-count {
  min-width: 1.8rem;
  padding: 0.1rem 0.5rem;
  border-radius: 1rem;
  background: var(--bg);
  border: 1px solid var(--border);
  text-align: center;
  font-size: 0.85rem;
}
.band-count.empty { background: var(--danger); border-color: var(--danger); color: #fff; }
.band-count.soon { background: var(--warning); border-color: var(--warning); color: #fff; }

.colcolor { width: 1.5rem; }
.chip {
  display: inline-block;
  width: 0.9rem;
  height: 0.9rem;
  border-radius: 50%;
  vertical-align: middle;
}
.collevel { width: 9rem; white-space: nowrap; }
.bar {
  display: inline-block;
  width: 5rem;
  height: 0.6rem;
  border-radius: 0.3rem;
  background: var(--bg);
  border: 1px solid var(--border);
  overflow: hidden;
  vertical-align: middle;
}
.bar-fill { display: block; height: 100%; background: var(--success); }
.bar-fill.empty { background: var(--danger); }
.bar-fill.soon { background: var(--warning); }
.level-text { margin-left: 0.4rem; font-size: 0.85rem; }

.days { font-weight: 700; }
.days.empty { color: var(--danger); }
.days.soon { color: var(--warning); }
.small { font-size: 0.8rem; }

/* Marks a rate the intervals do not agree on. Deliberately quiet - it qualifies
   the number beside it rather than competing with the urgency bands. */
.unstable { margin-left: 2px; font-weight: 600; cursor: help; }
.empty-state { padding: 2rem; text-align: center; }
.footnote { margin-top: 1rem; font-size: 0.85rem; }
</style>
