<template>
  <v-card v-if="sources.length" variant="tonal" color="error" class="fc-fail mb-4">
    <div class="fc-fail__head" @click="open = !open">
      <v-icon size="small">mdi-alert-circle</v-icon>
      <span class="fc-fail__title">
        {{ sources.length }}
        {{ sources.length === 1 ? 'source is' : 'sources are' }} failing
      </span>
      <v-spacer />
      <v-btn
        size="small" variant="text" prepend-icon="mdi-refresh"
        :loading="retryingAll"
        @click.stop="$emit('retry-all', sources)"
      >
        Retry all
      </v-btn>
      <v-icon size="small">{{ open ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
    </div>

    <v-expand-transition>
      <div v-if="open" class="fc-fail__body">
        <div v-for="s in sources" :key="s.id" class="fc-fail__row">
          <PlatformChip :platform="s.platform" size="x-small" />
          <span class="fc-fail__artist">{{ s.artist_name || `#${s.artist_id}` }}</span>
          <v-chip size="x-small" color="error" variant="flat" label class="fc-fail__count">
            {{ s.consecutive_failures }}× failed
          </v-chip>
          <v-chip
            v-if="s.error_type"
            size="x-small" variant="outlined" label
            :color="errorTypeColor(s.error_type)"
            class="fc-fail__class"
            :title="errorTypeHint(s.error_type)"
          >
            {{ s.error_type }}
          </v-chip>
          <span class="fc-fail__err" :title="s.last_error || ''">
            {{ s.last_error || 'no error message recorded' }}
          </span>
          <v-spacer />
          <v-btn
            size="x-small" variant="text" prepend-icon="mdi-text-box-search-outline"
            :loading="logLoadingIds.has(s.id)"
            @click="onViewLogs(s)"
            title="Show the most recent download event's stdout/stderr/error"
          >
            Logs
          </v-btn>
          <v-btn
            size="x-small" variant="text" prepend-icon="mdi-refresh"
            :loading="retryingIds.has(s.id)"
            @click="$emit('retry', s)"
          >
            Retry
          </v-btn>
        </div>
      </div>
    </v-expand-transition>
  </v-card>
</template>

<script setup>
import { ref } from 'vue'
import PlatformChip from './PlatformChip.vue'

defineProps({
  // source records from /api/sources?failing=true
  sources: { type: Array, default: () => [] },
  retryingIds: { type: Set, default: () => new Set() },
  retryingAll: { type: Boolean, default: false },
})
const emit = defineEmits(['retry', 'retry-all', 'view-logs'])

const open = ref(true)
// Per-row loading flag so the spinner lives on the row whose Logs
// button was clicked, not on every row.
const logLoadingIds = ref(new Set())

// Audit 2026-06-02: surface the ErrorType taxonomy as a colored chip
// next to the consecutive-failures count so operators can bulk-triage
// by error class. Color reflects "what to do next":
//   warning (yellow) — auth/cookie issue: operator should rotate
//   info (blue)      — backend-paced (cooldown / rate limit / timeout)
//   error (red)      — likely terminal without operator intervention
const ERROR_TYPE_COLOR = {
  auth_error: 'warning',
  rate_limited: 'info',
  timeout: 'info',
  network_error: 'info',
  not_found: 'error',
  access_denied: 'error',
  validation_failed: 'error',
  unsupported_url: 'error',
  http_error: 'error',
  unknown_error: 'error',
  api_drift: 'error',
  partial: 'info',
  tier_limited: 'info',
  no_new_content: 'info',
}
const ERROR_TYPE_HINT = {
  auth_error: 'Cookies likely expired — re-upload in Credentials.',
  rate_limited: 'Platform-wide cooldown active. Will retry after it expires.',
  timeout: 'Subprocess exceeded its time budget. Often retries cleanly.',
  network_error: 'Transient network issue. Will retry on next tick.',
  not_found: 'URL 404 — creator may have renamed or deleted.',
  access_denied: 'Subscription tier may not grant this content.',
  validation_failed: 'Downloaded files were quarantined by the validator.',
  http_error: 'Generic HTTP error — see Logs.',
  unsupported_url: 'gallery-dl does not support this URL pattern.',
  unknown_error: 'Could not classify — see Logs.',
  api_drift: 'Patreon changed its API shape — the native ingester needs a code update.',
}
function errorTypeColor(t) { return ERROR_TYPE_COLOR[t] || 'error' }
function errorTypeHint(t) { return ERROR_TYPE_HINT[t] || '' }
async function onViewLogs(s) {
  if (logLoadingIds.value.has(s.id)) return
  logLoadingIds.value = new Set(logLoadingIds.value).add(s.id)
  try {
    await emit('view-logs', s)
  } finally {
    const next = new Set(logLoadingIds.value)
    next.delete(s.id)
    logLoadingIds.value = next
  }
}
</script>

<style scoped>
.fc-fail { border-radius: 8px; }
.fc-fail__head {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px;
  cursor: pointer;
  user-select: none;
}
.fc-fail__title { font-weight: 600; }
.fc-fail__body {
  padding: 0 14px 10px;
  display: flex; flex-direction: column;
  /* Borders, not gap, so the row separators are visible inside the
   * tonal error card (where surface tints get washed out and gap is
   * just empty space). */
}
.fc-fail__row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px;
  border-radius: 4px;
  /* Hover-darken gives the eye a horizontal track from the artist
   * name on the left to the Logs/Retry buttons on the right.
   * Bottom border replaces the previous-too-subtle zebra striping —
   * inside the tonal error card, surface-tint contrast was negligible.
   * Operator-flagged 2026-06-01 (twice). */
  border-bottom: 1px solid rgb(255 255 255 / 0.08);
  transition: background 80ms ease;
}
.fc-fail__row:last-child {
  border-bottom: none;
}
.fc-fail__row:hover {
  background: rgb(0 0 0 / 0.25);
}
.fc-fail__artist { font-weight: 600; white-space: nowrap; }
.fc-fail__count { flex: 0 0 auto; }
.fc-fail__class { flex: 0 0 auto; }
.fc-fail__err {
  color: rgb(var(--v-theme-on-surface-variant));
  font-size: 0.8rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 46ch;
}
</style>
