// Self-serve subscription purchase (multi-mode / cloud only).
//
// Lets a signed-in cloud user buy their OWN prepaid Pro/Max period
// instead of waiting for the operator to grant it by hand. The relay
// owns the subscription (keyed by Recaps user-id, per the core-
// decoupling); Recaps just brokers the purchase:
//
//   POST /api/billing/buy     → ask the relay to mint a BTCPay invoice
//                               for {tier}; return the checkout URL the
//                               frontend opens. On settlement the relay's
//                               webhook extends the user's tier.
//   GET  /api/billing/status  → pull the user's current (expiry-enforced)
//                               tier from the relay and refresh the local
//                               users.tier cache so the badge flips the
//                               moment payment lands. The frontend polls
//                               this after opening checkout.
//
// Auth: both routes require a real signed-in user (req.user.id). Anon /
// trial visitors (req.userId = "anon:<cookie>") are refused — a tier is
// keyed to a durable user-id, which a trial cookie isn't.
//
// These live under /api/billing (NOT /api/subscriptions — that prefix is
// the channel-subscriptions feature, which is itself Pro-gated; a free
// user must be able to reach the buy flow). The prefix is added to the
// license middleware's open list so the activation gate lets Core users
// through to purchase.

import { getDb } from "./db.js";
import { requireUser } from "./tenant-auth.js";
import {
  createRelayTierInvoice,
  createRelayZapriteOrder,
  getRelayUserTier,
  getRelayTierPlans,
} from "./providers/relay.js";

const BUYABLE_TIERS = new Set(["pro", "max"]);
const PAYMENT_METHODS = new Set(["bitcoin", "card"]);

// Fallback prices (sats / 30-day period) used only when the relay is
// unreachable while rendering the picker — matches the relay config
// defaults so the UI never shows a blank price. The actual charge is
// always computed relay-side at invoice time.
const FALLBACK_PLANS = {
  period_days: 30,
  plans: [
    { tier: "pro", sats: 21000 },
    { tier: "max", sats: 42000 },
  ],
};

// Pull the user's effective (expiry-enforced) tier from the relay — the
// authoritative subscription owner — and update the cached users.tier if
// it drifted. Returns { tier, expires_at, synced } or { synced:false }
// when the relay is unreachable / unconfigured (caller falls back to the
// cached value rather than erroring the request).
export async function syncUserTierFromRelay(userId) {
  if (!userId) return { synced: false };
  let report;
  try {
    report = await getRelayUserTier({ userId });
  } catch (err) {
    console.warn(
      `[billing] relay tier read failed for ${userId}: ${err?.message || err}`,
    );
    return { synced: false };
  }
  // getRelayUserTier swallows errors and returns null when the relay
  // base URL / operator key isn't configured. Treat that as "can't
  // sync" rather than "downgrade to core".
  if (!report || typeof report.tier !== "string") {
    return { synced: false };
  }
  const tier = report.tier; // already expiry-enforced by the relay
  const expiresAt = report.subscription_expires_at || null;
  try {
    const db = getDb();
    const row = db.prepare("SELECT tier FROM users WHERE id = ?").get(userId);
    if (row && row.tier !== tier) {
      db.prepare("UPDATE users SET tier = ? WHERE id = ?").run(tier, userId);
      console.log(
        `[billing] synced ${userId} tier ${row.tier || "core"} → ${tier} from relay`,
      );
    }
  } catch (err) {
    console.warn(
      `[billing] tier cache update failed for ${userId}: ${err?.message || err}`,
    );
  }
  return {
    tier,
    expires_at: expiresAt,
    tier_snapshot: report.tier_snapshot || tier,
    subscription_expired: !!report.subscription_expired,
    synced: true,
  };
}

// Build the buyer-facing origin so the BTCPay checkout can redirect back
// to the app after settlement. Honors the reverse-proxy forwarding
// headers StartOS sets in front of the service.
function originFor(req) {
  const proto =
    (req.headers["x-forwarded-proto"] || "").split(",")[0].trim() ||
    req.protocol ||
    "https";
  const host = req.headers["x-forwarded-host"] || req.headers.host || "";
  return host ? `${proto}://${host}` : "";
}

export function setupBillingRoutes(app) {
  // GET /api/billing/plans → { period_days, plans: [{tier, sats}] }
  // Powers the purchase picker. Prices come from the relay (the pricing
  // source of truth); falls back to the config defaults if the relay is
  // briefly unreachable so the modal still renders.
  app.get("/api/billing/plans", requireUser, async (req, res) => {
    if (!req.user || !req.user.id) {
      return res.status(403).json({ error: "must_be_signed_in" });
    }
    try {
      const data = await getRelayTierPlans();
      if (data && Array.isArray(data.plans) && data.plans.length) {
        return res.json({
          period_days: data.period_days || FALLBACK_PLANS.period_days,
          plans: data.plans,
          // Whether the card (Zaprite) rail is configured — the UI hides
          // the "Pay by card" link when false so it never offers a rail
          // that 503s. Bitcoin is always available (the BTCPay rail).
          card_available: !!data.card_available,
          source: "relay",
        });
      }
    } catch (err) {
      console.warn(`[billing] plans read failed: ${err?.message || err}`);
    }
    return res.json({ ...FALLBACK_PLANS, card_available: false, source: "fallback" });
  });

  // POST /api/billing/buy  body: { tier: "pro" | "max", method?: "bitcoin" | "card" }
  // Bitcoin (default) → BTCPay invoice; card → Zaprite hosted checkout.
  // Returns { ok, method, checkout_url, tier, period_days, ... }.
  app.post("/api/billing/buy", requireUser, async (req, res) => {
    // Must be a real signed-in user — a tier is keyed to a durable
    // user-id, not an anon trial cookie.
    if (!req.user || !req.user.id) {
      return res.status(403).json({
        error: "must_be_signed_in",
        message: "Sign in to buy a subscription.",
      });
    }
    const tier = String(req.body?.tier || "").trim().toLowerCase();
    if (!BUYABLE_TIERS.has(tier)) {
      return res.status(400).json({
        error: "bad_tier",
        message: 'tier must be "pro" or "max"',
      });
    }
    const method = String(req.body?.method || "bitcoin").trim().toLowerCase();
    if (!PAYMENT_METHODS.has(method)) {
      return res.status(400).json({
        error: "bad_method",
        message: 'method must be "bitcoin" or "card"',
      });
    }
    const origin = originFor(req);
    // Land back on the app with a marker the frontend uses to kick an
    // immediate status sync (the modal also polls, so this is a courtesy
    // for buyers who follow the checkout redirect).
    const returnUrl = origin ? `${origin}/?billing=success` : null;
    try {
      if (method === "card") {
        const order = await createRelayZapriteOrder({
          userId: req.user.id,
          tier,
          returnUrl,
        });
        return res.json({
          ok: true,
          method: "card",
          checkout_url: order.checkout_url || null,
          order_id: order.order_id || null,
          amount: order.amount ?? null,
          currency: order.currency || null,
          tier: order.tier || tier,
          period_days: order.period_days ?? null,
        });
      }
      // Bitcoin (default) — BTCPay invoice.
      const invoice = await createRelayTierInvoice({
        userId: req.user.id,
        tier,
        returnUrl,
      });
      res.json({
        ok: true,
        method: "bitcoin",
        checkout_url: invoice.checkout_url || null,
        invoice_id: invoice.invoice_id || null,
        sats: invoice.sats ?? null,
        tier: invoice.tier || tier,
        period_days: invoice.period_days ?? null,
        // Lightning BOLT11 for the inline QR (no redirect). Null → the app
        // falls back to opening the hosted checkout_url.
        bolt11: invoice.bolt11 || null,
        lightning_payment_link: invoice.lightning_payment_link || null,
        lightning_expires_at: invoice.lightning_expires_at || null,
      });
    } catch (err) {
      const status = err?.status || 502;
      console.error(
        `[billing] buy failed for ${req.user.id} (${tier}/${method}): ${err?.message || err}`,
      );
      // 503 from the relay = that rail isn't configured; surface a hint.
      const notConfigured =
        status === 503 || /not[_ ]configured/i.test(err?.message || "");
      const rail = method === "card" ? "Card" : "Bitcoin";
      const tool = method === "card" ? "Zaprite" : "BTCPay";
      res.status(notConfigured ? 503 : 502).json({
        error: notConfigured ? "payments_unavailable" : "checkout_failed",
        message: notConfigured
          ? `${rail} payments aren't set up on this server yet. Ask the operator to configure ${tool}.`
          : "Couldn't start the payment. Please try again in a moment.",
      });
    }
  });

  // GET /api/billing/status
  // Returns { tier, expires_at, synced } — the user's current relay-owned
  // tier, with the local cache refreshed as a side effect.
  app.get("/api/billing/status", requireUser, async (req, res) => {
    if (!req.user || !req.user.id) {
      return res.status(403).json({ error: "must_be_signed_in" });
    }
    const synced = await syncUserTierFromRelay(req.user.id);
    if (synced.synced) {
      return res.json({
        tier: synced.tier,
        expires_at: synced.expires_at,
        tier_snapshot: synced.tier_snapshot,
        subscription_expired: synced.subscription_expired,
        synced: true,
      });
    }
    // Relay unreachable / unconfigured — fall back to the cached tier so
    // the UI still renders a sane badge instead of erroring.
    return res.json({
      tier: req.user.tier || "core",
      expires_at: null,
      synced: false,
    });
  });
}
