import mongoose from 'mongoose';
import * as saleRepo from '../repositories/saleRepository.js';
import * as productRepo from '../repositories/productRepository.js';
import * as stockHistoryRepo from '../repositories/stockHistoryRepository.js';
import { getSettings } from '../repositories/settingsRepository.js';
import Counter from '../models/Counter.js';
import logAudit from '../utils/audit.js';
import { roleCan } from '../utils/permissions.js';
import { ACTIONS } from '../utils/permissions.js';
import { getVariant, syncProductFromVariants } from '../utils/variantUtils.js';

const generateBillNumber = async (session) => {
  const seq = await Counter.getNextSequence('saleBill', session);
  const year = new Date().getFullYear();
  const num = String(seq).padStart(8, '0');
  return `BILL-${year}-${num}`;
};

const seedBillCounter = async () => {
  const latest = await saleRepo.findLatest();
  if (!latest || !latest.billNumber) return;
  const match = latest.billNumber.match(/BILL-\d{4}-(\d{8})/);
  if (!match) return;
  const maxSeq = parseInt(match[1], 10);
  await Counter.updateOne({ _id: 'saleBill' }, { $setOnInsert: { seq: maxSeq } }, { upsert: true });
};

const calculateDiscount = (quantity, discountConfig, unitPrice) => {
  const tiers = [discountConfig.tier3, discountConfig.tier2, discountConfig.tier1];
  for (const tier of tiers) {
    if (quantity >= tier.min && quantity <= tier.max) {
      const discountAmount = (unitPrice * quantity * tier.discount) / 100;
      return { discount: discountAmount, discountPercent: tier.discount };
    }
  }
  return { discount: 0, discountPercent: 0 };
};

const isTransientTxnError = (error) => {
  if (!error) return false;
  const labels = error.errorLabels || [];
  if (labels.includes('TransientTransactionError')) return true;
  return /Write conflict|Please retry your operation/i.test(error.message || '');
};

const createSaleOnce = async (data, req) => {
  const session = await mongoose.startSession();
  session.startTransaction();

  try {
    const existingCounter = await Counter.findById('saleBill');
    if (!existingCounter) {
      await seedBillCounter();
    }

    const settings = await getSettings();
    const discountConfig = settings.discountConfig;

    const productIds = [...new Set(data.items.map((item) => item.product))];
    const products = await productRepo.findMultiple(productIds);
    const productMap = new Map(products.map((p) => [p._id.toString(), p]));

    const saleItems = [];
    let subtotal = 0;
    let totalDiscount = 0;

    for (const item of data.items) {
      const product = productMap.get(item.product.toString());
      if (!product) {
        throw Object.assign(new Error(`Product not found: ${item.product}`), { statusCode: 404 });
      }

      let variant = null;
      let variantLabel = '';
      let sellingUnit = 'pcs';
      const size = item.size || '';

      if (product.variants && product.variants.length > 0) {
        variant = getVariant(product, item.variant);
        if (!variant) {
          throw Object.assign(new Error(`Variant is required for ${product.name}`), { statusCode: 400 });
        }
        variantLabel = variant.label;
        sellingUnit = variant.sellingUnit || 'pcs';
      } else if (product.trackingType === 'SIZE' && size) {
        variant = product.sizeVariants.find((v) => v.size === size) || null;
        variantLabel = variant?.size || size;
      }

      const price = variant
        ? Number(variant.sellingPrice) || 0
        : Number(product.sellingPrice) || 0;

      // Historical COGS: snapshot the purchase cost at sale time
      const costPrice =
        (variant && variant.purchasePrice != null ? Number(variant.purchasePrice) : null) ??
        (product.purchasePrice != null ? Number(product.purchasePrice) : null);

      let available;
      if (product.variants && product.variants.length > 0) {
        available = Number(variant?.currentStock) || 0;
      } else if (product.trackingType === 'SIZE' && variant) {
        available = Number(variant.stock) || 0;
      } else {
        available = Number(product.currentStock) || 0;
      }

      if (available < item.quantity) {
        throw Object.assign(
          new Error(`Insufficient stock for ${product.name}${variantLabel ? ` (${variantLabel})` : ''}. Available: ${available}`),
          { statusCode: 400 }
        );
      }

      const { discount } = calculateDiscount(item.quantity, discountConfig, price);
      const itemTotal = price * item.quantity - discount;

      saleItems.push({
        product: product._id,
        productName: product.name,
        quantity: item.quantity,
        price,
        discount,
        total: itemTotal,
        size,
        variant: variant?.variantId || '',
        variantLabel,
        sellingUnit,
        sku: variant?.sku || '',
        barcode: variant?.barcode || '',
        costPrice: Number.isFinite(costPrice) ? costPrice : null,
        measurement:
          variant?.measurementValue != null && variant?.measurementUnit
            ? `${variant.measurementValue} ${variant.measurementUnit}`
            : '',
      });

      subtotal += price * item.quantity;
      totalDiscount += discount;

      const previousStock = product.currentStock;
      if (product.variants && product.variants.length > 0 && variant) {
        variant.currentStock = (Number(variant.currentStock) || 0) - item.quantity;
        syncProductFromVariants(product);
      } else if (product.trackingType === 'SIZE' && variant) {
        variant.stock -= item.quantity;
        product.currentStock = product.sizeVariants.reduce((sum, v) => sum + (v.stock || 0), 0);
      } else {
        product.currentStock = (Number(product.currentStock) || 0) - item.quantity;
      }
      await product.save({ session });

      await stockHistoryRepo.create(
        {
          product: product._id,
          productName: product.name,
          type: 'SALE',
          quantity: item.quantity,
          previousStock,
          newStock: product.currentStock,
          variant: variant?.variantId || '',
          variantLabel,
          size,
          sellingUnit,
        },
        session
      );
    }

    const grandTotal = subtotal - totalDiscount;
    const billNumber = await generateBillNumber(session);

    const paymentMethod = ['CASH', 'UPI', 'CARD'].includes(data.paymentMethod)
      ? data.paymentMethod
      : 'CASH';

    let amountReceived = null;
    let changeDue = null;
    if (paymentMethod === 'CASH' && data.amountReceived != null) {
      const received = Number(data.amountReceived);
      if (!isNaN(received) && received >= 0) {
        if (received < grandTotal) {
          throw Object.assign(
            new Error('Amount received is less than the grand total'),
            { statusCode: 400 }
          );
        }
        amountReceived = Math.round(received * 100) / 100;
        changeDue = Math.round((received - grandTotal) * 100) / 100;
      }
    }

    const saleData = {
      billNumber,
      items: saleItems,
      subtotal,
      discount: totalDiscount,
      grandTotal,
      paymentMethod,
      customerName: (data.customerName || '').trim().slice(0, 120),
      customerPhone: (data.customerPhone || '').trim().slice(0, 20),
      amountReceived,
      changeDue,
      cashier: req?.user
        ? { id: req.user._id, name: req.user.name }
        : { id: null, name: '' },
      register: data.register || '',
      shiftId: data.shiftId || null,
    };

    const sale = await saleRepo.create(saleData, session);
    await session.commitTransaction();
    logAudit(req, 'SALE_CREATED', 'sale', sale._id, { billNumber, grandTotal });
    return sale;
  } catch (error) {
    await session.abortTransaction();
    throw error;
  } finally {
    session.endSession();
  }
};

const createSale = async (data, req) => {
  // Concurrent checkouts touching the same product document can abort with a
  // transient write conflict; retry with jittered backoff so parallel sales
  // all succeed cleanly instead of failing under burst load.
  const MAX_ATTEMPTS = 8;
  let lastError;
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
    try {
      return await createSaleOnce(data, req);
    } catch (error) {
      lastError = error;
      if (!isTransientTxnError(error) || attempt === MAX_ATTEMPTS) throw error;
      const jitter = Math.floor(Math.random() * 80);
      await new Promise((r) => setTimeout(r, 60 * attempt + jitter));
    }
  }
  throw lastError;
};

const getAllSales = async (query, user = null) => {
  // Cashiers without sale.view.all may only see their own sales
  if (user && !roleCan(user.role, ACTIONS.SALE_VIEW_ALL)) {
    return saleRepo.findAll({ ...query, cashierId: user._id });
  }
  return saleRepo.findAll(query);
};

const getSaleById = async (id) => {
  const sale = await saleRepo.findById(id);
  if (!sale) throw Object.assign(new Error('Sale not found'), { statusCode: 404 });
  return sale;
};

export default { createSale, getAllSales, getSaleById, generateBillNumber };