/**
 * Billing Service
 *
 * Centralized business logic for billing operations.
 * This service handles:
 * - Fetching billing history from local database and Stripe
 * - Invoice retrieval and formatting
 * - Data transformations between Stripe and internal formats
 *
 * Usage:
 * ```typescript
 * import { BillingService } from '@/lib/billing/billing.service';
 *
 * const history = await BillingService.getBillingHistory(userEmail, { limit: 20 });
 * const invoice = await BillingService.getInvoiceDetails(invoiceId, user);
 * ```
 */

import { prisma } from "@/lib/db";
import { stripe } from "@/lib/stripe";
import { logger } from "@/lib/logger";
import type Stripe from "stripe";
import {
  type BillingEvent,
  type BillingDataSource,
  type BillingHistoryResponse,
  type BillingHistoryQueryParams,
  type UserBillingContext,
  type CustomerIdSet,
  type StripeInvoiceDetails,
  type InvoiceDetails,
  type InvoiceLineItem,
  BILLING_HISTORY_LIMITS,
  ACTIVE_SUBSCRIPTION_STATUSES,
} from "./types";

// ============================================================================
// USER DATA RETRIEVAL
// ============================================================================

/**
 * Fetches user billing context from the database.
 *
 * @param email - User's email address
 * @param limit - Maximum number of billing history records to fetch
 * @returns User billing context or null if user not found
 */
export async function getUserBillingContext(
  email: string,
  limit: number = BILLING_HISTORY_LIMITS.DEFAULT,
): Promise<UserBillingContext | null> {
  return prisma.users.findUnique({
    where: { email },
    select: {
      id: true,
      stripeCustomerId: true,
      subscriptions: {
        where: {
          status: { in: [...ACTIVE_SUBSCRIPTION_STATUSES] },
        },
        select: {
          id: true,
          stripeCustomerId: true,
          stripeSubscriptionId: true,
          billing_history: {
            orderBy: { createdAt: "desc" },
            take: limit,
          },
        },
        orderBy: { createdAt: "desc" },
        take: 1,
      },
    },
  });
}

/**
 * Fetches user with their Stripe customer IDs for authorization.
 *
 * @param email - User's email address
 * @returns User with customer IDs or null if not found
 */
export async function getUserForInvoiceAccess(email: string) {
  return prisma.users.findUnique({
    where: { email },
    select: {
      id: true,
      stripeCustomerId: true,
      subscriptions: {
        select: {
          stripeCustomerId: true,
        },
      },
    },
  });
}

/**
 * Extracts all Stripe customer IDs associated with a user.
 *
 * @param user - User object with subscriptions
 * @returns Set of customer IDs
 */
export function extractCustomerIds(user: {
  stripeCustomerId: string | null;
  subscriptions: Array<{ stripeCustomerId: string | null }>;
}): CustomerIdSet {
  const customerIds = new Set<string>();

  if (user.stripeCustomerId) {
    customerIds.add(user.stripeCustomerId);
  }

  user.subscriptions.forEach((sub) => {
    if (sub.stripeCustomerId) {
      customerIds.add(sub.stripeCustomerId);
    }
  });

  return customerIds;
}

// ============================================================================
// LOCAL BILLING HISTORY
// ============================================================================

/**
 * Transforms local database billing records to API format.
 *
 * @param records - Raw billing history records from database
 * @returns Formatted billing events
 */
export function transformLocalBillingHistory(
  records: Array<{
    id: string;
    event: string;
    amount: number;
    currency: string;
    createdAt: Date;
    metadata: unknown;
    invoiceId: string | null;
    paymentIntentId: string | null;
  }>,
): BillingEvent[] {
  return records.map((item) => ({
    id: item.id,
    event: item.event,
    amount: item.amount,
    currency: item.currency,
    createdAt: item.createdAt.toISOString(),
    metadata: (item.metadata as Record<string, unknown>) || {},
    invoiceId: item.invoiceId,
    paymentIntentId: item.paymentIntentId,
  }));
}

// ============================================================================
// STRIPE INVOICE OPERATIONS
// ============================================================================

/**
 * Fetches paid invoices from Stripe for a customer.
 *
 * @param customerId - Stripe customer ID
 * @param limit - Maximum number of invoices to fetch
 * @returns Array of billing events from Stripe
 */
export async function fetchStripeInvoices(
  customerId: string,
  limit: number,
  statusFilter: "paid" | "all" = "paid",
): Promise<BillingEvent[]> {
  try {
    const params: Stripe.InvoiceListParams = {
      customer: customerId,
      limit,
      expand: ["data.subscription"],
    };

    if (statusFilter === "paid") {
      params.status = "paid";
    }

    const invoices = await stripe.invoices.list(params);
    return invoices.data.map((invoice) => transformStripeInvoice(invoice));
  } catch (error) {
    logger.error("Failed to fetch Stripe invoices", {
      customerId,
      error: error instanceof Error ? error.message : String(error),
    });
    return [];
  }
}

/**
 * Transforms a Stripe invoice to internal billing event format.
 *
 * @param invoice - Stripe invoice object
 * @returns Formatted billing event
 */
export function transformStripeInvoice(invoice: Stripe.Invoice): BillingEvent {
  const isPaid = invoice.status === "paid";

  return {
    id: invoice.id,
    event: isPaid ? "invoice_paid" : `invoice_${invoice.status}`,
    amount: isPaid ? invoice.amount_paid : invoice.amount_due,
    currency: invoice.currency,
    createdAt: timestampToISO(invoice.created),
    metadata: {
      billing_reason: invoice.billing_reason,
      invoice_number: invoice.number,
      period_start: invoice.period_start
        ? timestampToISO(invoice.period_start)
        : null,
      period_end: invoice.period_end
        ? timestampToISO(invoice.period_end)
        : null,
    },
    invoiceId: invoice.id,
    invoiceUrl: invoice.hosted_invoice_url,
    invoicePdf: invoice.invoice_pdf,
    invoiceStatus: invoice.status,
    invoiceNumber: invoice.number,
  };
}

/**
 * Fetches invoice details from Stripe for enrichment.
 *
 * @param invoiceIds - Array of invoice IDs to fetch
 * @returns Map of invoice ID to details
 */
export async function fetchInvoiceDetails(
  invoiceIds: string[],
): Promise<Map<string, StripeInvoiceDetails>> {
  const invoiceMap = new Map<string, StripeInvoiceDetails>();

  // Limit to prevent too many API calls
  const idsToFetch = invoiceIds.slice(
    0,
    BILLING_HISTORY_LIMITS.INVOICE_ENRICHMENT_MAX,
  );

  try {
    const invoicePromises = idsToFetch.map((id) =>
      stripe.invoices.retrieve(id).catch(() => null),
    );

    const results = await Promise.all(invoicePromises);

    results.forEach((invoice) => {
      if (invoice) {
        invoiceMap.set(invoice.id, {
          url: invoice.hosted_invoice_url,
          pdf: invoice.invoice_pdf,
          status: invoice.status,
          number: invoice.number,
        });
      }
    });
  } catch (error) {
    logger.error("Failed to fetch invoice details for enrichment", {
      error: error instanceof Error ? error.message : String(error),
    });
  }

  return invoiceMap;
}

/**
 * Enriches billing events with Stripe invoice details.
 *
 * @param events - Billing events to enrich
 * @param invoiceMap - Map of invoice details
 * @returns Enriched billing events
 */
export function enrichBillingEventsWithInvoices(
  events: BillingEvent[],
  invoiceMap: Map<string, StripeInvoiceDetails>,
): BillingEvent[] {
  return events.map((event) => {
    if (event.invoiceId && invoiceMap.has(event.invoiceId)) {
      const details = invoiceMap.get(event.invoiceId)!;
      return {
        ...event,
        invoiceUrl: details.url,
        invoicePdf: details.pdf,
        invoiceStatus: details.status,
        invoiceNumber: details.number,
      };
    }
    return event;
  });
}

// ============================================================================
// INVOICE DETAILS
// ============================================================================

/**
 * Retrieves a single invoice from Stripe.
 *
 * @param invoiceId - Stripe invoice ID
 * @returns Stripe invoice or null if not found
 */
export async function retrieveStripeInvoice(
  invoiceId: string,
): Promise<Stripe.Invoice | null> {
  try {
    return await stripe.invoices.retrieve(invoiceId, {
      expand: ["lines.data.price.product"],
    });
  } catch (error: any) {
    if (error.code === "resource_missing") {
      return null;
    }
    throw error;
  }
}

/**
 * Transforms a Stripe invoice to the invoice details format.
 *
 * @param invoice - Stripe invoice object
 * @returns Formatted invoice details
 */
export function transformInvoiceToDetails(
  invoice: Stripe.Invoice,
): InvoiceDetails {
  const lineItems: InvoiceLineItem[] = (invoice.lines?.data || []).map(
    (line) => {
      const product = (line.price as any)?.product;
      return {
        description: line.description || product?.name || "Subscription",
        amount: line.amount,
        quantity: line.quantity || 1,
        period: line.period
          ? {
              start: timestampToISO(line.period.start),
              end: timestampToISO(line.period.end),
            }
          : null,
      };
    },
  );

  return {
    id: invoice.id,
    number: invoice.number,
    status: invoice.status,
    amountPaid: invoice.amount_paid,
    amountDue: invoice.amount_due,
    total: invoice.total,
    currency: invoice.currency,
    hostedUrl: invoice.hosted_invoice_url,
    pdfUrl: invoice.invoice_pdf,
    created: timestampToISO(invoice.created),
    periodStart: invoice.period_start
      ? timestampToISO(invoice.period_start)
      : null,
    periodEnd: invoice.period_end ? timestampToISO(invoice.period_end) : null,
    billingReason: invoice.billing_reason,
    customerEmail: invoice.customer_email,
    lineItems,
  };
}

/**
 * Verifies if a user has access to a specific invoice.
 *
 * @param invoice - Stripe invoice
 * @param customerIds - Set of customer IDs the user owns
 * @returns True if user has access
 */
export function userCanAccessInvoice(
  invoice: Stripe.Invoice,
  customerIds: CustomerIdSet,
): boolean {
  return customerIds.has(invoice.customer as string);
}

// ============================================================================
// BILLING HISTORY AGGREGATION
// ============================================================================

/**
 * Main function to get billing history with automatic fallback to Stripe.
 *
 * Strategy:
 * 1. Try to get local billing history from database
 * 2. If local history exists and includeInvoices is true, enrich with Stripe details
 * 3. If no local history, fall back to Stripe invoices
 *
 * @param userContext - User billing context from database
 * @param params - Query parameters
 * @returns Billing history response
 */
export async function getBillingHistory(
  userContext: UserBillingContext,
  params: BillingHistoryQueryParams,
): Promise<BillingHistoryResponse> {
  const { limit, includeInvoices } = params;
  const subscription = userContext.subscriptions[0];
  const stripeCustomerId =
    subscription?.stripeCustomerId || userContext.stripeCustomerId;

  let billingHistory: BillingEvent[] = [];
  let source: BillingDataSource = "local";

  // Step 1: Try local billing history
  if (
    subscription?.billing_history &&
    subscription.billing_history.length > 0
  ) {
    billingHistory = transformLocalBillingHistory(subscription.billing_history);
    logger.debug("Retrieved local billing history", {
      count: billingHistory.length,
    });
  }

  // Step 2: If no local history, try Stripe (paid invoices first)
  if (billingHistory.length === 0 && stripeCustomerId) {
    logger.info("No local billing history, fetching from Stripe", {
      customerId: stripeCustomerId,
    });
    source = "stripe";
    billingHistory = await fetchStripeInvoices(stripeCustomerId, limit, "paid");
    logger.info("Fetched billing history from Stripe", {
      count: billingHistory.length,
    });
  }

  // Step 3: Enrich local history with Stripe invoice details
  if (
    billingHistory.length > 0 &&
    includeInvoices &&
    stripeCustomerId &&
    source === "local"
  ) {
    const invoiceIds = billingHistory
      .filter((item) => item.invoiceId)
      .map((item) => item.invoiceId as string);

    if (invoiceIds.length > 0) {
      source = "combined";
      const invoiceMap = await fetchInvoiceDetails(invoiceIds);
      billingHistory = enrichBillingEventsWithInvoices(
        billingHistory,
        invoiceMap,
      );
    }
  }

  // Step 4: Final fallback - try all invoices (not just paid)
  if (billingHistory.length === 0 && stripeCustomerId) {
    logger.info("Attempting final Stripe fallback", {
      customerId: stripeCustomerId,
    });
    source = "stripe";
    billingHistory = await fetchStripeInvoices(stripeCustomerId, limit, "all");
  }

  return {
    billingHistory,
    hasMore: billingHistory.length >= limit,
    source,
  };
}

// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================

/**
 * Converts a Unix timestamp to ISO 8601 string.
 *
 * @param timestamp - Unix timestamp in seconds
 * @returns ISO 8601 formatted date string
 */
export function timestampToISO(timestamp: number): string {
  return new Date(timestamp * 1000).toISOString();
}

/**
 * Parses and validates query parameters for billing history.
 *
 * @param searchParams - URL search parameters
 * @returns Validated query parameters
 */
export function parseBillingHistoryParams(
  searchParams: URLSearchParams,
): BillingHistoryQueryParams {
  const rawLimit = parseInt(
    searchParams.get("limit") || String(BILLING_HISTORY_LIMITS.DEFAULT),
    10,
  );
  const limit = Math.min(Math.max(1, rawLimit), BILLING_HISTORY_LIMITS.MAX);
  const includeInvoices = searchParams.get("include_invoices") !== "false";

  return { limit, includeInvoices };
}

// ============================================================================
// EXPORTED SERVICE OBJECT
// ============================================================================

/**
 * Billing Service - Main export for billing operations.
 *
 * Provides a clean interface for all billing-related operations.
 */
export const BillingService = {
  // User operations
  getUserBillingContext,
  getUserForInvoiceAccess,
  extractCustomerIds,

  // Billing history
  getBillingHistory,
  transformLocalBillingHistory,

  // Stripe operations
  fetchStripeInvoices,
  fetchInvoiceDetails,
  enrichBillingEventsWithInvoices,

  // Invoice operations
  retrieveStripeInvoice,
  transformInvoiceToDetails,
  userCanAccessInvoice,

  // Utilities
  timestampToISO,
  parseBillingHistoryParams,
};

export default BillingService;
