<template>
  <div v-if="visibleCards.length" class="dc-grid">
    <section
      v-for="card in visibleCards"
      :key="card.id"
      class="dc-card"
      :class="'dc-' + (card.severity || 'info')"
    >
      <header class="dc-head">
        <span class="dc-dot" aria-hidden="true"></span>
        <h3 class="dc-title">{{ card.title }}</h3>
        <span v-if="card.render !== 'metric'" class="dc-count">{{ countOf(card) }}</span>
      </header>

      <!-- metric: the count IS the story -->
      <p v-if="card.render === 'metric'" class="dc-metric">{{ metricValue(card) }}</p>

      <!-- exceptions / list: one line per thing, each linking to itself -->
      <ul v-else class="dc-rows">
        <li v-for="(row, index) in visibleRows(card)" :key="index" class="dc-row"
            :class="{ 'dc-row-stacked': card.layout === 'stacked' }">
          <!-- With coordinates, the name carries the same floor-plan preview
               the asset's own page uses. Without them, a plain link. -->
          <LocationMapTooltip v-if="row.link && row.maphover"
                              :left="row.maphover.x" :top="row.maphover.y" :levelid="row.maphover.levelid"
                              :machineName="row.maphover.label">
            <router-link :to="row.link" class="dc-row-title"
                         :title="row.titletip || undefined">
              {{ row.title }}
            </router-link>
          </LocationMapTooltip>
          <router-link v-else-if="row.link" :to="row.link" class="dc-row-title"
                       :title="row.titletip || undefined">
            {{ row.title }}
          </router-link>
          <span v-else class="dc-row-title dc-row-nolink"
                :title="row.titletip || undefined">{{ row.title }}</span>
          <span v-if="row.detail" class="dc-row-detail"
                :title="row.detailtip || undefined">{{ row.detail }}</span>
          <span v-for="(chip, c) in row.chips" :key="c" class="dc-chip"
                :class="'dc-chip-' + (chip.level || 'low')" :title="chip.title">
            {{ chip.text }}
          </span>
          <span v-if="row.meta.length" class="dc-row-meta">
            {{ row.meta.map((m) => m.text).join(' / ') }}
          </span>
        </li>
      </ul>

      <!-- The overflow line goes somewhere. Telling someone 35 more PCs are
           quiet and leaving them to find the list is worse than not saying it.
           A card without a viewall still says the count, because the number
           itself is information. -->
      <router-link v-if="overflowCount(card) && card.viewall"
                   :to="card.viewall" class="dc-more dc-more-link">
        and {{ overflowCount(card) }} more
      </router-link>
      <p v-else-if="overflowCount(card)" class="dc-more">
        and {{ overflowCount(card) }} more
      </p>
    </section>
  </div>
</template>

<script setup>
// Renders the dashboard cards plugins declare via get_dashboard_widgets
// (GET /api/dashboard/widgets). Core owns the renderers; a plugin declares data
// and shape. See docs/proposals/dashboard-live-fleet.md.
//
// Each card fetches INDEPENDENTLY and a failure is swallowed to null, so one
// slow or broken endpoint - a hung Zabbix call, a plugin mid-upgrade - cannot
// blank the board. A card whose fetch failed is hidden rather than drawn empty,
// because an empty card and a broken card must not look the same.
import { ref, computed, onMounted } from 'vue'
import api from '../api'
import { useAuthStore } from '@/stores/auth'
import LocationMapTooltip from '@/components/LocationMapTooltip.vue'
import {
  toApiPath, visibleRows, overflowCount, metricValue, cardVisible, sortCards,
  permittedCards, renderableCards, rows as cardData,
} from './dashboardCards'

const auth = useAuthStore()
const cards = ref([])

const visibleCards = computed(() => sortCards(cards.value.filter(cardVisible)))

function countOf(card) {
  return cardData(card).length
}

async function load() {
  let declared
  try {
    const response = await api.get('/dashboard/widgets')
    declared = response.data.data || []
  } catch (err) {
    return
  }

  // Filter BEFORE fetching: no point firing a request that would only 403, and
  // the permission gate belongs on both sides regardless.
  const wanted = renderableCards(
    permittedCards(declared, (name) => auth.hasPermission(name)))

  cards.value = await Promise.all(wanted.map(async (card) => {
    try {
      const response = await api.get(toApiPath(card.endpoint))
      return { ...card, _data: response.data.data }
    } catch (err) {
      return { ...card, _data: null }
    }
  }))
}

onMounted(load)
defineExpose({ load })
</script>

<style scoped>
.dc-grid {
  display: grid;
  /* Four across on a wide screen. A card holds a hostname, a machine number
     and a state on one line, so the track cannot go much below this without
     truncating the rows that matter most - but 28rem only fits three once the
     sidebar takes its share at 1920, which is the common case here. auto-fit
     rather than auto-fill so two cards fill the width instead of leaving
     empty tracks. */
  grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
  max-width: 120rem;
  gap: 1rem;
  margin-bottom: 1.75rem;
  align-items: start;
}

/* Four is the ceiling, not a consequence of the track width. Left to auto-fit
   alone this reaches five on an ultrawide, and a fifth column buys nothing -
   the cards get narrower and the rows they hold start truncating again. */
@media (min-width: 96rem) {
  .dc-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}
.dc-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.85rem 1rem 0.9rem;
  /* Nothing escapes the card. A printer row carrying three cartridge readings
     plus a location was pushing past the border: a flex child will not shrink
     below its content width unless told to, so text-overflow never engaged and
     the row simply overflowed. min-width:0 below is what actually enables the
     ellipsis; this is the backstop. */
  overflow: hidden;
  min-width: 0;
}

/* Severity is a small dot beside the title, not a coloured card or a thick
   bar. A card painted by severity reads as an alert even when it holds one
   minor row, and six of them read as a crisis - which is how a board stops
   being read at all. */
.dc-dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; flex: none; }
.dc-critical .dc-dot { background: var(--danger); }
.dc-warning .dc-dot { background: var(--warning); }
.dc-info .dc-dot { background: var(--primary); }

.dc-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--border);
}
.dc-title {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--text-light);
  flex: 1;
}
.dc-count {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.dc-metric { margin: 0.5rem 0 0; font-size: 2rem; font-weight: 600; color: var(--text); }

/* One row per line, not a wrapped paragraph. Each row is title / detail /
   meta on a single line that truncates, so ten rows are ten scannable lines
   rather than a block of text that has to be read. */
.dc-rows { list-style: none; margin: 0; padding: 0; }
.dc-row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
  min-width: 0;
}
.dc-row:last-child { border-bottom: none; }
/* Stacked: a label on one line, prose beneath. Inline, the label is truncated
   to make room for text that is then truncated anyway, and neither reads. */
.dc-row-stacked { flex-direction: column; align-items: stretch; gap: 0.15rem; }
.dc-row-stacked .dc-row-title { max-width: 100%; }
.dc-row-stacked .dc-row-detail { white-space: normal; color: var(--text-light); }
.dc-row-stacked .dc-row-meta { margin-left: 0; padding-left: 0; }
.dc-row-title {
  font-weight: 600;
  color: var(--link);
  text-decoration: none;
  flex: 0 1 auto;
  min-width: 0;
  max-width: 45%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dc-row-title:hover { text-decoration: underline; }
.dc-row-nolink { color: var(--text); }
.dc-row-detail {
  color: var(--text);
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Meta is pushed right and allowed to disappear first: it is the least
   important part of the line, and letting it wrap is what made rows look
   like paragraphs. */
.dc-row-meta {
  margin-left: auto;
  padding-left: 0.5rem;
  color: var(--text-light);
  font-size: 0.8rem;
  white-space: nowrap;
  /* Shrinks and truncates before the title or detail do: it is the least
     important part of the line, and it was the part running off the edge. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* One chip per depleted supply. Bordered rather than filled: a row of solid
   red pills reads as an emergency even when a cartridge is merely low. */
.dc-chip {
  flex: none;
  padding: 0.05rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 0.75rem;
  color: var(--text);
  white-space: nowrap;
  cursor: help;
}
.dc-chip-critical { border-color: var(--danger); color: var(--danger); }
.dc-chip-low { border-color: var(--warning); }

.dc-more {
  display: block;
  margin: 0.5rem 0 0;
  font-size: 0.8rem;
  color: var(--text-light);
}
.dc-more-link { color: var(--link); text-decoration: none; }
.dc-more-link:hover { text-decoration: underline; }
</style>
