<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import { hasNip07 } from './lib/nip07';
import { parseCatalogOpml, type CatalogEntry } from './lib/opml';
import { parseFeedXml, type PlayerEpisode, type PlayerFeed } from './lib/rss';
import {
  PaymentRequiredError,
  confirmPurchase,
  fetchCatalog,
  fetchDownloadUrl,
  fetchFeed,
  requestPurchase,
  type PurchaseQuote,
} from './lib/api';
import {
  addSubscription,
  findPurchase,
  loadSubscriptions,
  removeSubscription,
  savePurchase,
} from './lib/storage';
import PurchaseModal from './components/PurchaseModal.vue';
import Player from './components/Player.vue';

const subscriptions = ref(loadSubscriptions());
const newOrigin = ref('');
const activeOrigin = ref<string | null>(null);

const catalog = ref<CatalogEntry[]>([]);
const catalogLoading = ref(false);
const catalogError = ref<string | null>(null);

const activePodcast = ref<CatalogEntry | null>(null);
const feed = ref<PlayerFeed | null>(null);
const feedLoading = ref(false);
const feedError = ref<string | null>(null);

const nowPlaying = ref<{ url: string; title: string } | null>(null);

interface PurchaseState {
  episode: PlayerEpisode;
  quote: PurchaseQuote;
  checking: boolean;
  error: string | null;
}
const purchase = ref<PurchaseState | null>(null);
const episodeBusy = reactive<Record<string, boolean>>({});

function normalizeOrigin(input: string): string | null {
  const trimmed = input.trim().replace(/\/+$/, '');
  if (!trimmed) return null;
  try {
    // eslint-disable-next-line no-new
    new URL(trimmed);
    return trimmed;
  } catch {
    return null;
  }
}

async function selectServer(origin: string): Promise<void> {
  activeOrigin.value = origin;
  activePodcast.value = null;
  feed.value = null;
  catalog.value = [];
  catalogError.value = null;
  catalogLoading.value = true;
  try {
    const xml = await fetchCatalog(origin);
    catalog.value = parseCatalogOpml(xml);
  } catch (err) {
    catalogError.value = (err as Error).message;
  } finally {
    catalogLoading.value = false;
  }
}

async function addServer(): Promise<void> {
  const origin = normalizeOrigin(newOrigin.value);
  if (!origin) {
    catalogError.value = 'enter a full URL, e.g. https://example.com or http://host:8095';
    return;
  }
  subscriptions.value = addSubscription(origin);
  newOrigin.value = '';
  await selectServer(origin);
}

function removeServer(origin: string): void {
  subscriptions.value = removeSubscription(origin);
  if (activeOrigin.value === origin) {
    activeOrigin.value = null;
    catalog.value = [];
    activePodcast.value = null;
    feed.value = null;
  }
}

async function selectPodcast(entry: CatalogEntry): Promise<void> {
  activePodcast.value = entry;
  feed.value = null;
  feedError.value = null;
  feedLoading.value = true;
  try {
    const xml = await fetchFeed(activeOrigin.value!, entry.podcastId);
    feed.value = parseFeedXml(xml);
  } catch (err) {
    feedError.value = (err as Error).message;
  } finally {
    feedLoading.value = false;
  }
}

function play(url: string, title: string): void {
  nowPlaying.value = { url, title };
}

async function playOrBuy(episode: PlayerEpisode): Promise<void> {
  if (!episode.priced) {
    play(episode.enclosureUrl!, episode.title);
    return;
  }
  const origin = activeOrigin.value!;
  const podcastId = activePodcast.value!.podcastId;
  const episodeId = episode.episodeId!;

  const cached = findPurchase(origin, podcastId, episodeId);
  if (cached) {
    play(cached.url, episode.title);
    return;
  }

  if (!hasNip07()) {
    feedError.value = 'This episode is priced — install a NIP-07 Nostr extension (e.g. Alby, nos2x) to buy it.';
    return;
  }

  episodeBusy[episode.guid] = true;
  feedError.value = null;
  try {
    const url = await fetchDownloadUrl(origin, podcastId, episodeId);
    savePurchase({ origin, podcastId, episodeId, url });
    play(url, episode.title);
  } catch (err) {
    if (err instanceof PaymentRequiredError) {
      try {
        const quote = await requestPurchase(origin, podcastId, episodeId);
        purchase.value = { episode, quote, checking: false, error: null };
      } catch (purchaseErr) {
        feedError.value = (purchaseErr as Error).message;
      }
    } else {
      feedError.value = (err as Error).message;
    }
  } finally {
    episodeBusy[episode.guid] = false;
  }
}

async function checkPayment(): Promise<void> {
  const state = purchase.value;
  if (!state) return;
  state.checking = true;
  state.error = null;
  const { episode, quote } = state;
  const origin = activeOrigin.value!;
  const podcastId = activePodcast.value!.podcastId;
  const episodeId = episode.episodeId!;
  try {
    const paid = await confirmPurchase(origin, podcastId, episodeId, quote.quoteId);
    if (!paid) {
      state.error = 'Invoice not paid yet.';
      return;
    }
    const url = await fetchDownloadUrl(origin, podcastId, episodeId);
    savePurchase({ origin, podcastId, episodeId, url });
    play(url, episode.title);
    purchase.value = null;
  } catch (err) {
    state.error = (err as Error).message;
  } finally {
    state.checking = false;
  }
}

const nip07Available = computed(() => hasNip07());
</script>

<template>
  <div class="mx-auto max-w-2xl space-y-6 p-4 pb-32">
    <header>
      <h1 class="text-2xl font-bold text-white">podsteadr player</h1>
      <p class="text-sm text-white/60">Browse, pay, and play podsteadr feeds.</p>
      <p v-if="!nip07Available" class="mt-1 text-xs text-orange-400">
        No NIP-07 extension detected — free episodes still play, but buying priced ones needs one.
      </p>
    </header>

    <section class="card space-y-3">
      <h2 class="label">Servers</h2>
      <form class="flex gap-2" @submit.prevent="addServer">
        <input v-model="newOrigin" class="input" placeholder="https://your-podsteadr-server" />
        <button type="submit" class="btn-primary shrink-0">Add</button>
      </form>
      <ul class="space-y-1">
        <li
          v-for="sub in subscriptions"
          :key="sub.origin"
          class="flex items-center justify-between rounded-lg px-2 py-1"
          :class="activeOrigin === sub.origin ? 'bg-white/10' : ''"
        >
          <button type="button" class="truncate text-left text-sm text-white/90" @click="selectServer(sub.origin)">
            {{ sub.origin }}
          </button>
          <button type="button" class="text-xs text-white/30 hover:text-red-400" @click="removeServer(sub.origin)">
            remove
          </button>
        </li>
      </ul>
    </section>

    <section v-if="activeOrigin" class="card space-y-2">
      <h2 class="label">Podcasts</h2>
      <p v-if="catalogLoading" class="text-sm text-white/50">Loading…</p>
      <p v-else-if="catalogError" class="text-sm text-red-400">{{ catalogError }}</p>
      <p v-else-if="catalog.length === 0" class="text-sm text-white/50">No podcasts on this server yet.</p>
      <ul v-else class="space-y-1">
        <li
          v-for="p in catalog"
          :key="p.podcastId"
          class="flex items-center justify-between rounded-lg px-2 py-1"
          :class="activePodcast?.podcastId === p.podcastId ? 'bg-white/10' : ''"
        >
          <button type="button" class="truncate text-left text-sm text-white/90" @click="selectPodcast(p)">
            {{ p.title }}
          </button>
        </li>
      </ul>
    </section>

    <section v-if="activePodcast" class="card space-y-2">
      <h2 class="text-lg font-semibold text-white">{{ feed?.title ?? activePodcast.title }}</h2>
      <p v-if="feed?.description" class="text-sm text-white/60">{{ feed.description }}</p>
      <p v-if="feedLoading" class="text-sm text-white/50">Loading episodes…</p>
      <p v-else-if="feedError" class="text-sm text-red-400">{{ feedError }}</p>
      <ul v-else class="divide-y divide-white/10">
        <li v-for="ep in feed?.episodes ?? []" :key="ep.guid" class="flex items-center justify-between gap-3 py-3">
          <div class="min-w-0">
            <p class="truncate text-sm font-medium text-white">{{ ep.title }}</p>
            <p class="truncate text-xs text-white/50">{{ ep.description }}</p>
          </div>
          <button
            type="button"
            class="btn-primary shrink-0"
            :disabled="episodeBusy[ep.guid]"
            @click="playOrBuy(ep)"
          >
            {{ episodeBusy[ep.guid] ? '…' : ep.priced ? `Buy ${ep.priceSats ?? ''} sats` : 'Play' }}
          </button>
        </li>
      </ul>
    </section>

    <Player v-if="nowPlaying" :src="nowPlaying.url" :title="nowPlaying.title" class="fixed inset-x-4 bottom-4 mx-auto max-w-2xl" />

    <PurchaseModal
      v-if="purchase"
      :episode-title="purchase.episode.title"
      :amount-sats="purchase.quote.amountSats"
      :invoice="purchase.quote.invoice"
      :checking="purchase.checking"
      :error-message="purchase.error"
      @check="checkPayment"
      @close="purchase = null"
    />
  </div>
</template>
