<template>
  <div>
    <SchedulerStatusBar :status="store.scheduleStatus" class="fc-subs__sched" />

    <div class="fc-subs__bar">
      <v-btn color="accent" prepend-icon="mdi-plus" @click="openAddSource(null)">
        Add subscription
      </v-btn>
      <v-btn variant="outlined" prepend-icon="mdi-account-plus" @click="showArtistDialog = true">
        New artist
      </v-btn>
      <v-chip
        :color="needsAttention ? 'error' : undefined"
        :variant="needsAttention ? 'flat' : 'outlined'"
        prepend-icon="mdi-alert-circle-outline"
        @click="needsAttention = !needsAttention"
      >
        Needs attention
        <v-chip v-if="attentionCount" size="x-small" class="ms-2" variant="tonal">
          {{ attentionCount }}
        </v-chip>
      </v-chip>
      <v-spacer />
      <v-select
        v-model="statusFilter"
        :items="STATUS_OPTIONS"
        :disabled="needsAttention"
        density="compact" variant="outlined" hide-details
        class="fc-subs__status"
      />
      <v-text-field
        v-model="search"
        density="compact" variant="outlined" hide-details clearable
        prepend-inner-icon="mdi-magnify"
        placeholder="Search subscriptions"
        class="fc-subs__search"
      />
    </div>

    <v-slide-y-transition>
      <v-card
        v-if="selected.length"
        variant="tonal" color="info"
        class="fc-subs__bulk mb-3"
      >
        <v-card-text class="d-flex align-center pa-3 ga-3">
          <span class="text-body-2">
            {{ selected.length }} selected
          </span>
          <v-spacer />
          <v-btn size="small" variant="text" prepend-icon="mdi-toggle-switch" @click="bulkSetEnabled(true)">
            Enable all
          </v-btn>
          <v-btn size="small" variant="text" prepend-icon="mdi-toggle-switch-off-outline" @click="bulkSetEnabled(false)">
            Disable all
          </v-btn>
          <v-btn size="small" variant="text" prepend-icon="mdi-magnify-scan" @click="bulkBackfill">
            Backfill
          </v-btn>
          <v-btn size="small" variant="text" color="error" prepend-icon="mdi-delete" @click="bulkDelete">
            Delete
          </v-btn>
          <v-btn size="small" variant="text" @click="selected = []">Clear</v-btn>
        </v-card-text>
      </v-card>
    </v-slide-y-transition>

    <v-alert v-if="store.error" type="error" variant="tonal" closable class="mb-3">
      {{ String(store.error) }}
    </v-alert>

    <div v-if="store.loading && groups.length === 0" class="fc-subs__loading">
      <v-progress-circular indeterminate color="accent" size="36" />
    </div>

    <div v-else-if="filteredGroups.length === 0" class="fc-subs__empty">
      <p v-if="groups.length === 0">No subscriptions yet. Add your first artist.</p>
      <p v-else>No subscriptions match the current filter.</p>
    </div>

    <v-card v-else-if="!isMobile" class="fc-subs__card" variant="outlined">
      <v-data-table
        :headers="headers"
        :items="filteredGroups"
        item-value="key"
        v-model="selected"
        v-model:expanded="expanded"
        v-model:sort-by="sortBy"
        :items-per-page="50"
        :items-per-page-options="ITEMS_PER_PAGE_OPTIONS"
        density="comfortable"
        hover show-select show-expand
        @click:row="onRowClick"
      >
        <template #item.name="{ item }">
          <div class="fc-subs__name">{{ item.artist.name }}</div>
          <!-- #2: single-source subscriptions show their URL inline (no expand). -->
          <a
            v-if="item.singleSource"
            :href="item.singleSource.url" target="_blank" rel="noopener"
            class="fc-subs__sub-url" @click.stop
          >{{ item.singleSource.url }}</a>
        </template>

        <template #item.platforms="{ item }">
          <div class="fc-subs__chips">
            <PlatformChip
              v-for="p in item.platforms" :key="p"
              :platform="p" size="x-small"
            />
          </div>
        </template>

        <template #item.sources_count="{ item }">
          <v-chip size="x-small" variant="tonal" label>
            {{ item.sources.length }}
          </v-chip>
        </template>

        <template #item.health="{ item }">
          <SourceHealthDot
            v-if="item.worstSource"
            :source="item.worstSource"
            :warning-threshold="failureThreshold"
          />
          <span v-else class="fc-subs__zero">—</span>
        </template>

        <template #item.last_activity="{ item }">
          <span class="fc-subs__when">{{ formatRelative(item.lastActivity) }}</span>
        </template>

        <template #item.actions="{ item }">
          <!-- #2: single source → its own actions inline (Check / Backfill / ⋮),
               plus Edit, so the row is actionable without expanding. -->
          <template v-if="item.singleSource">
            <SourceActions
              :source="item.singleSource"
              :checking="store.checkingIds.has(item.singleSource.id)"
              @check="onCheck" @backfill="onBackfill"
              @recover="onRecover" @remove="removeSource"
            />
            <v-btn icon size="small" variant="text" @click.stop="openEditSource(item.singleSource)">
              <v-icon>mdi-pencil</v-icon>
              <v-tooltip activator="parent" location="top">Edit source</v-tooltip>
            </v-btn>
            <v-btn icon size="small" variant="text" @click.stop="openAddSource(item.artist)">
              <v-icon>mdi-plus</v-icon>
              <v-tooltip activator="parent" location="top">Add another source</v-tooltip>
            </v-btn>
            <v-btn
              icon size="small" variant="text"
              :to="`/artist/${item.artist.slug}`" @click.stop
            >
              <v-icon>mdi-account</v-icon>
              <v-tooltip activator="parent" location="top">Open artist page</v-tooltip>
            </v-btn>
          </template>
          <!-- Multi-source → artist-level actions; expand for per-source rows. -->
          <template v-else>
            <v-btn
              icon size="small" variant="text"
              :loading="anyChecking(item.sources)"
              @click.stop="checkAll(item)"
            >
              <v-icon>mdi-refresh</v-icon>
              <v-tooltip activator="parent" location="top">Check all sources</v-tooltip>
            </v-btn>
            <v-btn icon size="small" variant="text" @click.stop="openAddSource(item.artist)">
              <v-icon>mdi-plus</v-icon>
              <v-tooltip activator="parent" location="top">Add source</v-tooltip>
            </v-btn>
            <v-btn
              icon size="small" variant="text"
              :to="`/posts?artist_id=${item.artist.id}`" @click.stop
            >
              <v-icon>mdi-rss</v-icon>
              <v-tooltip activator="parent" location="top">View posts</v-tooltip>
            </v-btn>
            <v-btn
              icon size="small" variant="text"
              :to="`/artist/${item.artist.slug}`" @click.stop
            >
              <v-icon>mdi-account</v-icon>
              <v-tooltip activator="parent" location="top">Open artist page</v-tooltip>
            </v-btn>
          </template>
        </template>

        <template #expanded-row="{ columns, item }">
          <tr class="fc-subs__sources-row">
            <td :colspan="columns.length" class="fc-subs__sources-cell">
              <v-table density="compact" class="fc-subs__sources-table">
                <thead>
                  <tr>
                    <th></th>
                    <th>Platform</th>
                    <th>URL</th>
                    <th>Enabled</th>
                    <th>Last check</th>
                    <th>Next check</th>
                    <th>Errors</th>
                    <th class="text-right">Actions</th>
                  </tr>
                </thead>
                <tbody>
                  <SourceRow
                    v-for="s in item.sources" :key="s.id" :source="s"
                    :checking="store.checkingIds.has(s.id)"
                    :warning-threshold="failureThreshold"
                    @edit="openEditSource"
                    @remove="removeSource"
                    @toggle="toggleSourceEnabled"
                    @check="onCheck"
                    @backfill="onBackfill"
                    @recover="onRecover"
                  />
                  <tr v-if="item.sources.length === 0">
                    <td colspan="8" class="fc-subs__sources-empty">
                      No sources yet. Click + to add one.
                    </td>
                  </tr>
                </tbody>
              </v-table>
            </td>
          </tr>
        </template>
      </v-data-table>
    </v-card>

    <!-- Mobile: compact cards (several fit per screen), expanding to STACKED
         source cards so the wide source columns never force lateral scroll. -->
    <div v-else class="fc-subs__mlist">
      <div
        v-for="item in filteredGroups" :key="item.key"
        class="fc-subs__mcard"
      >
        <div class="fc-subs__mhead" @click="toggleExpand(item)">
          <v-checkbox-btn
            :model-value="isSelected(item)" density="compact" hide-details
            @click.stop @update:model-value="toggleSelect(item)"
          />
          <span class="fc-subs__name">{{ item.artist.name }}</span>
          <v-spacer />
          <SourceHealthDot
            v-if="item.worstSource"
            :source="item.worstSource" :warning-threshold="failureThreshold"
          />
          <v-icon size="small">
            {{ isExpanded(item) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
          </v-icon>
        </div>
        <div class="fc-subs__mmeta">
          <PlatformChip
            v-for="p in item.platforms" :key="p" :platform="p" size="x-small"
          />
          <span class="fc-subs__mmeta-text">
            {{ item.sources.length }} src · {{ formatRelative(item.lastActivity) }}
          </span>
        </div>

        <div v-if="isExpanded(item)" class="fc-subs__mbody">
          <div class="fc-subs__mactions">
            <v-btn
              size="small" variant="text" :loading="anyChecking(item.sources)"
              @click="checkAll(item)"
            >
              <v-icon>mdi-refresh</v-icon>
              <v-tooltip activator="parent" location="top">Check all</v-tooltip>
            </v-btn>
            <v-btn size="small" variant="text" @click="openAddSource(item.artist)">
              <v-icon>mdi-plus</v-icon>
              <v-tooltip activator="parent" location="top">Add source</v-tooltip>
            </v-btn>
            <v-btn size="small" variant="text" :to="`/posts?artist_id=${item.artist.id}`">
              <v-icon>mdi-rss</v-icon>
              <v-tooltip activator="parent" location="top">View posts</v-tooltip>
            </v-btn>
            <v-btn size="small" variant="text" :to="`/artist/${item.artist.slug}`">
              <v-icon>mdi-account</v-icon>
              <v-tooltip activator="parent" location="top">Artist page</v-tooltip>
            </v-btn>
          </div>
          <SourceCard
            v-for="s in item.sources" :key="s.id" :source="s"
            :checking="store.checkingIds.has(s.id)"
            :warning-threshold="failureThreshold"
            @edit="openEditSource"
            @remove="removeSource"
            @toggle="toggleSourceEnabled"
            @check="onCheck"
            @backfill="onBackfill"
            @recover="onRecover"
          />
          <div v-if="item.sources.length === 0" class="fc-subs__sources-empty">
            No sources yet. Tap + to add one.
          </div>
        </div>
      </div>
    </div>

    <SourceFormDialog
      v-model="showSourceDialog"
      :source="editingSource"
      :initial-artist="editingArtist"
      @saved="onSourceSaved"
    />
    <ArtistCreateDialog v-model="showArtistDialog" @created="onArtistCreated" />
  </div>
</template>

<script setup>
import { toast } from '../../utils/toast.js'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useDisplay } from 'vuetify'
import { useRoute, useRouter } from 'vue-router'
import { useSourcesStore } from '../../stores/sources.js'
import { usePlatformsStore } from '../../stores/platforms.js'
import { useImportStore } from '../../stores/import.js'
import SourceRow from './SourceRow.vue'
import SourceCard from './SourceCard.vue'
import SourceHealthDot from './SourceHealthDot.vue'
import SourceFormDialog from './SourceFormDialog.vue'
import ArtistCreateDialog from './ArtistCreateDialog.vue'
import SourceActions from './SourceActions.vue'
import PlatformChip from './PlatformChip.vue'
import SchedulerStatusBar from './SchedulerStatusBar.vue'
import { formatRelative } from '../../utils/date.js'

const ITEMS_PER_PAGE_OPTIONS = [
  { value: 25, title: '25' },
  { value: 50, title: '50' },
  { value: 100, title: '100' },
  { value: -1, title: 'All' },
]
const STATUS_OPTIONS = [
  { title: 'All status', value: 'all' },
  { title: 'Enabled',    value: 'enabled' },
  { title: 'Disabled',   value: 'disabled' },
  { title: 'Has errors', value: 'errors' },
  { title: 'Stale',      value: 'stale' },
]

const route = useRoute()
const router = useRouter()
const display = useDisplay()
// Match the 600px breakpoint the rest of the hub uses; below it the table is
// replaced by the custom compact-card list.
const isMobile = computed(() => display.width.value < 600)
const store = useSourcesStore()
const platformsStore = usePlatformsStore()
const importStore = useImportStore()

const search = ref('')
const statusFilter = ref('all')
const needsAttention = ref(false)
const expanded = ref([])
const selected = ref([])
// #3: default to worst-health first, name as the tiebreak (the table header
// stays clickable to re-sort by any column).
const sortBy = ref([
  { key: 'health', order: 'desc' },
  { key: 'name', order: 'asc' },
])

// Mobile card list drives the same `selected`/`expanded` key arrays the
// desktop v-data-table binds, so selection + bulk actions work identically.
function _toggleKey(arr, key) {
  const i = arr.value.indexOf(key)
  if (i === -1) arr.value = [...arr.value, key]
  else arr.value = arr.value.filter((k) => k !== key)
}
function isSelected(item) { return selected.value.includes(item.key) }
function toggleSelect(item) { _toggleKey(selected, item.key) }
function isExpanded(item) { return expanded.value.includes(item.key) }
function toggleExpand(item) { _toggleKey(expanded, item.key) }

const showSourceDialog = ref(false)
const editingSource = ref(null)
const editingArtist = ref(null)
const showArtistDialog = ref(false)

const artistFilter = computed(() => {
  const raw = route.query.artist_id
  return raw == null ? null : Number(raw)
})

const failureThreshold = computed(() =>
  importStore.settings?.download_failure_warning_threshold ?? 5,
)

async function refresh() {
  await store.loadAll()
  await platformsStore.loadAll()
  store.loadScheduleStatus()
  if (!importStore.settings) await importStore.loadSettings()
}

// Re-poll scheduler status every 30s so the "last ran" reading + health
// dot stay honest (the Beat tick fires every 60s).
let schedId = null
onMounted(() => {
  refresh()
  if (artistFilter.value != null) {
    expanded.value = [`artist-${artistFilter.value}`]
  }
  schedId = setInterval(() => {
    if (!document.hidden) store.loadScheduleStatus()
  }, 30000)
})
onUnmounted(() => {
  if (schedId) { clearInterval(schedId); schedId = null }
})
watch(() => route.query.artist_id, refresh)

const headers = [
  { title: 'Subscription', key: 'name',          sortable: true,  align: 'start' },
  { title: 'Platforms',    key: 'platforms',     sortable: false, align: 'start', width: 240 },
  { title: 'Sources',      key: 'sources_count', sortable: true,  align: 'start', width: 90 },
  { title: 'Health',       key: 'health',        sortable: true,  align: 'start', width: 80 },
  { title: 'Last activity',key: 'last_activity', sortable: true,  align: 'start', width: 140 },
  { title: 'Actions',      key: 'actions',       sortable: false, align: 'end',   width: 200 },
]

const groups = computed(() => {
  const all = store.sourcesByArtistGrouped()
  return all.map((g) => {
    const worstSource = pickWorstSource(g.sources, failureThreshold.value)
    const lastActivity = pickLastActivity(g.sources)
    const platforms = [...new Set(g.sources.map((s) => s.platform).filter(Boolean))]
    return {
      key: `artist-${g.artist.id}`,
      artist: g.artist,
      sources: g.sources,
      sources_count: g.sources.length,
      platforms,
      worstSource,
      lastActivity,
      name: g.artist.name,
      last_activity: lastActivity ?? '',
      // #3: numeric health rank so the Health column is sortable worst-first.
      health: healthRank(worstSource, failureThreshold.value),
      // #2: when a subscription has exactly one source, surface it on the
      // artist row directly (URL + actions) so the common case needs no expand.
      singleSource: g.sources.length === 1 ? g.sources[0] : null,
    }
  })
})

// 0 never-checked · 1 ok · 2 warning · 3 failing — higher = worse, so a
// descending sort floats the broken subscriptions to the top.
function healthRank(source, threshold) {
  if (!source) return 0
  if (!source.last_checked_at) return 0
  const f = source.consecutive_failures || 0
  if (f === 0) return 1
  if (f < threshold) return 2
  return 3
}

// A group "needs attention" if any of its sources has accumulated
// failures OR has never been checked — the ones worth acting on.
function groupNeedsAttention(g) {
  return groupMatchesStatus(g, 'errors') || groupMatchesStatus(g, 'stale')
}
const attentionCount = computed(
  () => groups.value.filter(groupNeedsAttention).length,
)

const filteredGroups = computed(() => {
  let arr = groups.value
  if (artistFilter.value != null) {
    arr = arr.filter((g) => g.artist.id === artistFilter.value)
  }
  if (needsAttention.value) {
    arr = arr.filter(groupNeedsAttention)
  } else if (statusFilter.value !== 'all') {
    arr = arr.filter((g) => groupMatchesStatus(g, statusFilter.value))
  }
  const q = search.value?.trim().toLowerCase()
  if (q) {
    arr = arr.filter(
      (g) =>
        g.artist.name.toLowerCase().includes(q) ||
        g.sources.some(
          (s) =>
            (s.url || '').toLowerCase().includes(q) ||
            (s.platform || '').toLowerCase().includes(q),
        ),
    )
  }
  return arr
})

function groupMatchesStatus(g, status) {
  if (status === 'enabled') return g.sources.some((s) => s.enabled)
  if (status === 'disabled') return g.sources.every((s) => !s.enabled)
  if (status === 'errors') return g.sources.some((s) => (s.consecutive_failures || 0) > 0)
  if (status === 'stale') return g.sources.some((s) => !s.last_checked_at)
  return true
}

function pickLastActivity(sources) {
  let max = null
  for (const s of sources) {
    if (s.last_checked_at && (!max || s.last_checked_at > max)) max = s.last_checked_at
  }
  return max
}

function pickWorstSource(sources, threshold) {
  if (!sources || sources.length === 0) return null
  function level(s) {
    if (!s.last_checked_at) return 0
    const f = s.consecutive_failures || 0
    if (f === 0) return 1
    if (f < threshold) return 2
    return 3
  }
  return sources.reduce((worst, s) => (level(s) > level(worst) ? s : worst), sources[0])
}

function onRowClick(_evt, { item }) {
  const key = item.key
  const idx = expanded.value.indexOf(key)
  if (idx === -1) expanded.value = [...expanded.value, key]
  else expanded.value = expanded.value.filter((k) => k !== key)
}

function openAddSource(artist) {
  editingSource.value = null
  editingArtist.value = artist
  showSourceDialog.value = true
}

function openEditSource(source) {
  editingSource.value = source
  editingArtist.value = {
    id: source.artist_id, name: source.artist_name, slug: source.artist_slug,
  }
  showSourceDialog.value = true
}

async function removeSource(source) {
  // Confirm — Remove moved into the overflow menu, but a destructive action
  // still deserves a guard (single-source removal had none before).
  if (!globalThis.window?.confirm(
    `Remove this ${source.platform} source?\n${source.url}\n\nThe artist and already-downloaded images stay.`,
  )) return
  try {
    // store._dropSource patches the cache in place — no full reload, so the
    // table + expansion don't reset (operator-flagged lock/reload, 2026-06-07).
    await store.remove(source.id, source.artist_id)
  } catch (e) {
    toast({ text: `Remove failed: ${e?.body?.detail || e?.message || e}`, type: 'error' })
  }
}

async function toggleSourceEnabled({ source, enabled }) {
  try {
    await store.update(source.id, { enabled }, source.artist_id)
  } catch (e) {
    toast({ text: `Update failed: ${e?.body?.detail || e?.message || e}`, type: 'error' })
  }
}

async function onSourceSaved() {
  showSourceDialog.value = false
  await refresh()
}

function onArtistCreated(artist) {
  showArtistDialog.value = false
  openAddSource(artist)
}

async function onCheck(source) {
  try {
    const body = await store.checkNow(source.id)
    // Audit 2026-06-02: /api/sources/<id>/check returns 202 with
    // `{status:'deferred', cooldown_until}` when the platform is in
    // cooldown — the previous handler treated this as success and
    // toasted "event #undefined", masking that nothing was enqueued.
    if (body?.status === 'deferred') {
      toast({
        text: 'Check deferred — platform in cooldown',
        type: 'info',
      })
      return
    }
    toast({
      text: `Check enqueued (event #${body.download_event_id})`,
      type: 'success',
    })
  } catch (e) {
    if (e?.body?.download_event_id) {
      toast({
        text: 'Already running — see Downloads',
        type: 'info',
      })
      router.push({ path: '/subscriptions', query: { tab: 'downloads', source_id: source.id } })
    } else {
      toast({
        text: `Check failed: ${e?.detail || e?.message || e}`,
        type: 'error',
      })
    }
  }
}

// Plan #693: toggle a run-until-done backfill. Starting walks the full post
// history in time-boxed chunks across ticks until it reaches the bottom (the
// row badge tracks progress / completion); stopping cancels back to tick mode.
async function onBackfill(source) {
  const running = source.backfill_state === 'running'
  try {
    if (running) {
      await store.stopBackfill(source.id, source.artist_id)
      toast({ text: `Backfill stopped for ${source.artist_name}`, type: 'success' })
    } else {
      await store.startBackfill(source.id, source.artist_id)
      toast({ text: `Backfill started for ${source.artist_name}`, type: 'success' })
    }
    // startBackfill/stopBackfill patch the source in place — no full reload.
  } catch (e) {
    toast({
      text: `Backfill ${running ? 'stop' : 'start'} failed: ${e?.body?.detail || e?.message || e}`,
      type: 'error',
    })
  }
}

// Plan #697: arm a recovery walk (Patreon-only) — re-fetches dropped-and-deleted
// near-dups and re-evaluates them under the current pHash threshold. Reuses the
// backfill lifecycle/badge; stop via the same Stop control (onBackfill).
async function onRecover(source) {
  try {
    await store.recoverSource(source.id, source.artist_id)
    toast({ text: `Recovery started for ${source.artist_name}`, type: 'success' })
    // recoverSource patches the source in place — no full reload.
  } catch (e) {
    toast({
      text: `Recovery start failed: ${e?.body?.detail || e?.message || e}`,
      type: 'error',
    })
  }
}

async function checkAll(group) {
  let ok = 0
  let conflict = 0
  let deferred = 0
  for (const s of group.sources) {
    if (!s.enabled) continue
    try {
      const body = await store.checkNow(s.id)
      // Audit 2026-06-02: deferred (202 + cooldown_until) used to be
      // counted as queued, inflating the success tally and hiding
      // that the cooldown actually held the work back.
      if (body?.status === 'deferred') deferred += 1
      else ok += 1
    } catch (e) {
      if (e?.body?.download_event_id) conflict += 1
    }
  }
  const parts = []
  if (ok) parts.push(`${ok} queued`)
  if (deferred) parts.push(`${deferred} deferred (cooldown)`)
  if (conflict) parts.push(`${conflict} already running`)
  toast({
    text: parts.join(', ') || 'Nothing to check (no enabled sources)',
    type: 'info',
  })
}

function anyChecking(sources) {
  return sources.some((s) => store.checkingIds.has(s.id))
}

function resolveSelectedGroups() {
  return groups.value.filter((g) => selected.value.includes(g.key))
}

async function bulkSetEnabled(enabled) {
  const groups = resolveSelectedGroups()
  let changed = 0
  for (const g of groups) {
    for (const s of g.sources) {
      if (s.enabled === enabled) continue
      try {
        await store.update(s.id, { enabled }, s.artist_id)
        changed += 1
      } catch { /* keep going */ }
    }
  }
  await refresh()
  toast({
    text: `${changed} source${changed === 1 ? '' : 's'} ${enabled ? 'enabled' : 'disabled'}`,
    type: 'success',
  })
  selected.value = []
}

// #5: arm a run-until-done backfill on every enabled source in the selected
// subscriptions (skips ones already backfilling). Patches each row in place.
async function bulkBackfill() {
  const sel = resolveSelectedGroups()
  let armed = 0
  for (const g of sel) {
    for (const s of g.sources) {
      if (!s.enabled || s.backfill_state === 'running') continue
      try {
        await store.startBackfill(s.id, s.artist_id)
        armed += 1
      } catch { /* keep going */ }
    }
  }
  toast({
    text: armed ? `Backfill started on ${armed} source${armed === 1 ? '' : 's'}`
      : 'Nothing to backfill (already running or disabled)',
    type: armed ? 'success' : 'info',
  })
  selected.value = []
}

async function bulkDelete() {
  const groups = resolveSelectedGroups()
  const total = groups.reduce((n, g) => n + g.sources.length, 0)
  if (!globalThis.window?.confirm(
    `Delete ${total} source${total === 1 ? '' : 's'} across ${groups.length} subscription${groups.length === 1 ? '' : 's'}? Artist rows remain.`,
  )) return
  let deleted = 0
  for (const g of groups) {
    for (const s of g.sources) {
      try {
        await store.remove(s.id, s.artist_id)
        deleted += 1
      } catch { /* keep going */ }
    }
  }
  await refresh()
  toast({
    text: `${deleted} source${deleted === 1 ? '' : 's'} deleted`,
    type: 'success',
  })
  selected.value = []
}
</script>

<style scoped>
.fc-subs__sched { margin-bottom: 10px; }
.fc-subs__bar {
  display: flex; gap: 0.75rem; align-items: center;
  flex-wrap: wrap;
  /* Sticky to the top of the hub's scroll window so the filter/search
     controls stay reachable while scrolling a long subscription list.
     Opaque bg so table rows don't bleed through. Operator-flagged
     2026-05-28. */
  position: sticky;
  top: 0;
  z-index: 3;
  background: rgb(var(--v-theme-background));
  padding: 8px 0 1rem;
}
.fc-subs__bar .v-chip { cursor: pointer; }
.fc-subs__status { max-width: 180px; }
.fc-subs__search { max-width: 320px; flex: 1 1 200px; }
/* Phones: status + search each take a full-width row; drop the spacer that
   would otherwise eat a row pushing them around. */
@media (max-width: 600px) {
  .fc-subs__status, .fc-subs__search { max-width: none; flex-basis: 100%; }
  .fc-subs__bar :deep(.v-spacer) { display: none; }
  /* The table renders as stacked cards (mobile-breakpoint); reclaim the
     desktop indent on the expanded sources detail (it keeps its own
     horizontal scroll for the wide source columns). */
  .fc-subs__sources-cell { padding-left: 0.5rem !important; }
}
.fc-subs__loading, .fc-subs__empty {
  display: flex; justify-content: center; padding: 2rem;
  color: rgb(var(--v-theme-on-surface-variant));
}
.fc-subs__card {
  background: rgb(var(--v-theme-surface));
}

/* Mobile compact-card list (replaces the data-table <600px). */
.fc-subs__mlist { display: flex; flex-direction: column; gap: 8px; }
.fc-subs__mcard {
  border: 1px solid rgb(var(--v-theme-surface-light));
  border-radius: 8px;
  background: rgb(var(--v-theme-surface));
  padding: 8px 10px;
}
.fc-subs__mhead { display: flex; align-items: center; gap: 6px; cursor: pointer; }
.fc-subs__mmeta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin-top: 4px;
}
.fc-subs__mmeta-text {
  font-size: 0.78rem; color: rgb(var(--v-theme-on-surface-variant));
  font-variant-numeric: tabular-nums;
}
.fc-subs__mbody {
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid rgb(var(--v-theme-surface-light));
  display: flex; flex-direction: column; gap: 6px;
}
.fc-subs__mactions { display: flex; gap: 2px; }
.fc-subs__name { font-weight: 600; }
.fc-subs__sub-url {
  display: block;
  max-width: 460px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.76rem;
  color: rgb(var(--v-theme-on-surface-variant));
  text-decoration: none;
}
.fc-subs__sub-url:hover { color: rgb(var(--v-theme-accent)); }
.fc-subs__chips {
  display: flex; flex-wrap: wrap; gap: 4px;
}
.fc-subs__when {
  color: rgb(var(--v-theme-on-surface-variant));
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.fc-subs__zero {
  color: rgb(var(--v-theme-on-surface-variant));
  opacity: 0.6;
}
.fc-subs__sources-row td {
  padding: 0 !important;
  background: rgb(var(--v-theme-surface-light));
}
.fc-subs__sources-cell {
  padding-left: 2rem !important;
  border-top: 1px solid rgb(var(--v-theme-on-surface-variant) / 0.15);
}
.fc-subs__sources-table {
  background: transparent !important;
}
.fc-subs__sources-empty {
  color: rgb(var(--v-theme-on-surface-variant));
  text-align: center;
  padding: 1rem;
}
.fc-subs__bulk { border-radius: 8px; }
</style>
