import { Hono } from "hono";
import { createFactory } from "hono/factory";
import type { AppConfig } from "../config.js";
import { getOrganizationBillingState } from "../helpers/billing.js";
import { MiddlewareManager } from "../middlewares/middleware.js";
import type { AuthEnv, BaseEnv } from "../types/app.types.js";

const DEFAULT_INFINITE_SUBSCRIPTION_ALLOWLIST = new Set([
  "arbelleville@gmail.com",
  "baptiste.belleville74@gmail.com",
  "hugo@xtablo.com",
]);

const BILLING_OWNER_ONLY_ERROR = "Only the organization billing owner can manage billing";

type CheckoutPlan = "solo" | "team" | "founder";

type StripePriceRow = {
  id: string;
  unit_amount: number | null;
  lookup_key: string | null;
  metadata: Record<string, string | null> | null;
};

type CreateCheckoutSessionBody = {
  plan?: CheckoutPlan;
  priceId?: string;
  price_id?: string;
  successUrl?: string;
  success_url?: string;
  cancelUrl?: string;
  cancel_url?: string;
};

const parseCsvList = (value: string | undefined): string[] =>
  (value ?? "")
    .split(",")
    .map((entry) => entry.trim())
    .filter((entry) => entry.length > 0);

const getInfinitePriceAllowlist = (): Set<string> => {
  const configuredAllowlist = parseCsvList(process.env.STRIPE_INFINITE_ALLOWED_EMAILS).map(
    (email) => email.toLowerCase()
  );

  return configuredAllowlist.length > 0
    ? new Set(configuredAllowlist)
    : DEFAULT_INFINITE_SUBSCRIPTION_ALLOWLIST;
};

const getInfinitePriceIds = (): Set<string> => {
  const configuredIds = [
    ...parseCsvList(process.env.STRIPE_INFINITE_PRICE_IDS),
    ...(process.env.STRIPE_INFINITE_PRICE_ID ? [process.env.STRIPE_INFINITE_PRICE_ID] : []),
  ].map((id) => id.trim());

  return new Set(configuredIds.filter((id) => id.length > 0));
};

const isInfinitePrice = (price: StripePriceRow): boolean => {
  const configuredPriceIds = getInfinitePriceIds();
  if (configuredPriceIds.size > 0) {
    return configuredPriceIds.has(price.id);
  }

  const lookupKey = price.lookup_key?.toLowerCase() ?? "";
  const metadataPlan = price.metadata?.plan?.toLowerCase() ?? "";
  const metadataTier = price.metadata?.tier?.toLowerCase() ?? "";

  // Backward-compatible fallback for existing infinite setup.
  return (
    lookupKey === "infinite" ||
    lookupKey === "infinite_subscription" ||
    metadataPlan === "infinite" ||
    metadataTier === "infinite" ||
    price.unit_amount === 0
  );
};

const getPlanPriceId = (config: AppConfig, plan: CheckoutPlan): string | null => {
  if (plan === "solo") {
    return config.STRIPE_SOLO_PRICE_ID ?? null;
  }

  if (plan === "team") {
    return config.STRIPE_TEAM_MONTHLY_PRICE_ID ?? null;
  }

  return config.STRIPE_FOUNDER_PRICE_ID ?? null;
};

const findStripeCustomerByUserId = async (
  supabase: AuthEnv["Variables"]["supabase"],
  userId: string
) => {
  const { data: customers, error } = await supabase
    .schema("stripe")
    .from("customers")
    .select("id, metadata")
    .limit(1000);

  if (error) {
    throw new Error(error.message);
  }

  return (
    (
      customers as Array<{ id: string; metadata: Record<string, string | null> | null }> | null
    )?.find((customer) => customer.metadata?.user_id === userId) ?? null
  );
};

const resolveBillingOwnerContext = async (
  supabase: AuthEnv["Variables"]["supabase"],
  userId: string
) => {
  const { data: profile, error: profileError } = await supabase
    .from("profiles")
    .select("organization_id")
    .eq("id", userId)
    .single();

  if (profileError || !profile?.organization_id) {
    return { error: "Failed to resolve your organization", data: null };
  }

  const { data: billingState, error: billingError } = await getOrganizationBillingState(
    supabase,
    profile.organization_id
  );

  if (billingError || !billingState) {
    return { error: "Failed to resolve organization billing state", data: null };
  }

  return {
    error: null,
    data: {
      organizationId: profile.organization_id,
      billingState,
      isBillingOwner: billingState.owner_user_id === userId,
    },
  };
};

const getCheckoutPrice = async (
  supabase: AuthEnv["Variables"]["supabase"],
  priceId: string
): Promise<StripePriceRow | null> => {
  const { data: price } = await supabase
    .schema("stripe")
    .from("prices")
    .select("id, unit_amount, lookup_key, metadata")
    .eq("id", priceId)
    .maybeSingle();

  return (price as StripePriceRow | null) ?? null;
};

const webhookFactory = createFactory<BaseEnv>();

/**
 * Stripe webhook handler using @supabase/stripe-sync-engine
 * This automatically syncs all Stripe events to Supabase tables
 * Repository: https://github.com/supabase/stripe-sync-engine
 */
const handleWebhook = webhookFactory.createHandlers(async (c) => {
  const stripeSync = c.get("stripeSync");

  try {
    const signature = c.req.header("stripe-signature");

    if (!signature) {
      return c.json({ error: "No signature provided" }, 400);
    }

    // Get raw body for signature verification
    const rawBody = await c.req.text();

    // Process webhook using Stripe Sync Engine
    // This handles signature verification and syncing automatically
    await stripeSync.processWebhook(rawBody, signature);

    return c.json({ received: true });
  } catch (error) {
    console.error("Webhook error:", error);
    return c.json(
      { error: error instanceof Error ? error.message : "Webhook processing failed" },
      400
    );
  }
});

export const getStripeWebhookRouter = () => {
  const stripeWebhookRouter = new Hono();
  stripeWebhookRouter.post("/", ...handleWebhook);

  return stripeWebhookRouter;
};

const stripeFactory = createFactory<AuthEnv>();

/**
 * Create a Stripe Checkout Session
 * POST /api/v1/stripe/create-checkout-session
 */
const createCheckoutSession = (
  config: AppConfig,
  middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>
) =>
  stripeFactory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const stripe = c.get("stripe");

    const body = (await c.req.json()) as CreateCheckoutSessionBody;
    const plan = body.plan;
    const legacyPriceId = body.priceId ?? body.price_id;
    const successUrl = body.successUrl ?? body.success_url;
    const cancelUrl = body.cancelUrl ?? body.cancel_url;

    if (!plan && !legacyPriceId) {
      return c.json({ error: "plan or priceId is required" }, 400);
    }

    if (plan && !["solo", "team", "founder"].includes(plan)) {
      return c.json({ error: "Invalid plan" }, 400);
    }

    const { data: ownerContext, error: ownerError } = await resolveBillingOwnerContext(
      supabase,
      user.id
    );

    if (ownerError || !ownerContext) {
      return c.json({ error: ownerError ?? "Failed to resolve billing owner" }, 500);
    }

    if (!ownerContext.isBillingOwner) {
      return c.json({ error: BILLING_OWNER_ONLY_ERROR }, 403);
    }

    let priceId = legacyPriceId ?? null;
    let quantity = 1;

    if (plan) {
      priceId = getPlanPriceId(config, plan);
      if (!priceId) {
        return c.json({ error: `Missing Stripe price configuration for plan: ${plan}` }, 500);
      }

      if (plan === "team") {
        quantity = Math.max(1, ownerContext.billingState.member_count);
      }
    }
    
    if (!priceId) {
      return c.json({ error: "priceId is required" }, 400);
    }

    const price = await getCheckoutPrice(supabase, priceId);

    if (!price) {
      return c.json({ error: "Price not found" }, 404);
    }

    const normalizedEmail = user.email?.toLowerCase() ?? "";
    const isRestrictedInfinitePrice = isInfinitePrice(price);
    const infinitePriceAllowlist = getInfinitePriceAllowlist();
    if (isRestrictedInfinitePrice && !infinitePriceAllowlist.has(normalizedEmail)) {
      return c.json({ error: "This price is not available" }, 403);
    }

    try {
      const ownerUserId = ownerContext.billingState.owner_user_id;
      const ownerCustomer = await findStripeCustomerByUserId(supabase, ownerUserId);

      let customerId = ownerCustomer?.id ?? "";
      if (!customerId) {
        if (!user.email) {
          return c.json({ error: "Billing owner email is required" }, 400);
        }

        // Create new Stripe customer with user_id in metadata.
        // stripe-sync-engine syncs this customer to Supabase via webhook.
        const customer = await stripe.customers.create({
          email: user.email,
          metadata: {
            user_id: ownerUserId,
          },
        });

        customerId = customer.id;
      }

      const session = await stripe.checkout.sessions.create({
        customer: customerId,
        line_items: [
          {
            price: priceId,
            quantity,
          },
        ],
        mode: "subscription",
        success_url: successUrl || `${config.XTABLO_URL}/settings?success=true`,
        cancel_url: cancelUrl || `${config.XTABLO_URL}/settings?canceled=true`,
        metadata: {
          user_id: ownerUserId,
          organization_id: String(ownerContext.organizationId),
          checkout_plan: plan ?? "legacy_price",
        },
        subscription_data: {
          metadata: {
            user_id: ownerUserId,
            organization_id: String(ownerContext.organizationId),
            checkout_plan: plan ?? "legacy_price",
          },
        },
      });

      return c.json({ sessionId: session.id, url: session.url });
    } catch (error) {
      console.error("Error creating checkout session:", error);
      return c.json(
        { error: error instanceof Error ? error.message : "Failed to create checkout session" },
        500
      );
    }
  });

/**
 * Create a Stripe Customer Portal Session
 * POST /api/v1/stripe/create-portal-session
 */
const createPortalSession = (
  config: AppConfig,
  middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>
) =>
  stripeFactory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const stripe = c.get("stripe");

    const body = await c.req.json();
    const returnUrl = body.returnUrl ?? body.return_url;

    const { data: ownerContext, error: ownerError } = await resolveBillingOwnerContext(
      supabase,
      user.id
    );

    if (ownerError || !ownerContext) {
      return c.json({ error: ownerError ?? "Failed to resolve billing owner" }, 500);
    }

    if (!ownerContext.isBillingOwner) {
      return c.json({ error: BILLING_OWNER_ONLY_ERROR }, 403);
    }

    try {
      const customer = await findStripeCustomerByUserId(
        supabase,
        ownerContext.billingState.owner_user_id
      );

      if (!customer) {
        return c.json({ error: "No Stripe customer found" }, 404);
      }

      // Create portal session
      const session = await stripe.billingPortal.sessions.create({
        customer: customer.id,
        return_url: returnUrl || `${config.XTABLO_URL}/settings`,
      });

      return c.json({ url: session.url });
    } catch (error) {
      console.error("Error creating portal session:", error);
      return c.json(
        { error: error instanceof Error ? error.message : "Failed to create portal session" },
        500
      );
    }
  });

// Note: Subscription status queries are handled directly from the frontend
// using Supabase client with RLS policies. No API endpoints needed for reads.

/**
 * Cancel subscription at period end
 * POST /api/v1/stripe/cancel-subscription
 */
const cancelSubscription = (middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>) =>
  stripeFactory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const stripe = c.get("stripe");

    const { data: ownerContext, error: ownerError } = await resolveBillingOwnerContext(
      supabase,
      user.id
    );

    if (ownerError || !ownerContext) {
      return c.json({ error: ownerError ?? "Failed to resolve billing owner" }, 500);
    }

    if (!ownerContext.isBillingOwner) {
      return c.json({ error: BILLING_OWNER_ONLY_ERROR }, 403);
    }

    try {
      const customer = await findStripeCustomerByUserId(
        supabase,
        ownerContext.billingState.owner_user_id
      );

      if (!customer) {
        return c.json({ error: "Customer not found" }, 404);
      }

      // Get user's active subscription for this customer
      const { data: subscription } = await supabase
        .schema("stripe")
        .from("subscriptions")
        .select("id, status")
        .eq("customer", customer.id)
        .in("status", ["active", "trialing"])
        .maybeSingle();

      if (!subscription) {
        return c.json({ error: "No active subscription found" }, 404);
      }

      // Cancel subscription at period end in Stripe
      // The webhook will automatically sync the change to our database
      await stripe.subscriptions.cancel(subscription.id);

      return c.json({
        success: true,
        message: "Subscription will cancel at period end",
      });
    } catch (error) {
      console.error("Error canceling subscription:", error);
      return c.json(
        { error: error instanceof Error ? error.message : "Failed to cancel subscription" },
        500
      );
    }
  });

/**
 * Reactivate a canceled subscription
 * POST /api/v1/stripe/reactivate-subscription
 */
const reactivateSubscription = (
  middlewareManager: ReturnType<typeof MiddlewareManager.getInstance>
) =>
  stripeFactory.createHandlers(middlewareManager.regularUserCheck, async (c) => {
    const user = c.get("user");
    const supabase = c.get("supabase");
    const stripe = c.get("stripe");

    const { data: ownerContext, error: ownerError } = await resolveBillingOwnerContext(
      supabase,
      user.id
    );

    if (ownerError || !ownerContext) {
      return c.json({ error: ownerError ?? "Failed to resolve billing owner" }, 500);
    }

    if (!ownerContext.isBillingOwner) {
      return c.json({ error: BILLING_OWNER_ONLY_ERROR }, 403);
    }

    try {
      const customer = await findStripeCustomerByUserId(
        supabase,
        ownerContext.billingState.owner_user_id
      );

      if (!customer) {
        return c.json({ error: "No subscription found to reactivate" }, 404);
      }

      // Get user's subscription that's set to cancel
      const { data: subscription } = await supabase
        .schema("stripe")
        .from("subscriptions")
        .select("id, cancel_at_period_end")
        .eq("customer", customer.id)
        .eq("cancel_at_period_end", true)
        .maybeSingle();

      if (!subscription) {
        return c.json({ error: "No subscription found to reactivate" }, 404);
      }

      // Reactivate subscription in Stripe
      // The webhook will automatically sync the change to our database
      await stripe.subscriptions.update(subscription.id, {
        cancel_at_period_end: false,
      });

      return c.json({ success: true, message: "Subscription reactivated" });
    } catch (error) {
      console.error("Error reactivating subscription:", error);
      return c.json(
        { error: error instanceof Error ? error.message : "Failed to reactivate subscription" },
        500
      );
    }
  });

export const getStripeRouter = (config: AppConfig) => {
  const stripeRouter = new Hono();
  const middlewareManager = MiddlewareManager.getInstance();

  stripeRouter.post(
    "/create-checkout-session",
    ...createCheckoutSession(config, middlewareManager)
  );
  stripeRouter.post("/create-portal-session", ...createPortalSession(config, middlewareManager));
  stripeRouter.post("/cancel-subscription", ...cancelSubscription(middlewareManager));
  stripeRouter.post("/reactivate-subscription", ...reactivateSubscription(middlewareManager));

  return stripeRouter;
};
