<script lang="ts">
  import { onMount } from 'svelte';
  import Modal from '../components/Modal.svelte';
  import { app, handleError, showToast } from '../lib/app.svelte';
  import { navigate } from '../lib/router.svelte';
  import { gateway } from '../lib/gateway';
  import { normalizeSubtype } from '../lib/platform';
  import { t, i18n, type MessageKey } from '../lib/i18n/index.svelte';
  import { directPurchaseNeedsEmail, executionContext, formatAmount, needsWebSpendWarning, spendableChips, type SpendContext } from '../lib/wallet';
  import { isGooglePlayBuild, purchasesHidden } from '../lib/distribution';
  import { onExternalLinkClick, onInAppPageLinkClick, openExternalUrl } from '../lib/links';
  import { gatewayOrigin } from '../lib/origin';
  import { vkPlatform, vkShowOrderBox } from '../lib/vk';
  import { telegramOpenInvoice } from '../lib/telegram';
  import { rewardedReady, showRewarded } from '../lib/ads';
  import { GatewayError } from '../lib/client';
  import type { Wallet, Catalog, CatalogProduct } from '../lib/model';

  // The Wallet section: a compact chip balance, the active benefits, and the storefront. The store
  // splits into "buy chips" (chip packs, funded with money — a provider redirect) and "spend chips"
  // (values bought with chips — an instant exchange). When purchases are hidden — the Google Play
  // build (behind a RuStore stub, store policy) or the native MVP that defers billing (a neutral
  // note) — the money purchases disappear while spending already-earned chips still works.

  let wallet = $state<Wallet | null>(null);
  let catalog = $state<Catalog | null>(null);
  let loading = $state(true);
  // The product whose purchase is in flight, or null. It both guards against a concurrent purchase
  // (busy) and scopes the pressed/dimmed visual to the tapped button — not every buy button.
  let busyId = $state<string | null>(null);
  const busy = $derived(busyId !== null);
  const context: SpendContext = executionContext();
  // The storefront half in view: buying chips for money, or spending chips on values.
  let tab = $state<'buy' | 'spend'>('buy');
  // The value awaiting an exchange confirmation, or null. Every chip-spend confirms (it is instant —
  // no provider redirect to stand in as a confirmation), and the dialog folds in the cross-platform
  // warning when the spend would draw vk/tg chips.
  let spendProduct = $state<CatalogProduct | null>(null);
  const spendWarn = $derived(
    !!spendProduct && needsWebSpendWarning(context, wallet?.segments ?? [], spendProduct.chips),
  );

  const gpBuild = isGooglePlayBuild();
  // Money purchases are hidden in the Google Play build (policy) and in the native MVP that defers
  // billing; the RuStore-pointer stub shows only for the former (gpBuild).
  const noPurchases = purchasesHidden();
  // Money purchases are not permitted inside the VK iOS app (Apple ToS); the pack CTA is shown
  // muted and taps explain why, rather than hiding the storefront. VK's device family is not on the
  // client platformSubtype (server-derived) — read it from the signed vk_platform launch param.
  const purchaseBlocked = context === 'vk' && normalizeSubtype(vkPlatform()) === 'ios';
  // The server enforces the direct rail's email anchor; showing the rule here means a player signed
  // in through VK or Telegram in a browser reads it instead of tapping Buy and getting a bare error.
  const needsEmail = $derived(directPurchaseNeedsEmail(context, app.profile?.email ?? ''));
  // The "other version" link points at this deployment's own site root (the landing lists every
  // platform), so it follows prod vs the contour without a hardcoded host.
  const siteRoot = `${gatewayOrigin()}/`;

  const values = $derived(catalog?.products.filter((p) => p.kind === 'value') ?? []);
  const packs = $derived(catalog?.products.filter((p) => p.kind === 'pack') ?? []);
  // The chips the player can actually spend in this context — a value costing more is out of reach,
  // so its exchange button is disabled (the server stays authoritative and also rejects it).
  const spendable = $derived(wallet ? spendableChips(wallet) : 0);

  // The balance reads as the context's own segment first (🪙 N), with any linked other-platform
  // segments appended behind their logo — on the web all linked segments show and spend together;
  // inside a VK/Telegram store only that store's own segment is present, so it stands alone.
  const mainSeg = $derived(wallet?.segments.find((s) => s.source === context));
  const linkedSegs = $derived((wallet?.segments ?? []).filter((s) => s.source !== context));

  // The active benefits, one short phrase each, only for what the player actually holds — nothing
  // to show (ad-free lapsed and no hints) hides the whole section.
  const activeItems = $derived.by(() => {
    const items: string[] = [];
    if (!wallet) return items;
    if (wallet.hints > 0) items.push(t('wallet.activeHints', { n: wallet.hints }));
    if (wallet.adsForever) items.push(t('wallet.noAdsForever'));
    else if (wallet.adsPaidUntilMs > Date.now()) {
      const date = new Date(wallet.adsPaidUntilMs).toLocaleDateString(i18n.locale === 'ru' ? 'ru-RU' : 'en-US');
      items.push(t('wallet.noAdsUntil', { date }));
    }
    return items;
  });

  // Rewarded video (VK ads): the "watch for chips" CTA shows only when the server offers a payout in
  // this context (wallet.rewardChips > 0 — VK, configured) and an ad is ready to show.
  let rewardReady = $state(false);
  const canReward = $derived(!!wallet && wallet.rewardChips > 0 && rewardReady);

  // onWatchAd shows a rewarded ad and, if watched, credits the payout server-side (client-attested +
  // a server daily/hourly cap). A stub view (contour test) shows an "ad fired" marker; a reached cap
  // is surfaced as its own toast, distinct from a generic error.
  async function onWatchAd() {
    if (busy) return;
    busyId = 'reward';
    try {
      const res = await showRewarded();
      if (res.stub) showToast('ad fired');
      if (!res.watched) {
        showToast(t('wallet.rewardFailed'));
        return;
      }
      const nonce = crypto.randomUUID();
      const before = wallet?.segments.find((s) => s.source === 'vk')?.chips ?? 0;
      const w = await gateway.walletReward(nonce);
      wallet = w;
      const gained = (w.segments.find((s) => s.source === 'vk')?.chips ?? 0) - before;
      showToast(gained > 0 ? t('wallet.rewardCredited', { n: gained }) : t('wallet.rewardCredited', { n: w.rewardChips }));
    } catch (e) {
      if (e instanceof GatewayError && e.code === 'reward_capped') {
        showToast(t('wallet.rewardCapped'));
      } else {
        handleError(e);
      }
    } finally {
      busyId = null;
    }
  }

  async function load() {
    try {
      const [w, c] = await Promise.all([gateway.wallet(), gateway.catalog()]);
      wallet = w;
      catalog = c;
    } catch (e) {
      handleError(e);
    } finally {
      loading = false;
    }
  }
  onMount(() => {
    void load();
    // Probe rewarded-ad readiness once so the "watch for chips" button only shows when a view would
    // actually play.
    void rewardedReady().then((r) => (rewardReady = r));
    // Fallback to the payment push: re-fetch when the tab regains focus, i.e. the user returned
    // from the provider's payment window.
    const onVisible = () => {
      if (!document.hidden) void load();
    };
    document.addEventListener('visibilitychange', onVisible);
    return () => document.removeEventListener('visibilitychange', onVisible);
  });

  // Main path: a payment-intake push bumps app.walletRefresh; re-fetch in place when it changes
  // (a credit landed while the screen is open).
  let seenRefresh = app.walletRefresh;
  $effect(() => {
    if (app.walletRefresh !== seenRefresh) {
      seenRefresh = app.walletRefresh;
      void load();
    }
  });

  function sourceLabel(source: string): string {
    return t(`wallet.source.${source}` as MessageKey);
  }

  function currencyLabel(currency: string): string {
    return t(`wallet.cur.${currency}` as MessageKey);
  }

  function platformLogo(source: string): string {
    return source === 'vk' ? 'vk-logo.svg' : 'telegram-logo.svg';
  }

  // onSpend opens the exchange confirmation for a chip-priced value; doBuy runs the actual spend.
  function onSpend(p: CatalogProduct) {
    spendProduct = p;
  }

  async function doBuy(p: CatalogProduct) {
    spendProduct = null;
    if (busy) return;
    busyId = p.productId;
    try {
      wallet = await gateway.walletBuy(p.productId);
    } catch (e) {
      handleError(e);
    } finally {
      busyId = null;
    }
  }

  // onOrder funds a chip pack with money: it opens a pending order and sends the player to the
  // provider's hosted-payment page. The chips are credited later, by the verified server callback;
  // paying accepts the public offer (linked below the packs).
  async function onOrder(p: CatalogProduct) {
    if (busy) return;
    busyId = p.productId;
    try {
      const order = await gateway.walletOrder(p.productId);
      if (context === 'vk') {
        // VK settles the payment in-app via the bridge; the chips arrive on the server callback.
        await vkShowOrderBox(order.orderId);
      } else if (context === 'telegram') {
        // Telegram Stars: the bot minted an invoice link (returned as redirectUrl); open it in-app.
        // The chips are credited server-side by the forwarded payment and arrive on the live push;
        // refresh on 'paid' as a fallback. Outside a Stars-capable client, fall back to the link.
        const opened = telegramOpenInvoice(order.redirectUrl, (status) => {
          if (status === 'paid') void load();
        });
        if (!opened) openExternalUrl(order.redirectUrl);
      } else {
        openExternalUrl(order.redirectUrl);
      }
    } catch (e) {
      // A rail kill switch (or a per-account block) returns payment_unavailable with an operator's
      // localized explanation — show it to the user instead of the generic error.
      if (e instanceof GatewayError && e.code === 'payment_unavailable') {
        showToast(e.message);
      } else {
        handleError(e);
      }
    } finally {
      busyId = null;
    }
  }
</script>

<div class="wallet" data-testid="wallet">
  <div class="balance" data-testid="balance">
    <span class="coin" aria-hidden="true">🪙</span><span class="num" class:dim={mainSeg && !mainSeg.spendable}>{mainSeg?.chips ?? 0}</span>
    {#each linkedSegs as s (s.source)}
      <span class="plus" aria-hidden="true">+</span><img class="plogo" src={platformLogo(s.source)} width="16" height="16" alt={sourceLabel(s.source)} /><span class="num" class:dim={!s.spendable}>{s.chips}</span>
    {/each}
  </div>

  {#if activeItems.length > 0}
    <section data-testid="active">
      <h3>{t('wallet.active')}</h3>
      <div class="row"><span class="name">{activeItems.join('. ')}</span></div>
    </section>
  {/if}

  <section>
    <h3>{t('wallet.store')}</h3>
    <div class="seg">
      <button class="opt" class:active={tab === 'buy'} data-testid="tab-buy" onclick={() => (tab = 'buy')}>{t('wallet.tabBuy')}</button>
      <button class="opt" class:active={tab === 'spend'} data-testid="tab-spend" onclick={() => (tab = 'spend')}>{t('wallet.tabSpend')}</button>
    </div>

    {#if tab === 'buy'}
      {#if purchaseBlocked}
        <!-- prettier-ignore -->
        <p class="ios-note" data-testid="ios-note">{t('wallet.iosBlockedPre')}<a href={siteRoot} target="_blank" rel="noopener noreferrer" onclick={onExternalLinkClick}>{t('wallet.iosBlockedLink')}</a>{t('wallet.iosBlockedPost')}</p>
      {/if}
      {#if canReward}
        <div class="row reward" data-testid="reward-row">
          <span class="name">{t('wallet.rewardWatch', { n: wallet?.rewardChips ?? 0 })}</span>
          <button class="buy" class:working={busyId === 'reward'} data-testid="watch-ad" disabled={busy} onclick={onWatchAd}
            >{t('wallet.rewardCta')}</button
          >
        </div>
      {/if}
      {#if noPurchases}
        {#if gpBuild}
          <p class="stub" data-testid="gp-stub">{t('wallet.gpStub')}</p>
        {:else}
          <p class="stub" data-testid="purchases-hidden">{t('wallet.purchasesSoon')}</p>
        {/if}
      {:else if needsEmail}
        <p class="stub" data-testid="email-required">
          <button class="hintlink" onclick={() => navigate('/profile')}>{t('wallet.emailToBuy')}</button>
        </p>
      {:else}
        {#each packs as p (p.productId)}
          <div class="row product" data-testid="product" data-kind="pack" data-pid={p.productId}>
            <span class="name">{p.title}</span>
            <span class="price">{formatAmount(p.moneyAmount, p.moneyCurrency)}&nbsp;{currencyLabel(p.moneyCurrency)}</span>
            <button
              class="buy"
              class:blocked={purchaseBlocked}
              class:working={busyId === p.productId}
              data-testid="buy-pack"
              disabled={busy}
              onclick={() => {
                if (purchaseBlocked) showToast(t('wallet.platformNoBuy'));
                else void onOrder(p);
              }}>{t('wallet.buy')}</button
            >
          </div>
        {/each}
        {#if packs.length > 0}
          <p class="offer" data-testid="offer">
            <a href="/offer/" target="_blank" rel="noopener noreferrer" onclick={onInAppPageLinkClick}>{t('wallet.offer')}</a>
          </p>
        {:else if !loading}
          <p class="empty">{t('wallet.empty')}</p>
        {/if}
      {/if}
    {:else}
      {#each values as p (p.productId)}
        <div class="row product" data-testid="product" data-kind="value" data-pid={p.productId}>
          <span class="name">{p.title}</span>
          <span class="price">🪙 {p.chips}</span>
          <button class="buy" class:working={busyId === p.productId} class:cantafford={spendable < p.chips} data-testid="buy" disabled={busy || spendable < p.chips} onclick={() => onSpend(p)}
            >{t('wallet.spend')}</button
          >
        </div>
      {/each}
      {#if !loading && values.length === 0}
        <p class="empty">{t('wallet.empty')}</p>
      {/if}
    {/if}
  </section>
</div>

{#if spendProduct}
  <Modal title={t('wallet.spendTitle')} onclose={() => (spendProduct = null)}>
    <p class="spend-body" data-testid="spend-body">{t('wallet.spendBody', { n: spendProduct.chips, title: spendProduct.title })}</p>
    {#if spendWarn}<p class="warn-body" data-testid="warn">{t('wallet.warnBody')}</p>{/if}
    <div class="warn-actions">
      <button class="cancel" onclick={() => (spendProduct = null)}>{t('wallet.warnCancel')}</button>
      <button class="buy" data-testid="spend-confirm" onclick={() => spendProduct && doBuy(spendProduct)}
        >{t('wallet.spendConfirm')}</button
      >
    </div>
  </Modal>
{/if}

<style>
  .wallet {
    display: flex;
    flex-direction: column;
    gap: 18px;
    padding: var(--pad);
  }
  /* The compact balance: the context's own chips (🪙 N) then any linked other-platform segments
     behind their logo, digits monospaced so they align; a non-spendable (view-only) segment dims. */
  .balance {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 5px;
    font-weight: 600;
    font-size: 1.05rem;
  }
  .balance .num {
    font-family: monospace;
  }
  .balance .num.dim {
    opacity: 0.5;
  }
  .balance .plus {
    margin: 0 3px;
    color: var(--text-muted);
  }
  .balance .plogo {
    display: block;
  }
  section {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  h3 {
    margin: 0;
    font-size: 0.95rem;
    color: var(--text-muted);
  }
  /* The buy/spend segmented control, matching the New Game type selector. */
  .seg {
    display: flex;
    gap: 8px;
  }
  .opt {
    flex: 1;
    min-width: 64px;
    padding: 10px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius-sm);
    cursor: pointer;
  }
  .opt.active {
    background: var(--accent);
    color: var(--accent-text);
    border-color: var(--accent);
  }
  .row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
  }
  .name {
    flex: 1 1 auto;
    min-width: 0;
  }
  .price {
    flex: 0 0 auto;
    font-weight: 600;
    white-space: nowrap;
  }
  .buy {
    flex: 0 0 auto;
    padding: 8px 14px;
    border: 1px solid var(--accent);
    background: var(--accent);
    color: var(--accent-text);
    border-radius: var(--radius-sm);
    font-weight: 600;
    cursor: pointer;
  }
  .buy:disabled {
    cursor: default;
  }
  /* The dimmed look is scoped: the tapped button while its purchase is in flight (working), or a
     blocked pack on VK iOS — never every buy button at once when one purchase is busy. */
  .buy.blocked,
  .buy.working,
  .buy.cantafford {
    opacity: 0.5;
  }
  .ios-note {
    margin: 0 0 4px;
    padding: 8px 12px;
    color: var(--text-muted);
    font-size: 0.85rem;
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
  }
  .ios-note a {
    color: var(--accent);
  }
  .empty,
  .stub {
    margin: 0;
    padding: 10px 12px;
    color: var(--text-muted);
  }
  .stub {
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
  }
  /* The whole explanation is the call to action, so the line itself routes to the profile. */
  .hintlink {
    background: none;
    border: none;
    padding: 0;
    color: var(--accent);
    font: inherit;
    text-align: left;
    text-decoration: underline;
    cursor: pointer;
  }
  .offer {
    margin: 2px 0 0;
    text-align: center;
    font-size: 0.85rem;
  }
  .offer a {
    color: var(--text-muted);
  }
  .spend-body {
    margin: 0 0 10px;
  }
  .warn-body {
    margin: 0 0 14px;
    color: var(--text-muted);
    font-size: 0.9rem;
  }
  .warn-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
  }
  .cancel {
    padding: 8px 14px;
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    border-radius: var(--radius-sm);
    cursor: pointer;
  }
</style>
