import prisma from "../db.server";

export interface CreditAllocationSummary {
  id: string;
  sourceType: string;
  sourceId: string | null;
  creditsAllocated: number;
  creditsUsed: number;
  creditsRemaining: number;
  expiresAt: Date | null;
  createdAt: Date;
  status: string;
}

export interface CreditBalanceResult {
  availableBalance: number;
  totalAllocated: number;
  totalUsed: number;
  monthlyLimit: number;
  monthlyUsed: number;
  allocations: CreditAllocationSummary[];
}

export interface AllocateCreditsOptions {
  shopId: string;
  sourceType: "MONTHLY_PLAN" | "ADDITIONAL_PURCHASE" | "PROMO" | "ADMIN_ADJUSTMENT";
  amount: number;
  sourceId?: string;
  expiresAt?: Date | null;
  idempotencyKey?: string;
}

export interface ConsumeCreditsOptions {
  shopId: string;
  creditsNeeded?: number;
  eventType?: string;
  provider?: string;
  model?: string;
  requestId?: string;
  metadata?: any;
}

/**
 * Calculates current available credit balance and allocation breakdown across active ledger records.
 */
export async function getAvailableCredits(shopId: string): Promise<CreditBalanceResult> {
  const now = new Date();

  // 1. Expire outdated allocations
  try {
    await prisma.creditAllocation.updateMany({
      where: {
        shopId,
        status: "ACTIVE",
        expiresAt: { lt: now },
      },
      data: { status: "EXPIRED" },
    });

    // Mark exhausted allocations
    await prisma.creditAllocation.updateMany({
      where: {
        shopId,
        status: "ACTIVE",
        creditsRemaining: { lte: 0 },
      },
      data: { status: "EXHAUSTED" },
    });
  } catch (err) {
    console.warn("[Credit Ledger] Expiration check warning:", err);
  }

  // 2. Fetch active allocations
  const activeAllocations = await prisma.creditAllocation.findMany({
    where: { shopId, status: "ACTIVE" },
    orderBy: [
      { expiresAt: "asc" }, // FEFO: Soonest expiring first
      { createdAt: "asc" }, // FIFO: Oldest first
    ],
  });

  const availableBalance = activeAllocations.reduce((sum, a) => sum + Math.max(0, a.creditsRemaining), 0);

  // 3. Fetch total historical allocation & usage
  const allAllocations = await prisma.creditAllocation.findMany({
    where: { shopId },
  });

  const totalAllocated = allAllocations.reduce((sum, a) => sum + a.creditsAllocated, 0);
  const totalUsed = allAllocations.reduce((sum, a) => sum + a.creditsUsed, 0);

  // 4. Fetch subscription details for monthly usage context
  const activeSub = await prisma.subscription.findFirst({
    where: { shopId, status: "ACTIVE" },
    orderBy: { createdAt: "desc" },
  });

  const monthlyLimit = activeSub?.monthlyLimit || 100;
  const monthlyUsed = activeSub?.currentUsage || 0;

  const summaries: CreditAllocationSummary[] = activeAllocations.map((a) => ({
    id: a.id,
    sourceType: a.sourceType,
    sourceId: a.sourceId,
    creditsAllocated: a.creditsAllocated,
    creditsUsed: a.creditsUsed,
    creditsRemaining: a.creditsRemaining,
    expiresAt: a.expiresAt,
    createdAt: a.createdAt,
    status: a.status,
  }));

  return {
    availableBalance,
    totalAllocated,
    totalUsed,
    monthlyLimit,
    monthlyUsed,
    allocations: summaries,
  };
}

/**
 * Idempotently allocates new credits to a shop.
 * ADDITIVE RULE: Appends credits to the ledger without zeroing or replacing existing remaining credits.
 */
export async function allocateCredits({
  shopId,
  sourceType,
  amount,
  sourceId,
  expiresAt,
  idempotencyKey,
}: AllocateCreditsOptions) {
  if (amount <= 0) {
    throw new Error("Credit allocation amount must be greater than zero.");
  }

  // Idempotency Protection: Prevent double-crediting
  if (idempotencyKey) {
    const existing = await prisma.creditAllocation.findUnique({
      where: { idempotencyKey },
    });
    if (existing) {
      console.log(`[Credit Ledger] Idempotent hit: Allocation ${existing.id} already exists for key ${idempotencyKey}`);
      return existing;
    }
  }

  const newAllocation = await prisma.creditAllocation.create({
    data: {
      shopId,
      sourceType,
      sourceId,
      creditsAllocated: amount,
      creditsUsed: 0,
      creditsRemaining: amount,
      expiresAt: expiresAt || null,
      status: "ACTIVE",
      idempotencyKey: idempotencyKey || null,
    },
  });

  console.log(`[Credit Ledger] Allocated +${amount} credits (${sourceType}) to shop ${shopId}. Allocation ID: ${newAllocation.id}`);

  return newAllocation;
}

/**
 * Atomically reserve and consume credits in a database transaction.
 * Guarantees creditsRemaining >= 0 invariant under concurrent operations and enforces idempotency.
 */
export async function reserveAndConsumeCredits({
  shopId,
  creditsNeeded = 1,
  eventType = "AI_CHAT_REQUEST",
  provider,
  model,
  requestId,
  metadata,
}: ConsumeCreditsOptions) {
  // Idempotency check: If requestId already processed, return existing record without double-deduction
  if (requestId) {
    const existingEvent = await prisma.usageEvent.findFirst({
      where: { shopId, requestId },
    });
    if (existingEvent) {
      const currentBalance = await getAvailableCredits(shopId);
      return {
        success: true,
        consumedCredits: 0,
        duplicate: true,
        availableBalance: currentBalance.availableBalance,
        usageEventId: existingEvent.id,
      };
    }
  }

  const result = await prisma.$transaction(async (tx) => {
    const now = new Date();

    // Fetch active allocations within transaction
    const activeAllocations = await tx.creditAllocation.findMany({
      where: {
        shopId,
        status: "ACTIVE",
        creditsRemaining: { gt: 0 },
        OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
      },
      orderBy: [
        { expiresAt: "asc" }, // FEFO
        { createdAt: "asc" }, // FIFO
      ],
    });

    const totalAvailable = activeAllocations.reduce((sum, a) => sum + a.creditsRemaining, 0);

    if (totalAvailable < creditsNeeded) {
      return {
        success: false,
        reason: "usage_limit_reached",
        availableBalance: totalAvailable,
      };
    }

    let remainingToDeduct = creditsNeeded;
    let primaryAllocationId: string | null = null;

    for (const alloc of activeAllocations) {
      if (remainingToDeduct <= 0) break;

      const deductFromThis = Math.min(alloc.creditsRemaining, remainingToDeduct);

      // Atomic conditional decrement in MySQL engine
      const affected = await tx.creditAllocation.updateMany({
        where: {
          id: alloc.id,
          creditsRemaining: { gte: deductFromThis },
        },
        data: {
          creditsRemaining: { decrement: deductFromThis },
          creditsUsed: { increment: deductFromThis },
        },
      });

      if (affected.count > 0) {
        if (!primaryAllocationId) primaryAllocationId = alloc.id;
        remainingToDeduct -= deductFromThis;

        // Check if exhausted
        await tx.creditAllocation.updateMany({
          where: { id: alloc.id, creditsRemaining: { lte: 0 } },
          data: { status: "EXHAUSTED" },
        });
      }
    }

    if (remainingToDeduct > 0) {
      return {
        success: false,
        reason: "usage_limit_reached",
        availableBalance: 0,
      };
    }

    // Record usage event
    const usageEvent = await tx.usageEvent.create({
      data: {
        shopId,
        creditAllocationId: primaryAllocationId,
        requestId: requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
        eventType,
        creditsConsumed: creditsNeeded,
        provider: provider || "openai",
        model: model || "gpt-4o-mini",
        metadata: metadata || null,
      },
    });

    // Increment active subscription usage
    const activeSub = await tx.subscription.findFirst({
      where: { shopId, status: "ACTIVE" },
      orderBy: { createdAt: "desc" },
    });
    if (activeSub) {
      await tx.subscription.update({
        where: { id: activeSub.id },
        data: { currentUsage: { increment: creditsNeeded } },
      });
    }

    return {
      success: true,
      consumedCredits: creditsNeeded,
      availableBalance: totalAvailable - creditsNeeded,
      usageEventId: usageEvent.id,
    };
  });

  return result;
}

/**
 * Perform auditable manual credit adjustment by SaaS Admin.
 */
export async function adminAdjustCredits({
  shopId,
  amount,
  reason,
  adminId = "saas_admin",
}: {
  shopId: string;
  amount: number;
  reason: string;
  adminId?: string;
}) {
  const currentBalance = await getAvailableCredits(shopId);

  if (amount === 0) {
    throw new Error("Adjustment amount cannot be zero.");
  }

  let allocation = null;

  if (amount > 0) {
    allocation = await allocateCredits({
      shopId,
      sourceType: "ADMIN_ADJUSTMENT",
      amount,
      sourceId: `admin_${Date.now()}`,
    });
  } else {
    const absDeduct = Math.abs(amount);
    await prisma.$transaction(async (tx) => {
      const activeAllocations = await tx.creditAllocation.findMany({
        where: { shopId, status: "ACTIVE", creditsRemaining: { gt: 0 } },
        orderBy: [{ expiresAt: "asc" }, { createdAt: "asc" }],
      });

      let remainingToDeduct = absDeduct;
      for (const alloc of activeAllocations) {
        if (remainingToDeduct <= 0) break;
        const deduct = Math.min(alloc.creditsRemaining, remainingToDeduct);
        const newRem = alloc.creditsRemaining - deduct;
        const newUsed = alloc.creditsUsed + deduct;
        await tx.creditAllocation.update({
          where: { id: alloc.id },
          data: {
            creditsRemaining: newRem,
            creditsUsed: newUsed,
            status: newRem <= 0 ? "EXHAUSTED" : "ACTIVE",
          },
        });
        remainingToDeduct -= deduct;
      }
    });
  }

  const newBalance = await getAvailableCredits(shopId);

  // Mandatory Audit Log
  await prisma.auditLog.create({
    data: {
      shopId,
      action: "ADMIN_MANUAL_CREDIT_ADJUSTMENT",
      resource: `Admin ${adminId} adjusted credits: ${amount > 0 ? "+" : ""}${amount}. Reason: ${reason}`,
      status: "SUCCESSFUL",
      metadata: {
        adminId,
        amount,
        reason,
        previousBalance: currentBalance.availableBalance,
        newBalance: newBalance.availableBalance,
      },
    },
  });

  return {
    success: true,
    previousBalance: currentBalance.availableBalance,
    newBalance: newBalance.availableBalance,
    adjustmentAmount: amount,
    allocation,
  };
}
