import { NextResponse } from "next/server";
import crypto from "crypto";
import { sql } from "@/db";
import { getRazorpayConfig } from "@/lib/billing/config";
import { grantPremium, revokeToFree } from "@/lib/billing/entitlements";

/**
 * POST /api/webhooks/razorpay — THE source of truth for entitlement.
 *
 * Razorpay calls this on every subscription lifecycle change. We:
 *   1. Verify the HMAC signature (timing-safe) over the RAW body.
 *   2. Idempotency: unique-insert on x-razorpay-event-id. Duplicate → 200 & bail.
 *   3. Update family_subscriptions by razorpay_subscription_id.
 *   4. Sync entitlement onto families.tier (grant/revoke) so the existing
 *      quota.ts guards enforce the new limits.
 *
 * Status codes (Razorpay retries on non-2xx):
 *   400 — bad signature (do NOT retry)
 *   200 — processed OK, or duplicate (already processed)
 *   500 — processing error (Razorpay WILL retry — that's what we want)
 */

// Razorpay event → how it affects entitlement.
const GRANT_EVENTS: Record<string, string> = {
  "subscription.authenticated": "authenticated",
  "subscription.activated": "active",
  "subscription.charged": "active",
  "subscription.resumed": "active",
  "subscription.pending": "pending", // grace — keep entitled
};
const REVOKE_EVENTS: Record<string, string> = {
  "subscription.halted": "halted",
  "subscription.cancelled": "cancelled",
  "subscription.completed": "completed",
  "subscription.expired": "expired",
  "subscription.paused": "paused",
};

function unixToDate(v: unknown): Date | null {
  const n = typeof v === "number" ? v : Number(v);
  return Number.isFinite(n) && n > 0 ? new Date(n * 1000) : null;
}

export async function POST(req: Request) {
  // RAW body — never req.json() here; the signature is over the exact bytes.
  const rawBody = await req.text();
  const signature = req.headers.get("x-razorpay-signature") ?? "";
  const eventId = req.headers.get("x-razorpay-event-id") ?? "";

  let cfg;
  try {
    cfg = getRazorpayConfig();
  } catch (e) {
    console.error("Razorpay webhook: not configured", e);
    return new NextResponse("not configured", { status: 500 });
  }

  // 1. Verify signature (timing-safe).
  const expected = crypto
    .createHmac("sha256", cfg.webhookSecret)
    .update(rawBody)
    .digest("hex");
  const sigBuf = Buffer.from(signature);
  const expBuf = Buffer.from(expected);
  const validSig = sigBuf.length === expBuf.length && crypto.timingSafeEqual(sigBuf, expBuf);
  if (!validSig) {
    return new NextResponse("bad signature", { status: 400 });
  }

  if (!eventId) {
    // No event id to dedupe on — refuse so Razorpay retries with headers.
    return new NextResponse("missing event id", { status: 400 });
  }

  let event: {
    event?: string;
    payload?: { subscription?: { entity?: Record<string, unknown> } };
  };
  try {
    event = JSON.parse(rawBody);
  } catch {
    return new NextResponse("bad json", { status: 400 });
  }
  const eventType = event.event ?? "unknown";

  // 2. Idempotency — unique insert on event id. If it already exists, bail 200.
  try {
    const inserted = await sql`
      INSERT INTO razorpay_webhook_events (razorpay_event_id, event_type, payload)
      VALUES (${eventId}, ${eventType}, ${rawBody}::jsonb)
      ON CONFLICT (razorpay_event_id) DO NOTHING
      RETURNING id
    `;
    if (inserted.length === 0) {
      return new NextResponse("ok (duplicate)", { status: 200 });
    }
  } catch (e) {
    console.error("Razorpay webhook: log insert failed", e);
    return new NextResponse("log error", { status: 500 }); // retry
  }

  // 3 + 4. Process the event. Errors here → 500 so Razorpay retries.
  try {
    const sub = event.payload?.subscription?.entity;
    const subId = sub?.id as string | undefined;

    // Events without a subscription entity (e.g. payment.* if ever subscribed)
    // are logged above; nothing to sync.
    if (!subId) return new NextResponse("ok (no subscription entity)", { status: 200 });

    // Find our row for this Razorpay subscription.
    const rows = await sql`
      SELECT id, family_id FROM family_subscriptions
      WHERE razorpay_subscription_id = ${subId}
      LIMIT 1
    `;
    const row = rows[0] as { id: string; family_id: string } | undefined;
    if (!row) {
      // Unknown subscription (e.g. created outside this app). Logged; ack so
      // Razorpay stops retrying — there's nothing for us to update.
      console.warn("Razorpay webhook: no family_subscriptions row for", subId);
      return new NextResponse("ok (unknown subscription)", { status: 200 });
    }

    const currentStart = unixToDate(sub?.current_start);
    const currentEnd = unixToDate(sub?.current_end);
    const customerId = (sub?.customer_id as string) ?? null;

    const grantStatus = GRANT_EVENTS[eventType];
    const revokeStatus = REVOKE_EVENTS[eventType];

    if (grantStatus) {
      await sql`
        UPDATE family_subscriptions SET
          status               = ${grantStatus}::subscription_status_enum,
          razorpay_customer_id = COALESCE(${customerId}, razorpay_customer_id),
          current_start        = COALESCE(${currentStart}, current_start),
          current_end          = COALESCE(${currentEnd}, current_end),
          updated_at           = NOW()
        WHERE id = ${row.id}
      `;
      // paused is a GRANT_EVENTS key? No — paused is in REVOKE. resumed→active.
      await grantPremium(row.family_id, grantStatus);
    } else if (revokeStatus) {
      const endedAt = revokeStatus === "paused" ? null : new Date();
      await sql`
        UPDATE family_subscriptions SET
          status      = ${revokeStatus}::subscription_status_enum,
          cancelled_at = ${revokeStatus === "cancelled" ? new Date() : null},
          ended_at    = COALESCE(${endedAt}, ended_at),
          updated_at  = NOW()
        WHERE id = ${row.id}
      `;
      await revokeToFree(row.family_id, revokeStatus);
    } else {
      // Unhandled event type — already logged, ack it.
      return new NextResponse("ok (unhandled event)", { status: 200 });
    }

    return new NextResponse("ok", { status: 200 });
  } catch (e) {
    console.error("Razorpay webhook: processing error", e);
    return new NextResponse("processing error", { status: 500 }); // retry
  }
}
