import { Hono } from 'hono';
import type { Env } from '../types.js';
import type { AuthContext } from '../middleware/auth.js';
import { requireApiKeyScope, requireAuth } from '../middleware/auth.js';
import { createAdminClient } from '../db/client.js';
import { logger } from '../lib/logger.js';
import {
  getStripe,
  getStripeWebhookSecret,
  STRIPE_PRO_PRICE_ID,
  STRIPE_TEAM_PRICE_ID,
} from '../stripe/index.js';
import { handleStripeWebhook } from '../stripe/webhooks.js';
import {
  getCommercialEntitlements,
  getCommercialLimits,
  type OrganizationEntitlementSummary,
} from '../lib/entitlements.js';
import { recordAuditEvent } from '../lib/audit-log.js';
import { emitApiTelemetry } from '../lib/telemetry.js';

export const billingRoutes = new Hono<Env>();

function isBillingLaunchEnabled(): boolean {
  return process.env.REGISTRY_BILLING_ENABLED === 'true';
}

function billingComingSoonResponse(c: any) {
  return c.json({
    error: 'Paid plan checkout is coming soon. Billing is not active yet.',
    code: 'billing_coming_soon',
  }, 403);
}

type CheckoutPlan = 'pro' | 'team';

function normalizeCheckoutPlan(value: unknown): CheckoutPlan | undefined {
  return value === 'pro' || value === 'team' ? value : undefined;
}

function getAllowedBillingOrigins(): Set<string> {
  const configured = process.env.DECANTR_BILLING_ALLOWED_ORIGINS || process.env.ALLOWED_ORIGINS || '';
  return new Set(
    configured
      .split(',')
      .map((origin) => origin.trim())
      .filter(Boolean)
      .map((origin) => {
        try {
          return new URL(origin).origin;
        } catch {
          return '';
        }
      })
      .filter(Boolean),
  );
}

function validateBillingCallbackUrl(value: string, label: string): string | null {
  let url: URL;
  try {
    url = new URL(value);
  } catch {
    return `${label} must be an absolute URL.`;
  }

  if (!['https:', 'http:'].includes(url.protocol)) {
    return `${label} must use http or https.`;
  }

  const allowedOrigins = getAllowedBillingOrigins();
  if (allowedOrigins.size === 0) {
    return 'Billing callback origins are not configured.';
  }

  if (!allowedOrigins.has(url.origin)) {
    return `${label} origin is not allowed.`;
  }

  return null;
}

// ---------------------------------------------------------------------------
// POST /billing/checkout -- Create a Stripe Checkout session for Pro or Team
// ---------------------------------------------------------------------------
billingRoutes.post('/billing/checkout', requireAuth(), requireApiKeyScope('billing:manage'), async (c) => {
  const auth = c.get('auth') as AuthContext;
  const user = auth.user!;

  const body = await c.req.json<{
    plan: 'pro' | 'team';
    quantity?: number;
    success_url: string;
    cancel_url: string;
  }>().catch(() => ({} as {
    plan?: unknown;
    quantity?: number;
    success_url?: string;
    cancel_url?: string;
  }));

  if (!isBillingLaunchEnabled()) {
    emitApiTelemetry(c, {
      name: 'billing.checkout_blocked',
      properties: {
        billingEnabled: false,
        orgScoped: false,
        plan: normalizeCheckoutPlan(body.plan),
        reason: 'billing_coming_soon',
        surface: 'api_billing_checkout',
      },
    });
    return billingComingSoonResponse(c);
  }

  const plan = normalizeCheckoutPlan(body.plan);
  if (!plan) {
    return c.json({ error: 'Invalid plan. Must be "pro" or "team".' }, 400);
  }

  if (!body.success_url || !body.cancel_url) {
    return c.json({ error: 'success_url and cancel_url are required.' }, 400);
  }
  const successUrlError = validateBillingCallbackUrl(body.success_url, 'success_url');
  if (successUrlError) {
    return c.json({ error: successUrlError }, 400);
  }
  const cancelUrlError = validateBillingCallbackUrl(body.cancel_url, 'cancel_url');
  if (cancelUrlError) {
    return c.json({ error: cancelUrlError }, 400);
  }

  if (plan === 'team' && user.tier === 'team') {
    return c.json({ error: 'Already on Team plan. Use billing portal to manage seats.' }, 400);
  }

  if (plan === 'pro' && (user.tier === 'pro' || user.tier === 'team' || user.tier === 'enterprise')) {
    return c.json({ error: 'Already on a paid plan equal to or above Pro.' }, 400);
  }

  const stripe = getStripe();
  const adminClient = createAdminClient();

  // Get or create Stripe customer
  let stripeCustomerId: string;

  const { data: userRow } = await adminClient
    .from('users')
    .select('stripe_customer_id')
    .eq('id', user.id)
    .single();

  if (userRow?.stripe_customer_id) {
    stripeCustomerId = userRow.stripe_customer_id;
  } else {
    const customer = await stripe.customers.create({
      email: user.email,
      metadata: { supabase_user_id: user.id },
    });
    stripeCustomerId = customer.id;

    await adminClient
      .from('users')
      .update({ stripe_customer_id: stripeCustomerId, updated_at: new Date().toISOString() })
      .eq('id', user.id);
  }

  const priceId = plan === 'pro' ? STRIPE_PRO_PRICE_ID : STRIPE_TEAM_PRICE_ID;
  const quantity = plan === 'team' ? (body.quantity ?? 1) : 1;

  const session = await stripe.checkout.sessions.create({
    customer: stripeCustomerId,
    mode: 'subscription',
    line_items: [
      {
        price: priceId,
        quantity: quantity,
      },
    ],
    metadata: {
      supabase_user_id: user.id,
      plan,
      quantity: String(quantity),
    },
    success_url: body.success_url,
    cancel_url: body.cancel_url,
  });

  await recordAuditEvent({
    actor_user_id: user.id,
    scope: 'billing',
    action: 'checkout.started',
    target_type: 'checkout_session',
    target_id: session.id,
    details: {
      plan,
      quantity,
    },
  });

  return c.json({ checkout_url: session.url, session_id: session.id });
});

// ---------------------------------------------------------------------------
// POST /billing/portal -- Create a Stripe Billing Portal session
// ---------------------------------------------------------------------------
billingRoutes.post('/billing/portal', requireAuth(), requireApiKeyScope('billing:manage'), async (c) => {
  if (!isBillingLaunchEnabled()) {
    return billingComingSoonResponse(c);
  }

  const auth = c.get('auth') as AuthContext;
  const user = auth.user!;

  const body = await c.req.json<{ return_url: string }>();

  if (!body.return_url) {
    return c.json({ error: 'return_url is required.' }, 400);
  }
  const returnUrlError = validateBillingCallbackUrl(body.return_url, 'return_url');
  if (returnUrlError) {
    return c.json({ error: returnUrlError }, 400);
  }

  const stripe = getStripe();
  const adminClient = createAdminClient();
  const { data: userRow } = await adminClient
    .from('users')
    .select('stripe_customer_id')
    .eq('id', user.id)
    .single();

  if (!userRow?.stripe_customer_id) {
    return c.json({ error: 'No billing account found. Subscribe to a plan first.' }, 400);
  }

  const session = await stripe.billingPortal.sessions.create({
    customer: userRow.stripe_customer_id,
    return_url: body.return_url,
  });

  await recordAuditEvent({
    actor_user_id: user.id,
    scope: 'billing',
    action: 'portal.started',
    target_type: 'billing_portal',
    target_id: userRow.stripe_customer_id,
    details: {
      return_url: body.return_url,
    },
  });

  return c.json({ portal_url: session.url });
});

// ---------------------------------------------------------------------------
// GET /billing/status -- Get current subscription status
// ---------------------------------------------------------------------------
billingRoutes.get('/billing/status', requireAuth(), requireApiKeyScope('read', 'billing:manage'), async (c) => {
  const auth = c.get('auth') as AuthContext;
  const user = auth.user!;

  const adminClient = createAdminClient();
  const { data: userRow } = await adminClient
    .from('users')
    .select('id, username, display_name, tier, stripe_customer_id')
    .eq('id', user.id)
    .single();

  if (!userRow) {
    return c.json({ error: 'User not found.' }, 404);
  }

  const { data: memberships } = await adminClient
    .from('org_members')
    .select('org_id, role, organizations(id, name, slug, tier, stripe_subscription_id, seat_limit)')
    .eq('user_id', user.id)
    .order('created_at', { ascending: true });

  const organizationLists = await Promise.all(
    (memberships ?? []).map(async (membership: any) => {
      const org = membership.organizations;
      if (!org?.id || !org?.slug || !org?.name || !org?.tier) return [];

      const { count } = await adminClient
        .from('org_members')
        .select('*', { count: 'exact', head: true })
        .eq('org_id', org.id);

      return [{
        id: org.id,
        slug: org.slug,
        name: org.name,
        tier: org.tier,
        role: membership.role,
        seat_limit: org.seat_limit ?? 1,
        member_count: count ?? 0,
        stripe_subscription_id: org.stripe_subscription_id ?? null,
      }];
    }),
  );
  const organizations: OrganizationEntitlementSummary[] = organizationLists.flat();

  const activeOrg = organizations[0] ?? null;
  const entitlements = getCommercialEntitlements(userRow.tier);
  const limits = getCommercialLimits(userRow.tier, activeOrg);

  const { count: personalContentItems } = await adminClient
    .from('content')
    .select('*', { count: 'exact', head: true })
    .eq('owner_id', user.id)
    .is('org_id', null);

  const { count: personalPrivatePackages } = await adminClient
    .from('content')
    .select('*', { count: 'exact', head: true })
    .eq('owner_id', user.id)
    .is('org_id', null)
    .eq('visibility', 'private');

  const { count: orgContentItems } = activeOrg
    ? await adminClient
        .from('content')
        .select('*', { count: 'exact', head: true })
        .eq('org_id', activeOrg.id)
    : { count: 0 };
  const thirtyDaysAgo = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)).toISOString();
  const { data: usageRows } = await adminClient
    .from('usage_events')
    .select('metric, quantity')
    .eq('user_id', user.id)
    .gte('created_at', thirtyDaysAgo);
  const usageTotals = (usageRows ?? []).reduce((totals: Record<string, number>, row: any) => {
    const metric = row.metric ?? 'unknown';
    totals[metric] = (totals[metric] ?? 0) + (row.quantity ?? 0);
    return totals;
  }, {});
  const apiRequests30d = usageTotals.api_request ?? 0;
  const personalPublishes30d = usageTotals.content_publish ?? 0;
  const privatePublishes30d = usageTotals.private_package_publish ?? 0;
  const orgPublishes30d = usageTotals.org_package_publish ?? 0;
  const approvalActions30d = usageTotals.approval_action ?? 0;

  // Base response for free users, users without Stripe, or pre-launch billing.
  if (!userRow.stripe_customer_id || !isBillingLaunchEnabled()) {
    return c.json({
      tier: userRow.tier,
      entitlements,
      limits,
      usage: {
        api_requests_30d: apiRequests30d,
        personal_publishes_30d: personalPublishes30d,
        private_package_publishes_30d: privatePublishes30d,
        org_package_publishes_30d: orgPublishes30d,
        approval_actions_30d: approvalActions30d,
        personal_content_items: personalContentItems ?? 0,
        personal_private_packages: personalPrivatePackages ?? 0,
        org_content_items: orgContentItems ?? 0,
        seats_used: activeOrg?.member_count ?? 0,
        seats_limit: activeOrg?.seat_limit ?? 0,
      },
      organizations,
      subscription: null,
    });
  }

  // Fetch active subscriptions from Stripe
  const stripe = getStripe();
  const subscriptions = await stripe.subscriptions.list({
    customer: userRow.stripe_customer_id,
    status: 'active',
    limit: 1,
  });

  const subscription = subscriptions.data[0] ?? null;

  if (!subscription) {
    return c.json({
      tier: userRow.tier,
      entitlements,
      limits,
      usage: {
        api_requests_30d: apiRequests30d,
        personal_publishes_30d: personalPublishes30d,
        private_package_publishes_30d: privatePublishes30d,
        org_package_publishes_30d: orgPublishes30d,
        approval_actions_30d: approvalActions30d,
        personal_content_items: personalContentItems ?? 0,
        personal_private_packages: personalPrivatePackages ?? 0,
        org_content_items: orgContentItems ?? 0,
        seats_used: activeOrg?.member_count ?? 0,
        seats_limit: activeOrg?.seat_limit ?? 0,
      },
      organizations,
      subscription: null,
    });
  }

  const item = subscription.items.data[0];

  return c.json({
    tier: userRow.tier,
    entitlements,
    limits,
    usage: {
      api_requests_30d: apiRequests30d,
      personal_publishes_30d: personalPublishes30d,
      private_package_publishes_30d: privatePublishes30d,
      org_package_publishes_30d: orgPublishes30d,
      approval_actions_30d: approvalActions30d,
      personal_content_items: personalContentItems ?? 0,
      personal_private_packages: personalPrivatePackages ?? 0,
      org_content_items: orgContentItems ?? 0,
      seats_used: activeOrg?.member_count ?? 0,
      seats_limit: activeOrg?.seat_limit ?? 0,
    },
    organizations,
    subscription: {
      id: subscription.id,
      status: subscription.status,
      price_id: item?.price.id ?? null,
      quantity: item?.quantity ?? null,
      current_period_start: item?.current_period_start ?? null,
      current_period_end: item?.current_period_end ?? null,
      cancel_at_period_end: subscription.cancel_at_period_end,
    },
  });
});

// ---------------------------------------------------------------------------
// POST /billing/webhooks -- Stripe webhook handler (raw body required)
// ---------------------------------------------------------------------------
billingRoutes.post('/billing/webhooks', async (c) => {
  const signature = c.req.header('stripe-signature');

  if (!signature) {
    return c.json({ error: 'Missing stripe-signature header.' }, 400);
  }

  let rawBody: string;
  try {
    rawBody = await c.req.text();
  } catch {
    return c.json({ error: 'Failed to read request body.' }, 400);
  }

  const stripe = getStripe();
  let event;
  try {
    event = stripe.webhooks.constructEvent(rawBody, signature, getStripeWebhookSecret());
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    logger.error({ error: message }, 'Webhook signature verification failed');
    return c.json({ error: `Webhook signature verification failed: ${message}` }, 400);
  }

  // Idempotency: skip if we've already processed this event
  const adminClient = createAdminClient();
  const { error: insertError } = await adminClient
    .from('stripe_events')
    .insert({ event_id: event.id, event_type: event.type });

  if (insertError) {
    // Unique constraint violation = already processed
    if (insertError.code === '23505') {
      logger.info({ eventId: event.id }, 'Webhook event already processed, skipping');
      return c.json({ received: true, deduplicated: true });
    }
    // Other DB error — log but still try to process (fail open)
    logger.error({ eventId: event.id, error: insertError.message }, 'Failed to record webhook event');
  }

  try {
    await handleStripeWebhook(event);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    logger.error({ eventType: event.type, error: message }, 'Webhook handler error');
    return c.json({ error: 'Webhook handler failed.' }, 500);
  }

  return c.json({ received: true });
});
