<template>
  <div class="pb-6">
    <!-- Header with back button — shared component so peer files match local
         files (CloudFolder) on both desktop and mobile. -->
    <div class="shrink-0 mb-4">
      <BackButton label="Back to Cloud" @click="goBack" />

      <!-- Peer Header -->
      <div class="flex items-center gap-4">
        <div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/15">
          <svg class="w-7 h-7 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
          </svg>
        </div>
        <div class="hidden md:block">
          <div class="flex items-center gap-2">
            <h1 class="text-2xl font-bold text-white">{{ peerDisplayName }}</h1>
            <span
              v-if="transportPill"
              :class="transportPill.cls"
              :title="transportPill.title"
              class="text-xs px-2 py-0.5 rounded-full font-medium"
            >{{ transportPill.label }}</span>
          </div>
          <p v-if="currentPeer?.did" class="text-sm text-white/50 font-mono truncate max-w-md" :title="currentPeer.did">{{ currentPeer.did }}</p>
          <p v-else class="text-sm text-white/50">Peer files</p>
        </div>
        <!-- Mobile: the title block above is hidden (the global header carries the
             peer name), so the transport pill would vanish with it. Render it on
             its own here so mobile also sees whether this peer is FIPS or Tor. -->
        <span
          v-if="transportPill"
          :class="transportPill.cls"
          :title="transportPill.title"
          class="md:hidden text-xs px-2 py-0.5 rounded-full font-medium"
        >{{ transportPill.label }}</span>
      </div>
    </div>

    <!-- Loading -->
    <div v-if="loading && catalogItems.length === 0" class="glass-card p-8 text-center">
      <svg class="animate-spin h-6 w-6 text-purple-400 mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
      </svg>
      <p class="text-white/50 text-sm">Connecting to peer… This may take a few seconds.</p>
    </div>

    <!-- Error -->
    <div v-else-if="catalogError && catalogItems.length === 0" class="glass-card p-6">
      <div class="alert-error mb-4">{{ catalogError }}</div>
      <button class="glass-button px-4 py-2 rounded-lg text-sm" @click="loadCatalog">Retry</button>
    </div>

    <!-- Empty -->
    <div v-else-if="catalogItems.length === 0 && !loading" class="glass-card p-8 text-center">
      <p class="text-white/50">This peer has no shared files.</p>
    </div>

    <!-- Purchase error -->
    <div v-if="purchaseError" class="glass-card p-3 mb-4 flex items-center gap-3 border border-red-500/30">
      <svg class="w-4 h-4 text-red-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
      </svg>
      <span class="text-sm text-red-400 flex-1">{{ purchaseError }}</span>
      <button class="text-xs text-white/50 hover:text-white" @click="purchaseError = null">Dismiss</button>
    </div>

    <!-- File Grid -->
    <div v-if="catalogItems.length > 0" class="space-y-3">
      <div v-if="loading" class="p-2 text-center text-white/45 text-xs flex items-center justify-center gap-2">
        <svg class="animate-spin h-3.5 w-3.5" fill="none" viewBox="0 0 24 24">
          <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
          <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
        </svg>
        Refreshing peer files...
      </div>
      <div v-else-if="catalogError" class="p-3 rounded-lg border border-red-400/20 bg-red-500/10 text-red-200/85 text-sm">
        {{ catalogError }}
      </div>
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        <div
          v-for="item in catalogItems"
          :key="item.id"
          class="glass-card overflow-hidden flex flex-col h-full"
        >
        <!-- Media preview (images / videos / audio) -->
        <div
          v-if="isMediaMime(item.mime_type)"
          class="relative aspect-video overflow-hidden cursor-pointer group"
          @click="isOwned(item) ? viewOwned(item) : (isPlayable(item.mime_type) ? playMedia(item) : undefined)"
        >
          <img
            v-if="item.mime_type.startsWith('image/') && previewUrls[item.id]"
            :src="previewUrls[item.id]"
            :alt="item.filename"
            class="w-full h-full object-cover"
            :style="(isPaidItem(item.access) && !isOwned(item)) ? 'filter: blur(16px); transform: scale(1.15);' : ''"
          />
          <video
            v-else-if="item.mime_type.startsWith('video/') && previewUrls[item.id]"
            :src="previewUrls[item.id]"
            class="w-full h-full object-cover pointer-events-none"
            muted
            autoplay
            loop
            playsinline
          />
          <!-- Audio waveform placeholder -->
          <div v-else-if="item.mime_type.startsWith('audio/')" class="w-full h-full flex flex-col items-center justify-center bg-gradient-to-br from-orange-500/10 to-orange-600/5">
            <svg class="w-12 h-12 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
            </svg>
            <p class="mt-2 text-xs text-white/50 truncate max-w-[80%]">{{ item.filename.split('/').pop() }}</p>
          </div>
          <div v-else class="w-full h-full flex items-center justify-center" :class="fileIconBg(item.mime_type)">
            <svg class="w-10 h-10" :class="fileIconColor(item.mime_type)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="fileIconPath(item.mime_type)" />
            </svg>
          </div>
          <!-- Play button overlay for video/audio -->
          <div
            v-if="isPlayable(item.mime_type)"
            class="absolute inset-0 flex items-center justify-center bg-black/0 group-hover:bg-black/30 transition-colors"
          >
            <div class="w-12 h-12 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
              <svg class="w-6 h-6 text-white ml-0.5" fill="currentColor" viewBox="0 0 24 24">
                <path d="M8 5v14l11-7L8 5z" />
              </svg>
            </div>
          </div>
          <!-- Owned badge (top-right) — purchased, unlocked for this buyer -->
          <div v-if="isPaidItem(item.access) && isOwned(item)" class="absolute top-2 right-2 flex items-center gap-1.5 px-2 py-1 rounded-lg bg-green-500/20 backdrop-blur-sm">
            <svg class="w-4 h-4 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
            </svg>
            <span class="text-xs font-medium text-green-400">Owned</span>
          </div>
          <!-- Paid badge (top-right) — not yet purchased -->
          <div v-else-if="isPaidItem(item.access)" class="absolute top-2 right-2 flex items-center gap-1.5 px-2 py-1 rounded-lg bg-black/60 backdrop-blur-sm">
            <svg class="w-4 h-4 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
            </svg>
            <span class="text-xs font-medium text-orange-400">{{ getItemPrice(item.access) }} sats</span>
          </div>
          <!-- Preview badge for paid playable items (only before purchase) -->
          <div v-if="isPaidItem(item.access) && !isOwned(item) && isPlayable(item.mime_type)" class="absolute bottom-2 left-2 px-2 py-0.5 rounded bg-black/60 backdrop-blur-sm">
            <span class="text-xs text-white/70">10% preview</span>
          </div>
        </div>

        <!-- Card body — pinned to the bottom so the filename + action buttons
             line up across cards of differing preview heights. -->
        <div class="p-4 flex items-center gap-4 mt-auto">
          <div v-if="!isMediaMime(item.mime_type)" class="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center" :class="fileIconBg(item.mime_type)">
            <svg class="w-5 h-5" :class="fileIconColor(item.mime_type)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="fileIconPath(item.mime_type)" />
            </svg>
          </div>
          <div class="flex-1 min-w-0">
            <p class="text-sm font-medium text-white truncate">{{ item.filename }}</p>
            <p class="text-xs text-white/40">{{ formatSize(item.size_bytes) }}</p>
          </div>
          <div class="flex items-center gap-2">
            <span
              v-if="!isPaidItem(item.access)"
              class="text-xs px-2 py-0.5 rounded-full"
              :class="accessBadgeClass(item.access)"
            >
              {{ accessLabel(item.access) }}
            </span>
            <!-- View button for owned content (image/video/audio), from cache -->
            <button
              v-if="isOwned(item) && isMediaMime(item.mime_type)"
              class="glass-button px-3 py-1.5 rounded-lg text-xs font-medium flex items-center gap-1.5"
              :disabled="playing === item.id"
              @click="viewOwned(item)"
            >
              <template v-if="playing === item.id">
                <div class="w-3 h-3 border-2 border-white/20 border-t-white/80 rounded-full animate-spin"></div>
                <span>Opening...</span>
              </template>
              <template v-else>
                <svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
                  <path d="M8 5v14l11-7L8 5z" />
                </svg>
                <span>View</span>
              </template>
            </button>
            <!-- Preview/Play button — pre-purchase only -->
            <button
              v-else-if="isPlayable(item.mime_type)"
              class="glass-button px-3 py-1.5 rounded-lg text-xs font-medium flex items-center gap-1.5"
              :disabled="playing === item.id"
              @click="playMedia(item)"
            >
              <template v-if="playing === item.id">
                <div class="w-3 h-3 border-2 border-white/20 border-t-white/80 rounded-full animate-spin"></div>
                <span>Loading...</span>
              </template>
              <template v-else>
                <svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
                  <path d="M8 5v14l11-7L8 5z" />
                </svg>
                <span>{{ isPaidItem(item.access) ? 'Preview' : 'Play' }}</span>
              </template>
            </button>
            <button
              class="glass-button px-3 py-1.5 rounded-lg text-xs font-medium flex items-center gap-1.5"
              :disabled="downloading === item.id"
              @click="downloadFile(item)"
            >
              <template v-if="downloading === item.id">
                <div class="w-3 h-3 border-2 border-white/20 border-t-white/80 rounded-full animate-spin"></div>
                <span>{{ isPaidItem(item.access) && !isOwned(item) ? 'Paying...' : 'Loading...' }}</span>
              </template>
              <template v-else-if="isPaidItem(item.access) && !isOwned(item)">
                <svg class="w-3.5 h-3.5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
                </svg>
                <span>Buy {{ getItemPrice(item.access) }} sats</span>
              </template>
              <template v-else>
                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
                </svg>
                <span>{{ isOwned(item) ? 'Save' : 'Download' }}</span>
              </template>
            </button>
          </div>
        </div>
        </div>
      </div>
    </div>

    <!-- Video player modal -->
    <Teleport to="body">
      <Transition name="fade">
        <div
          v-if="videoPlayerUrl && videoPlayerItem"
          class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
          @click.self="closeVideoPlayer"
        >
          <div class="relative w-full max-w-4xl mx-4">
            <!-- Close button -->
            <button
              class="absolute -top-10 right-0 text-white/60 hover:text-white transition-colors"
              @click="closeVideoPlayer"
            >
              <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
              </svg>
            </button>
            <!-- Picture-in-picture -->
            <button
              v-if="pipSupported"
              class="absolute -top-10 right-10 text-white/60 hover:text-white transition-colors"
              title="Picture-in-picture"
              @click="togglePip(peerVideoRef)"
            >
              <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <rect x="3" y="5" width="18" height="14" rx="2" stroke-width="2" />
                <rect x="12" y="12" width="7" height="5" rx="1" stroke-width="2" />
              </svg>
            </button>
            <!-- Video element -->
            <div class="relative">
              <video
                ref="peerVideoRef"
                :src="videoPlayerUrl"
                class="w-full rounded-xl bg-black"
                controls
                autoplay
                @playing="videoLoading = false"
                @canplay="videoLoading = false"
                @error="videoLoading = false; videoError = true"
              />
              <!-- Loader while the stream connects over mesh/Tor -->
              <div v-if="videoLoading && !videoError" class="absolute inset-0 flex flex-col items-center justify-center gap-3 rounded-xl bg-black/60 pointer-events-none">
                <svg class="w-8 h-8 animate-spin text-white/80" fill="none" viewBox="0 0 24 24">
                  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
                  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.4 0 0 5.4 0 12h4z" />
                </svg>
                <span class="text-sm text-white/70">Connecting to peer…</span>
              </div>
              <!-- Error state -->
              <div v-if="videoError" class="absolute inset-0 flex flex-col items-center justify-center gap-2 rounded-xl bg-black/70 text-center px-4">
                <p class="text-sm text-white/80">Couldn't play this video</p>
                <p class="text-xs text-white/50">The peer may be offline, or this preview can't be played. Try downloading it instead.</p>
              </div>
            </div>
            <!-- Info bar -->
            <div class="mt-3 flex items-center justify-between">
              <div>
                <p class="text-sm font-medium text-white">{{ videoPlayerItem.filename.split('/').pop() }}</p>
                <p class="text-xs text-white/40">{{ formatSize(videoPlayerItem.size_bytes) }}</p>
              </div>
              <div v-if="videoPlayerPaid" class="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-orange-500/15">
                <svg class="w-3.5 h-3.5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
                </svg>
                <span class="text-xs text-orange-400">10% preview - Buy to unlock full</span>
              </div>
            </div>
          </div>
        </div>
      </Transition>
    </Teleport>

    <!-- Owned-content viewer — full file from the local cache (no re-payment) -->
    <Teleport to="body">
      <Transition name="fade">
        <div
          v-if="viewerUrl && viewerItem"
          class="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm p-4"
          @click.self="closeViewer"
        >
          <div class="relative w-full max-w-4xl mx-4">
            <button
              class="absolute -top-10 right-0 text-white/60 hover:text-white transition-colors"
              @click="closeViewer"
            >
              <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
              </svg>
            </button>
            <img
              v-if="viewerMime.startsWith('image/')"
              :src="viewerUrl"
              :alt="viewerItem.filename"
              class="w-full max-h-[80vh] object-contain rounded-xl bg-black"
            />
            <video
              v-else-if="viewerMime.startsWith('video/')"
              :src="viewerUrl"
              class="w-full max-h-[80vh] rounded-xl bg-black"
              controls
              autoplay
              playsinline
            />
            <audio
              v-else-if="viewerMime.startsWith('audio/')"
              :src="viewerUrl"
              class="w-full"
              controls
              autoplay
            />
            <div v-else class="glass-card p-8 rounded-xl text-center">
              <p class="text-sm text-white/70">This file type can't be previewed. Use Save to download it.</p>
            </div>
            <div class="mt-3 flex items-center justify-between gap-3">
              <div class="min-w-0">
                <p class="text-sm font-medium text-white truncate">{{ viewerItem.filename.split('/').pop() }}</p>
                <p class="text-xs text-green-400">Owned · unlocked</p>
              </div>
              <button
                class="glass-button px-3 py-1.5 rounded-lg text-xs font-medium flex items-center gap-1.5 shrink-0"
                @click="saveOwned(viewerItem)"
              >
                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
                </svg>
                <span>Save</span>
              </button>
            </div>
          </div>
        </div>
      </Transition>
    </Teleport>

    <!-- Payment method picker / Lightning invoice QR (#46) -->
    <Teleport to="body">
      <Transition name="fade">
        <div
          v-if="payItem"
          class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
          @click.self="closePayModal"
        >
          <div class="glass-card w-full max-w-md p-5 rounded-2xl relative">
            <button
              class="absolute top-3 right-3 text-white/50 hover:text-white transition-colors"
              @click="closePayModal"
            >
              <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
              </svg>
            </button>

            <h3 class="text-base font-semibold text-white mb-1">Buy this file</h3>
            <p class="text-xs text-white/50 mb-4 truncate">
              {{ payItem.filename.split('/').pop() }} · {{ getItemPrice(payItem.access) }} sats
            </p>

            <!-- Step 1: choose a payment method — only the methods the SELLER
                 accepts for this item are offered -->
            <div v-if="payMode === 'choose'" class="space-y-3">
              <button
                v-if="acceptsMethod(payItem.access, 'ecash') || acceptsMethod(payItem.access, 'fedimint')"
                class="w-full glass-button px-4 py-3 rounded-xl flex items-center justify-start gap-3 text-left"
                :disabled="ecashPreparing || downloading === payItem.id"
                @click="prepareEcashPay"
              >
                <svg class="w-6 h-6 text-green-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
                </svg>
                <span>
                  <span class="block text-base text-white">{{ ecashPreparing ? 'Checking your wallets…' : 'Pay from this node’s ecash wallet' }}</span>
                  <span class="block text-sm text-white/50">Instant, using your Cashu or Fedimint balance</span>
                </span>
              </button>

              <button
                v-if="acceptsMethod(payItem.access, 'lightning')"
                class="w-full glass-button px-4 py-3 rounded-xl flex items-center justify-start gap-3 text-left"
                :disabled="lnPaying"
                @click="payWithLightning"
              >
                <svg class="w-6 h-6 text-yellow-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
                </svg>
                <span>
                  <span class="block text-base text-white">{{ lnPaying ? 'Paying…' : 'Pay with my Lightning node' }}</span>
                  <span class="block text-sm text-white/50">Pays the seller’s invoice from your node’s Lightning wallet</span>
                </span>
              </button>

              <button
                v-if="acceptsMethod(payItem.access, 'lightning') || acceptsMethod(payItem.access, 'onchain')"
                class="w-full glass-button px-4 py-3 rounded-xl flex items-center justify-start gap-3 text-left"
                :disabled="lnPaying || onchainPaying"
                @click="openQrPay"
              >
                <svg class="w-6 h-6 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
                </svg>
                <span>
                  <span class="block text-base text-white">Pay from another wallet (QR)</span>
                  <span class="block text-sm text-white/50">Scan an on-chain or Lightning QR with any wallet</span>
                </span>
              </button>

              <button
                v-if="acceptsMethod(payItem.access, 'onchain')"
                class="w-full glass-button px-4 py-3 rounded-xl flex items-center justify-start gap-3 text-left"
                :disabled="lnPaying || onchainPaying"
                @click="payOnchain"
              >
                <svg class="w-6 h-6 text-orange-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
                </svg>
                <span>
                  <span class="block text-base text-white">{{ onchainPaying ? 'Sending…' : 'Pay on-chain from my node' }}</span>
                  <span class="block text-sm text-white/50">Sends Bitcoin on-chain from your node’s wallet (slower)</span>
                </span>
              </button>

              <p v-if="lnError" class="text-xs text-red-400 px-1">{{ lnError }}</p>
            </div>

            <!-- Step 1b: ecash confirmation — show which wallet will be spent -->
            <div v-else-if="payMode === 'ecash-confirm' && ecashPlan" class="space-y-4">
              <div class="text-center py-2">
                <div class="text-3xl font-bold text-white">{{ getItemPrice(payItem.access) }} <span class="text-lg text-white/50">sats</span></div>
                <div class="text-xs text-white/50 mt-1">from your node’s ecash wallet</div>
              </div>

              <!-- Backend selector: the chosen one is highlighted; the user can
                   switch to the other if it has enough balance. -->
              <div class="space-y-2">
                <button
                  v-for="b in (['cashu', 'fedimint', 'ark'] as const)"
                  :key="b"
                  @click="ecashPlan.chosen = b"
                  :disabled="ecashBalanceOf(b) < getItemPrice(payItem.access)"
                  class="w-full px-4 py-3 rounded-xl flex items-center gap-3 text-left border transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
                  :class="ecashPlan.chosen === b ? 'border-green-400/70 bg-green-400/10' : 'border-white/10 bg-white/5 hover:bg-white/10'"
                >
                  <span class="text-xl shrink-0">{{ b === 'cashu' ? '🥜' : b === 'fedimint' ? '🤝' : '⚓' }}</span>
                  <span class="flex-1 min-w-0">
                    <span class="block text-base text-white">{{ b === 'cashu' ? 'Cashu' : b === 'fedimint' ? 'Fedimint' : 'Ark' }}</span>
                    <span class="block text-xs text-white/50">Balance: {{ ecashBalanceOf(b).toLocaleString() }} sats<span v-if="ecashBalanceOf(b) < getItemPrice(payItem.access)"> · not enough</span></span>
                  </span>
                  <svg v-if="ecashPlan.chosen === b" class="w-5 h-5 text-green-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
                  </svg>
                </button>
              </div>

              <p v-if="purchaseError" class="text-sm text-red-400">{{ purchaseError }}</p>

              <div class="flex gap-2 pt-1">
                <button
                  class="flex-1 glass-button px-4 py-2.5 rounded-xl text-sm text-white/70"
                  :disabled="downloading === payItem.id"
                  @click="payMode = 'choose'"
                >Back</button>
                <button
                  class="flex-1 px-4 py-2.5 rounded-xl text-sm font-semibold text-black bg-green-400 hover:bg-green-300 transition-colors disabled:opacity-50"
                  :disabled="!ecashPlan.chosen || downloading === payItem.id"
                  @click="confirmEcashPay"
                >{{ downloading === payItem.id ? 'Paying…' : 'Pay' }}</button>
              </div>
            </div>

            <!-- Step 2: pay from another wallet — tabbed QR (on-chain default) -->
            <div v-else>
              <!-- Method tabs, styled like the wallet Send/Receive modal;
                   only tabs the seller accepts are shown -->
              <div class="flex gap-1 mb-4 p-1 bg-white/5 rounded-lg">
                <button
                  v-for="m in (['onchain', 'lightning'] as const).filter(m => acceptsMethod(payItem!.access, m))"
                  :key="m"
                  @click="selectQrTab(m)"
                  class="flex-1 px-2 py-1.5 rounded text-xs font-medium transition-colors"
                  :class="qrTab === m ? 'bg-white/15 text-white' : 'text-white/50 hover:text-white/80'"
                >{{ m === 'onchain' ? 'On-chain' : 'Lightning' }}</button>
              </div>

              <!-- On-chain QR -->
              <div v-if="qrTab === 'onchain'" class="text-center">
                <div v-if="onchainWaiting && !onchainData" class="py-10 flex flex-col items-center gap-3">
                  <svg class="w-7 h-7 animate-spin text-white/80" fill="none" viewBox="0 0 24 24">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.4 0 0 5.4 0 12h4z" />
                  </svg>
                  <span class="text-sm text-white/70">Requesting an address from the seller…</span>
                </div>
                <div v-else-if="onchainData">
                  <div v-if="onchainQr" class="bg-white rounded-xl p-3 inline-block mb-3">
                    <img :src="onchainQr" alt="On-chain payment QR" class="w-48 h-48" />
                  </div>
                  <p class="text-sm text-white mb-1">{{ onchainData.amount_sats }} sats</p>
                  <p class="text-xs text-white/50 mb-3 flex items-center justify-center gap-2">
                    <svg class="w-3.5 h-3.5 animate-spin text-orange-400" fill="none" viewBox="0 0 24 24">
                      <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
                      <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.4 0 0 5.4 0 12h4z" />
                    </svg>
                    Waiting for payment…
                  </p>
                  <div class="flex items-center gap-2 bg-black/40 rounded-lg px-2 py-1.5">
                    <code class="text-[10px] text-white/60 truncate flex-1 text-left">{{ onchainData.address }}</code>
                    <button class="text-xs text-white/60 hover:text-white shrink-0" @click="copyOnchain">
                      {{ onchainCopied ? 'Copied!' : 'Copy' }}
                    </button>
                  </div>
                  <p class="text-[10px] text-white/40 mt-2">Needs 1 confirmation before the file unlocks.</p>
                </div>
                <p v-if="onchainError" class="text-sm text-red-400 mt-3">{{ onchainError }}</p>
              </div>

              <!-- Lightning invoice QR -->
              <div v-else class="text-center">
                <div v-if="invoiceWaiting && !invoiceData" class="py-10 flex flex-col items-center gap-3">
                  <svg class="w-7 h-7 animate-spin text-white/80" fill="none" viewBox="0 0 24 24">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.4 0 0 5.4 0 12h4z" />
                  </svg>
                  <span class="text-sm text-white/70">Requesting invoice from seller…</span>
                </div>
                <div v-else-if="invoiceData">
                  <div v-if="invoiceQr" class="bg-white rounded-xl p-3 inline-block mb-3">
                    <img :src="invoiceQr" alt="Lightning invoice QR" class="w-48 h-48" />
                  </div>
                  <p class="text-sm text-white mb-1">{{ invoiceData.price_sats }} sats</p>
                  <p class="text-xs text-white/50 mb-3 flex items-center justify-center gap-2">
                    <svg class="w-3.5 h-3.5 animate-spin text-amber-400" fill="none" viewBox="0 0 24 24">
                      <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
                      <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.4 0 0 5.4 0 12h4z" />
                    </svg>
                    Waiting for payment…
                  </p>
                  <div class="flex items-center gap-2 bg-black/40 rounded-lg px-2 py-1.5">
                    <code class="text-[10px] text-white/60 truncate flex-1 text-left">{{ invoiceData.bolt11 }}</code>
                    <button class="text-xs text-white/60 hover:text-white shrink-0" @click="copyInvoice">
                      {{ invoiceCopied ? 'Copied!' : 'Copy' }}
                    </button>
                  </div>
                </div>
                <p v-if="invoiceError" class="text-sm text-red-400 mt-3">{{ invoiceError }}</p>
              </div>

              <button
                class="glass-button px-4 py-2 rounded-lg text-sm mt-4 w-full"
                @click="payMode = 'choose'"
              >
                Back
              </button>
            </div>
          </div>
        </div>
      </Transition>
    </Teleport>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, reactive, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import QRCode from 'qrcode'
import { rpcClient } from '@/api/rpc-client'
import { useAudioPlayer } from '@/composables/useAudioPlayer'
import { pipSupported, togglePip } from '@/utils/pip'
import BackButton from '@/components/BackButton.vue'

const props = defineProps<{
  peerId?: string
}>()

const router = useRouter()

interface PeerNode {
  did: string
  pubkey: string
  onion: string
  name?: string
  trust_level: string
}

interface CatalogItem {
  id: string
  filename: string
  mime_type: string
  size_bytes: number
  description: string
  access: string | { paid: { price_sats: number } }
}

const loading = ref(true)
const currentPeer = ref<PeerNode | null>(null)
const catalogError = ref('')
const catalogItems = ref<CatalogItem[]>([])
const downloading = ref<string | null>(null)
const playing = ref<string | null>(null)
const purchaseError = ref<string | null>(null)
// Transport actually used to reach this peer (returned by content.browse-peer)
// so we can show a FIPS/Tor pill instead of always assuming Tor (B21).
const transport = ref<string | null>(null)
const transportPill = computed(() => {
  switch (transport.value) {
    case 'fips':
      return { label: 'FIPS', cls: 'bg-green-500/20 text-green-300', title: 'Connected over the fast encrypted mesh (FIPS)' }
    case 'mesh':
      return { label: 'Mesh', cls: 'bg-green-500/20 text-green-300', title: 'Connected over the mesh' }
    case 'lan':
      return { label: 'LAN', cls: 'bg-blue-500/20 text-blue-300', title: 'Connected over the local network' }
    case 'tor':
      return { label: 'Tor', cls: 'bg-amber-500/20 text-amber-300', title: 'Connected over Tor (slower)' }
    default:
      return null
  }
})
const previewUrls = reactive<Record<string, string>>({})
const audioPlayer = useAudioPlayer()

// ─── Owned (purchased) content ───────────────────────────────────────────
// A paid item the buyer has purchased stays unlocked for them: the backend
// caches the bytes keyed by (seller onion, content_id). We load that set so the
// gallery renders owned items unblurred and opens them in-app from the local
// cache — no re-payment, and no reliance on a browser download (which silently
// fails on the mobile companion, the "paid but never unlocked" report).
const ownedKeys = ref<Set<string>>(new Set())
function ownKey(onion: string, id: string): string { return `${onion}::${id}` }
function isOwned(item: CatalogItem): boolean {
  const onion = props.peerId || currentPeer.value?.onion
  return !!onion && ownedKeys.value.has(ownKey(onion, item.id))
}
async function loadOwned() {
  try {
    const res = await rpcClient.call<{ items: { onion: string; content_id: string }[] }>({ method: 'content.owned-list', timeout: 10000 })
    const set = new Set<string>()
    for (const it of res?.items ?? []) set.add(ownKey(it.onion, it.content_id))
    ownedKeys.value = set
  } catch { /* keep last-known owned set on a transient failure */ }
}

function base64ToBlob(base64: string, mime: string): Blob {
  return new Blob([Uint8Array.from(atob(base64), c => c.charCodeAt(0))], { type: mime })
}

// In-app viewer for owned content (image / video / audio), served from cache.
const viewerItem = ref<CatalogItem | null>(null)
const viewerUrl = ref<string | null>(null)
const viewerMime = ref<string>('')
async function viewOwned(item: CatalogItem) {
  const onion = props.peerId || currentPeer.value?.onion
  if (!onion) return
  playing.value = item.id
  purchaseError.value = null
  try {
    const res = await rpcClient.call<{ data?: string; mime_type?: string; error?: string }>({
      method: 'content.owned-get',
      params: { onion, content_id: item.id },
      timeout: 60000,
    })
    if (!res?.data) { purchaseError.value = res?.error || 'Could not open your purchased file'; return }
    const mime = res.mime_type || item.mime_type
    // Audio always plays in the global bottom-bar player — never the lightbox
    // (the blob URL is intentionally not revoked while the bar plays it).
    if (mime.startsWith('audio/')) {
      const url = URL.createObjectURL(base64ToBlob(res.data, mime))
      audioPlayer.play(url, item.filename.split('/').pop() || item.filename)
      return
    }
    if (viewerUrl.value) URL.revokeObjectURL(viewerUrl.value)
    viewerUrl.value = URL.createObjectURL(base64ToBlob(res.data, mime))
    viewerMime.value = mime
    viewerItem.value = item
  } catch (e: unknown) {
    purchaseError.value = e instanceof Error ? e.message : 'Could not open your purchased file'
  } finally {
    playing.value = null
  }
}
function closeViewer() {
  if (viewerUrl.value) URL.revokeObjectURL(viewerUrl.value)
  viewerUrl.value = null
  viewerItem.value = null
  viewerMime.value = ''
}
// Save the owned file to disk from cache (the optional "downloadable" path).
async function saveOwned(item: CatalogItem) {
  const onion = props.peerId || currentPeer.value?.onion
  if (!onion) return
  try {
    const res = await rpcClient.call<{ data?: string; mime_type?: string; error?: string }>({
      method: 'content.owned-get',
      params: { onion, content_id: item.id },
      timeout: 60000,
    })
    if (res?.data) triggerDownload(res.data, item)
    else purchaseError.value = res?.error || 'Could not save your purchased file'
  } catch (e: unknown) {
    purchaseError.value = e instanceof Error ? e.message : 'Could not save your purchased file'
  }
}

// ─── Payment picker (#46) ────────────────────────────────────────────────
// When buying a paid item the user chooses how to pay: their local ecash
// wallet (instant), or a Lightning invoice drawn on the SELLER's node that
// they can pay from any external wallet by scanning a QR.
const payItem = ref<CatalogItem | null>(null)
const payMode = ref<'choose' | 'ecash-confirm' | 'qr'>('choose')
// Ecash confirmation step: after the user picks "pay from this node's ecash",
// we look at both balances, decide which backend covers the price, and show a
// confirm screen so they see (and can switch) which ecash is spent (#3).
type EcashBackend = 'cashu' | 'fedimint' | 'ark'
const ecashPlan = ref<{
  cashu: number
  fedimint: number
  ark: number
  total: number
  chosen: EcashBackend | null
} | null>(null)
const ecashPreparing = ref(false)
// Pay-from-another-wallet QR view: tabbed like the wallet's Send/Receive modal,
// on-chain first (the default).
const qrTab = ref<'onchain' | 'lightning'>('onchain')
const invoiceData = ref<{ bolt11: string; payment_hash: string; price_sats: number } | null>(null)
const invoiceQr = ref('')
const invoiceWaiting = ref(false)
const invoiceError = ref('')
const invoiceCopied = ref(false)
// On-chain QR (pay the seller's address from any external wallet).
const onchainData = ref<{ address: string; amount_sats: number } | null>(null)
const onchainQr = ref('')
const onchainWaiting = ref(false)
const onchainError = ref('')
const onchainCopied = ref(false)
const lnPaying = ref(false)
const lnError = ref('')
const onchainPaying = ref(false)
let onchainPollTimer: ReturnType<typeof setTimeout> | null = null
let invoicePollTimer: ReturnType<typeof setTimeout> | null = null

// Video player modal state
const peerVideoRef = ref<HTMLVideoElement | null>(null)
const videoPlayerItem = ref<CatalogItem | null>(null)
const videoPlayerUrl = ref<string | null>(null)
const videoPlayerPaid = ref(false)
// Streaming a peer's file connects over mesh/Tor before the first frame, so
// show a loader until the element can actually play (or errors).
const videoLoading = ref(false)
const videoError = ref(false)

const peerDisplayName = computed(() => {
  if (currentPeer.value?.name) return currentPeer.value.name
  if (currentPeer.value?.did) return truncateDid(currentPeer.value.did)
  return props.peerId ? truncateOnion(props.peerId) : 'Peer Files'
})

function goBack() {
  router.push({ name: 'cloud' })
}

onMounted(async () => {
  if (props.peerId) {
    // The peer-name lookup is cosmetic — the catalog only needs the onion we
    // already have. Serialized, it added a full mesh round-trip before the
    // files even started loading.
    await Promise.all([
      rpcClient.federationListNodes()
        .then((result) => {
          const peers = result?.nodes ?? []
          currentPeer.value = peers.find((p: PeerNode) => p.onion === props.peerId) || null
        })
        .catch(() => { /* continue with just the onion address */ }),
      loadCatalog(),
      loadOwned(),
    ])
  } else {
    loading.value = false
  }
})

async function loadCatalog() {
  const onion = props.peerId || currentPeer.value?.onion
  if (!onion) return
  const hadItems = catalogItems.value.length > 0
  loading.value = true
  catalogError.value = ''
  try {
    const result = await rpcClient.call<{ items?: CatalogItem[]; transport?: string }>({
      method: 'content.browse-peer',
      params: { onion },
      timeout: 30000,
    })
    catalogItems.value = result?.items ?? []
    transport.value = result?.transport ?? null
  } catch (e: unknown) {
    catalogError.value = e instanceof Error ? e.message : 'Failed to connect to peer'
    if (!hadItems) catalogItems.value = []
  } finally {
    loading.value = false
  }
}

// Load visual previews for image and video items when catalog loads
// Audio files don't need visual thumbnails — they show a waveform icon
watch(catalogItems, async (items) => {
  const onion = props.peerId || currentPeer.value?.onion
  if (!onion) return
  for (const item of items) {
    if ((item.mime_type.startsWith('image/') || item.mime_type.startsWith('video/')) && !previewUrls[item.id]) {
      loadPreview(onion, item)
    }
  }
})

async function loadPreview(onion: string, item: CatalogItem) {
  try {
    const result = await rpcClient.call<{ data?: string; content_type?: string }>({
      method: 'content.preview-peer',
      params: { onion, content_id: item.id },
      timeout: 30000,
    })
    if (result?.data) {
      const mime = result.content_type || item.mime_type
      const bytes = Uint8Array.from(atob(result.data), c => c.charCodeAt(0))
      const blob = new Blob([bytes], { type: mime })
      previewUrls[item.id] = URL.createObjectURL(blob)
    }
  } catch {
    // Preview not available — icon fallback is fine
  }
}

function truncateDid(did: string): string {
  if (did.length <= 24) return did
  return did.slice(0, 16) + '...' + did.slice(-8)
}

function truncateOnion(onion: string): string {
  if (onion.length <= 20) return onion
  return onion.slice(0, 12) + '...'
}

function formatSize(bytes: number): string {
  if (bytes === 0) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB']
  const i = Math.floor(Math.log(bytes) / Math.log(1024))
  return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i]
}

function fileIconBg(mime: string): string {
  if (mime.startsWith('image/')) return 'bg-blue-500/15'
  if (mime.startsWith('audio/')) return 'bg-orange-500/15'
  if (mime.startsWith('video/')) return 'bg-pink-500/15'
  if (mime.startsWith('text/')) return 'bg-green-500/15'
  return 'bg-white/10'
}

function fileIconColor(mime: string): string {
  if (mime.startsWith('image/')) return 'text-blue-400'
  if (mime.startsWith('audio/')) return 'text-orange-400'
  if (mime.startsWith('video/')) return 'text-pink-400'
  if (mime.startsWith('text/')) return 'text-green-400'
  return 'text-white/60'
}

function fileIconPath(mime: string): string {
  if (mime.startsWith('image/')) return 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'
  if (mime.startsWith('audio/')) return 'M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3'
  if (mime.startsWith('video/')) return 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z'
  return 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'
}

function accessLabel(access: CatalogItem['access']): string {
  if (access === 'free') return 'Free'
  if (access === 'peersonly') return 'Peers Only'
  if (typeof access === 'object' && 'paid' in access) return `${access.paid.price_sats} sats`
  return String(access)
}

function accessBadgeClass(access: CatalogItem['access']): string {
  if (access === 'free') return 'bg-green-500/15 text-green-400'
  if (access === 'peersonly') return 'bg-blue-500/15 text-blue-400'
  if (typeof access === 'object' && 'paid' in access) return 'bg-orange-500/15 text-orange-400'
  return 'bg-white/10 text-white/50'
}

function isMediaMime(mime: string): boolean {
  return mime.startsWith('image/') || mime.startsWith('video/') || mime.startsWith('audio/')
}

function isPlayable(mime: string): boolean {
  return mime.startsWith('video/') || mime.startsWith('audio/')
}

function isPaidItem(access: CatalogItem['access']): boolean {
  return typeof access === 'object' && 'paid' in access
}

function getItemPrice(access: CatalogItem['access']): number {
  if (typeof access === 'object' && 'paid' in access) return access.paid.price_sats
  return 0
}

/** Payment methods the seller accepts for this item. Missing/empty = all
 *  (items shared before sellers could restrict methods). */
function acceptsMethod(access: CatalogItem['access'], method: string): boolean {
  if (typeof access !== 'object' || !('paid' in access)) return true
  const accepted = (access.paid as { accepted?: string[] }).accepted
  if (!Array.isArray(accepted) || accepted.length === 0) return true
  return accepted.includes(method)
}

async function downloadFile(item: CatalogItem) {
  const onion = props.peerId || currentPeer.value?.onion
  if (!onion) return
  purchaseError.value = null

  // Already purchased: save from the local cache, no payment.
  if (isOwned(item)) {
    await saveOwned(item)
    return
  }

  const price = getItemPrice(item.access)
  if (price > 0) {
    // Let the buyer choose how to pay (local ecash vs external-wallet QR).
    openPayModal(item)
    return
  }

  // Free / peers-only download: stream straight from the Range-capable proxy
  // (B3) to disk instead of pulling the whole file as a base64 blob over RPC.
  // The old base64-over-RPC path buffered the entire file in node memory AND
  // the browser, which failed outright for large files (#38). A tiny probe
  // first surfaces the real server error (peer unreachable on mesh and Tor)
  // instead of a generic failure (#30).
  downloading.value = item.id
  try {
    const streamUrl = `/api/peer-content/${encodeURIComponent(onion)}/${encodeURIComponent(item.id)}`
    const probe = await probePeerContent(streamUrl)
    if (probe !== true) {
      purchaseError.value = probe
      return
    }
    streamDownload(streamUrl, item)
  } catch (e: unknown) {
    purchaseError.value = e instanceof Error ? e.message : 'Download failed'
  } finally {
    downloading.value = null
  }
}

function openPayModal(item: CatalogItem) {
  payItem.value = item
  payMode.value = 'choose'
  qrTab.value = 'onchain'
  invoiceData.value = null
  invoiceQr.value = ''
  invoiceWaiting.value = false
  invoiceError.value = ''
  invoiceCopied.value = false
  onchainData.value = null
  onchainQr.value = ''
  onchainWaiting.value = false
  onchainError.value = ''
  onchainCopied.value = false
  lnPaying.value = false
  lnError.value = ''
  onchainPaying.value = false
}

function closePayModal() {
  if (invoicePollTimer) { clearTimeout(invoicePollTimer); invoicePollTimer = null }
  if (onchainPollTimer) { clearTimeout(onchainPollTimer); onchainPollTimer = null }
  payItem.value = null
  payMode.value = 'choose'
  ecashPlan.value = null
  ecashPreparing.value = false
  invoiceWaiting.value = false
  onchainWaiting.value = false
  onchainPaying.value = false
}

/**
 * Open the "pay from another wallet" view: tabbed QR like the wallet's
 * Send/Receive modal, defaulting to the on-chain tab (so a QR is shown
 * immediately for any external wallet).
 */
function openQrPay() {
  payMode.value = 'qr'
  invoiceData.value = null
  invoiceQr.value = ''
  invoiceError.value = ''
  invoiceWaiting.value = false
  onchainData.value = null
  onchainQr.value = ''
  onchainError.value = ''
  // Start on the first tab the seller actually accepts.
  if (payItem.value && !acceptsMethod(payItem.value.access, 'onchain')) {
    qrTab.value = 'lightning'
    payWithInvoice()
  } else {
    qrTab.value = 'onchain'
    loadOnchainQr()
  }
}

/** Switch QR tab, lazily loading that method's QR the first time it's shown and
 * resuming its payment poll if it was already loaded (so switching back and
 * forth doesn't silently stop watching for payment). */
function selectQrTab(tab: 'onchain' | 'lightning') {
  if (qrTab.value === tab) return
  qrTab.value = tab
  if (tab === 'onchain') {
    if (invoicePollTimer) { clearTimeout(invoicePollTimer); invoicePollTimer = null }
    if (!onchainData.value && !onchainWaiting.value) {
      loadOnchainQr()
    } else if (onchainData.value && !onchainPaying.value) {
      onchainPaying.value = true
      pollOnchain(onchainData.value.address)
    }
  } else {
    if (onchainPollTimer) { clearTimeout(onchainPollTimer); onchainPollTimer = null }
    onchainPaying.value = false
    if (!invoiceData.value && !invoiceWaiting.value) {
      payWithInvoice()
    } else if (invoiceData.value) {
      scheduleInvoicePoll()
    }
  }
}

/**
 * On-chain QR: ask the seller for a fresh address + amount, render a
 * `bitcoin:` QR for any external wallet, and poll the seller until the payment
 * lands, then release the file (the address is the gate token).
 */
async function loadOnchainQr() {
  const item = payItem.value
  const onion = props.peerId || currentPeer.value?.onion
  if (!item || !onion) return
  onchainError.value = ''
  onchainData.value = null
  onchainQr.value = ''
  onchainWaiting.value = true
  try {
    const req = await rpcClient.call<{ address?: string; amount_sats?: number; error?: string }>({
      method: 'content.request-onchain',
      params: { onion, content_id: item.id },
      timeout: 45000,
    })
    if (!req?.address || !req?.amount_sats) {
      onchainError.value = req?.error || 'The seller could not provide an on-chain address.'
      onchainWaiting.value = false
      return
    }
    onchainData.value = { address: req.address, amount_sats: req.amount_sats }
    const btc = (req.amount_sats / 1e8).toFixed(8)
    try {
      onchainQr.value = await QRCode.toDataURL(`bitcoin:${req.address}?amount=${btc}`, { margin: 1, width: 240 })
    } catch {
      onchainQr.value = '' // fall back to showing the raw address
    }
    onchainWaiting.value = false
    onchainPaying.value = true // "waiting for payment" — drives the poll loop
    pollOnchain(req.address)
  } catch (e: unknown) {
    onchainError.value = e instanceof Error ? e.message : 'Could not request an on-chain address'
    onchainWaiting.value = false
  }
}

async function copyOnchain() {
  if (!onchainData.value) return
  try {
    await navigator.clipboard.writeText(onchainData.value.address)
    onchainCopied.value = true
    setTimeout(() => { onchainCopied.value = false }, 1500)
  } catch { /* clipboard denied */ }
}

/**
 * Pay on-chain from THIS node's wallet: ask the seller for a fresh address +
 * amount, broadcast with lnd.sendcoins, then poll the seller until it detects
 * the payment and release the file (address is the gate token). Slower than LN
 * because the seller waits for the tx to appear/confirm.
 */
async function payOnchain() {
  const item = payItem.value
  const onion = props.peerId || currentPeer.value?.onion
  if (!item || !onion || onchainPaying.value) return

  onchainPaying.value = true
  lnError.value = ''
  try {
    const req = await rpcClient.call<{ address?: string; amount_sats?: number; error?: string }>({
      method: 'content.request-onchain',
      params: { onion, content_id: item.id },
      timeout: 45000,
    })
    if (!req?.address || !req?.amount_sats) {
      lnError.value = req?.error || 'The seller could not provide an on-chain address.'
      onchainPaying.value = false
      return
    }
    const addr = req.address
    const send = await rpcClient.call<{ txid?: string; error?: string }>({
      method: 'lnd.sendcoins',
      params: { addr, amount: req.amount_sats },
      timeout: 60000,
    })
    if (!send?.txid) {
      lnError.value = send?.error || 'On-chain send failed (insufficient on-chain balance?).'
      onchainPaying.value = false
      return
    }
    // Broadcast — now wait for the seller to see it and release.
    pollOnchain(addr)
  } catch (e: unknown) {
    lnError.value = e instanceof Error ? e.message : 'Could not pay on-chain'
    onchainPaying.value = false
  }
}

async function pollOnchain(address: string) {
  const item = payItem.value
  const onion = props.peerId || currentPeer.value?.onion
  if (!item || !onion) { onchainPaying.value = false; return }
  try {
    const res = await rpcClient.call<{ paid?: boolean }>({
      method: 'content.onchain-status',
      params: { onion, content_id: item.id, address },
      timeout: 30000,
    })
    if (res?.paid) {
      const dl = await rpcClient.call<{ data?: string; error?: string }>({
        method: 'content.download-peer-onchain',
        params: { onion, content_id: item.id, address },
        timeout: 120000,
      })
      onchainPaying.value = false
      if (dl?.data) {
        triggerDownload(dl.data, item)
        closePayModal()
      } else {
        lnError.value = dl?.error || 'Paid, but the download failed. Try again shortly.'
      }
      return
    }
  } catch {
    // transient — keep polling
  }
  if (payItem.value && onchainPaying.value) {
    onchainPollTimer = setTimeout(() => pollOnchain(address), 5000)
  }
}

/** Spendable balance for a given ecash backend in the current plan. */
function ecashBalanceOf(b: EcashBackend): number {
  if (!ecashPlan.value) return 0
  return b === 'cashu' ? ecashPlan.value.cashu : b === 'fedimint' ? ecashPlan.value.fedimint : ecashPlan.value.ark
}

/**
 * Step 1b: look at BOTH ecash balances, pick the backend that covers the price
 * (Cashu preferred, else Fedimint), and show a confirmation screen so the user
 * sees exactly which wallet is spent and can switch before committing (#3).
 */
async function prepareEcashPay() {
  const item = payItem.value
  if (!item) return
  const price = getItemPrice(item.access)
  ecashPreparing.value = true
  purchaseError.value = null
  try {
    let cashu = 0
    let fedimint = 0
    let ark = 0
    try {
      const res = await rpcClient.call<{ cashu_sats?: number; fedimint_sats?: number; ark_sats?: number; total_sats?: number; balance_sats?: number }>({
        method: 'wallet.ecash-balance',
      })
      cashu = res?.cashu_sats ?? res?.balance_sats ?? 0
      fedimint = res?.fedimint_sats ?? 0
      ark = res?.ark_sats ?? 0
    } catch {
      // Couldn't read balances — let the user try anyway (auto backend).
    }
    const total = cashu + fedimint + ark
    // Prefer Cashu when it covers the price, else Fedimint, else Ark, else
    // leave null (insufficient — shown in the confirm screen, Confirm disabled).
    const chosen: EcashBackend | null =
      cashu >= price ? 'cashu' : fedimint >= price ? 'fedimint' : ark >= price ? 'ark' : null
    ecashPlan.value = { cashu, fedimint, ark, total, chosen }
    if (!chosen) {
      purchaseError.value = `Not enough funds: Cashu ${cashu} + Fedimint ${fedimint} + Ark ${ark} sats, need ${price}. Fund a wallet, or pay another way.`
    }
    payMode.value = 'ecash-confirm'
  } finally {
    ecashPreparing.value = false
  }
}

/** Confirm the ecash payment with the backend the user selected. */
async function confirmEcashPay() {
  const item = payItem.value
  const onion = props.peerId || currentPeer.value?.onion
  const method = ecashPlan.value?.chosen
  if (!item || !onion || !method) return
  const price = getItemPrice(item.access)

  downloading.value = item.id
  purchaseError.value = null
  try {
    const result = await rpcClient.call<{ data?: string; error?: string; ecash_backend?: string; mime_type?: string }>({
      method: 'content.download-peer-paid',
      params: { onion, content_id: item.id, price_sats: price, method, filename: item.filename },
      timeout: 120000,
    })
    if (result?.data) {
      // The purchase is now cached + owned by this node (backend persisted it).
      // Mark it owned and open the in-app viewer rather than firing a browser
      // download — the latter silently fails on the mobile companion, which is
      // why a paid item used to never "unlock". The item stays unlocked going
      // forward; the viewer offers a Save button for an explicit download.
      ownedKeys.value = new Set(ownedKeys.value).add(ownKey(onion, item.id))
      const mime = result.mime_type || item.mime_type
      // A just-bought song goes straight to the bottom-bar player — the
      // owned-content lightbox is for images/video only.
      if (mime.startsWith('audio/')) {
        audioPlayer.play(
          URL.createObjectURL(base64ToBlob(result.data, mime)),
          item.filename.split('/').pop() || item.filename,
        )
      } else {
        if (viewerUrl.value) URL.revokeObjectURL(viewerUrl.value)
        viewerUrl.value = URL.createObjectURL(base64ToBlob(result.data, mime))
        viewerMime.value = mime
        viewerItem.value = item
      }
      closePayModal()
      void loadOwned()
    } else if (result?.error) {
      // Keep the confirm screen open so the user can switch backend and retry.
      purchaseError.value = result.error
    }
  } catch (e: unknown) {
    purchaseError.value = e instanceof Error ? e.message : 'Download failed'
  } finally {
    downloading.value = null
  }
}

/** Ask the seller for a Lightning invoice, render a QR, and poll for payment. */
async function payWithInvoice() {
  const item = payItem.value
  const onion = props.peerId || currentPeer.value?.onion
  if (!item || !onion) return

  payMode.value = 'qr'
  invoiceError.value = ''
  invoiceWaiting.value = true
  try {
    const res = await rpcClient.call<{ bolt11?: string; payment_hash?: string; price_sats?: number; error?: string }>({
      method: 'content.request-invoice',
      params: { onion, content_id: item.id },
      timeout: 45000,
    })
    if (!res?.bolt11 || !res?.payment_hash) {
      invoiceError.value = res?.error || 'The seller could not create an invoice (is its Lightning node running?).'
      invoiceWaiting.value = false
      return
    }
    invoiceData.value = { bolt11: res.bolt11, payment_hash: res.payment_hash, price_sats: res.price_sats ?? getItemPrice(item.access) }
    try {
      invoiceQr.value = await QRCode.toDataURL(res.bolt11.toUpperCase(), { margin: 1, width: 240 })
    } catch {
      invoiceQr.value = '' // fall back to showing the raw invoice string
    }
    scheduleInvoicePoll()
  } catch (e: unknown) {
    invoiceError.value = e instanceof Error ? e.message : 'Could not request an invoice'
    invoiceWaiting.value = false
  }
}

/**
 * Pay the seller's invoice straight from THIS node's Lightning wallet, then
 * release the file. payLightningInvoice resolves to a real terminal state, so
 * on success the payment_hash is immediately valid as the download gate token.
 */
async function payWithLightning() {
  const item = payItem.value
  const onion = props.peerId || currentPeer.value?.onion
  if (!item || !onion || lnPaying.value) return

  lnPaying.value = true
  lnError.value = ''
  try {
    // 1. Ask the seller to mint a bolt11 (also records the pending entitlement).
    const inv = await rpcClient.call<{ bolt11?: string; payment_hash?: string; error?: string }>({
      method: 'content.request-invoice',
      params: { onion, content_id: item.id },
      timeout: 45000,
    })
    if (!inv?.bolt11 || !inv?.payment_hash) {
      lnError.value = inv?.error || 'The seller could not create an invoice (is its Lightning node running?).'
      return
    }
    // 2. Pay it from our own node. Tracked to a REAL terminal state — a slow
    // multi-hop route resolves via status polling instead of a false failure.
    const pay = await rpcClient.payLightningInvoice({ payment_request: inv.bolt11 })
    if (pay.status === 'failed') {
      lnError.value = `Payment failed: ${pay.failure_reason || 'unknown reason'}`
      return
    }
    if (pay.status === 'pending') {
      lnError.value = 'Payment is still settling — this can take a few minutes. Check your wallet transactions before paying again.'
      return
    }
    // 3. Settled — pull the file using the payment hash as the gate token.
    const dl = await rpcClient.call<{ data?: string; error?: string }>({
      method: 'content.download-peer-invoice',
      params: { onion, content_id: item.id, payment_hash: inv.payment_hash },
      timeout: 120000,
    })
    if (dl?.data) {
      triggerDownload(dl.data, item)
      closePayModal()
    } else {
      lnError.value = dl?.error || 'Paid, but the download failed. Try again shortly.'
    }
  } catch (e: unknown) {
    lnError.value = e instanceof Error ? e.message : 'Could not pay from your Lightning node'
  } finally {
    lnPaying.value = false
  }
}

function scheduleInvoicePoll() {
  if (invoicePollTimer) clearTimeout(invoicePollTimer)
  invoicePollTimer = setTimeout(pollInvoice, 1000)
}

async function pollInvoice() {
  const item = payItem.value
  const inv = invoiceData.value
  const onion = props.peerId || currentPeer.value?.onion
  if (!item || !inv || !onion) return
  try {
    const res = await rpcClient.call<{ paid?: boolean }>({
      method: 'content.invoice-status',
      params: { onion, content_id: item.id, payment_hash: inv.payment_hash },
      timeout: 30000,
    })
    if (res?.paid) {
      // Settled — pull the file using the payment hash as the gate token.
      invoiceWaiting.value = false
      const dl = await rpcClient.call<{ data?: string; error?: string }>({
        method: 'content.download-peer-invoice',
        params: { onion, content_id: item.id, payment_hash: inv.payment_hash },
        timeout: 120000,
      })
      if (dl?.data) {
        triggerDownload(dl.data, item)
        closePayModal()
      } else {
        invoiceError.value = dl?.error || 'Paid, but the download failed. Try again shortly.'
      }
      return
    }
  } catch {
    // Transient — keep polling.
  }
  if (payItem.value && invoiceData.value) scheduleInvoicePoll()
}

async function copyInvoice() {
  if (!invoiceData.value) return
  try {
    await navigator.clipboard.writeText(invoiceData.value.bolt11)
    invoiceCopied.value = true
    setTimeout(() => { invoiceCopied.value = false }, 2000)
  } catch { /* clipboard blocked */ }
}

/** Play audio/video inline. For free items, downloads full file; for paid, uses the preview. */
async function playMedia(item: CatalogItem) {
  const onion = props.peerId || currentPeer.value?.onion
  if (!onion) return

  const paid = isPaidItem(item.access)

  // Free content: stream via the Range-capable proxy (B3) so the player can
  // seek and start instantly, instead of downloading the whole file as a
  // base64 blob into a non-seekable Blob URL (which broke video/large audio).
  if (!paid) {
    const streamUrl = `/api/peer-content/${encodeURIComponent(onion)}/${encodeURIComponent(item.id)}`
    if (item.mime_type.startsWith('audio/')) {
      audioPlayer.play(streamUrl, item.filename.split('/').pop() || item.filename)
    } else if (item.mime_type.startsWith('video/')) {
      videoPlayerItem.value = item
      videoPlayerUrl.value = streamUrl
      videoPlayerPaid.value = false
    }
    return
  }

  // Paid content: use the preview/download flow below.
  // If we already have a preview blob URL, use it
  const existingUrl = previewUrls[item.id]
  if (existingUrl) {
    if (item.mime_type.startsWith('audio/')) {
      audioPlayer.play(existingUrl, item.filename.split('/').pop() || item.filename)
    } else if (item.mime_type.startsWith('video/')) {
      videoPlayerItem.value = item
      videoPlayerUrl.value = existingUrl
      videoPlayerPaid.value = paid
    }
    return
  }

  // Download the content (preview for paid, full for free)
  playing.value = item.id
  try {
    const method = paid ? 'content.preview-peer' : 'content.download-peer'
    const result = await rpcClient.call<{ data?: string; content_type?: string }>({
      method,
      params: { onion, content_id: item.id },
      timeout: 120000,
    })

    if (result?.data) {
      const mime = result.content_type || item.mime_type
      const bytes = Uint8Array.from(atob(result.data), c => c.charCodeAt(0))
      const blob = new Blob([bytes], { type: mime })
      const blobUrl = URL.createObjectURL(blob)
      previewUrls[item.id] = blobUrl

      if (item.mime_type.startsWith('audio/')) {
        audioPlayer.play(blobUrl, item.filename.split('/').pop() || item.filename)
      } else if (item.mime_type.startsWith('video/')) {
        videoPlayerItem.value = item
        videoPlayerUrl.value = blobUrl
        videoPlayerPaid.value = paid
      }
    } else {
      purchaseError.value = 'No preview data returned — peer may be offline'
    }
  } catch (e: unknown) {
    purchaseError.value = e instanceof Error ? e.message : 'Failed to load media — peer may be offline'
  } finally {
    playing.value = null
  }
}

function closeVideoPlayer() {
  videoPlayerItem.value = null
  videoPlayerUrl.value = null
  videoPlayerPaid.value = false
  videoLoading.value = false
  videoError.value = false
}

// Show the loader the moment a video opens; the element's playing/canplay/error
// events clear it.
watch(videoPlayerUrl, (url) => {
  if (url) {
    videoLoading.value = true
    videoError.value = false
  }
})

/**
 * Probe the streaming proxy with a 1-byte Range request so we can report the
 * real failure reason (peer offline on both mesh and Tor) before kicking off a
 * browser-managed download that can't surface HTTP status. Returns `true` on
 * success, or a human-readable error string. Aborts immediately on success so
 * a large body is never drained.
 */
async function probePeerContent(url: string): Promise<true | string> {
  const controller = new AbortController()
  try {
    const res = await fetch(url, {
      headers: { Range: 'bytes=0-0' },
      signal: controller.signal,
    })
    if (res.ok || res.status === 206) {
      controller.abort()
      return true
    }
    let msg = `Peer returned an error (${res.status})`
    try {
      const body = await res.json()
      if (body?.error) msg = String(body.error)
    } catch {
      // non-JSON error body — keep the status-based message
    }
    return msg
  } catch (e) {
    return e instanceof Error ? e.message : 'Could not reach the peer — it may be offline'
  }
}

/**
 * Stream a free file to disk via the Range-capable proxy. The browser manages
 * the transfer with constant memory (no base64, no in-memory Blob), so large
 * files download reliably (#38). Same-origin, so the session cookie rides along.
 */
function streamDownload(url: string, item: CatalogItem) {
  const a = document.createElement('a')
  a.href = url
  a.download = item.filename.split('/').pop() || item.filename
  document.body.appendChild(a)
  a.click()
  a.remove()
}

function triggerDownload(base64Data: string, item: CatalogItem) {
  const blob = new Blob(
    [Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))],
    { type: item.mime_type },
  )
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = item.filename.split('/').pop() || item.filename
  a.click()
  URL.revokeObjectURL(url)
}

defineExpose({ loadCatalog })
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
