const db = require('../models');
const { Op } = require('sequelize');
const asyncHandler = require('../middlewares/asyncHandler');
const { sendResponse, sendError } = require('../utils/response');
const { getPagination, getPagingData } = require('../utils/pagination');
const { generateNextNumber } = require('../services/numberFormatService');
const { toNum } = require('../services/fgStockLedgerService');
const glPostingService = require('../services/glPostingService');

const generateReceiptNo = async (companyId, fiscalYearId, transaction) => {
  try {
    return await generateNextNumber(companyId, 'RECEIPT_ENTRY', fiscalYearId, transaction);
  } catch (e) {
    const year = new Date().getFullYear();
    const prefix = `RCPT${year}`;
    const last = await db.ReceiptEntry.findOne({
      where: {
        company_id: companyId,
        receipt_no: { [Op.like]: `${prefix}%` }
      },
      attributes: ['receipt_no'],
      order: [['receipt_no', 'DESC']],
      ...(transaction ? { transaction } : {})
    });
    const next = (parseInt(String(last?.receipt_no || '').slice(-4), 10) || 0) + 1;
    return `${prefix}${String(next).padStart(4, '0')}`;
  }
};

const getPostedAppliedAmount = async (invoiceId, excludeReceiptId = null, transaction = null) => {
  const where = { invoice_id: invoiceId, status: 'POSTED' };
  if (excludeReceiptId) where.id = { [Op.ne]: excludeReceiptId };
  try {
    const posted = await db.ReceiptEntry.sum(
      db.sequelize.literal('(paid_amount - advance_amount + adjusted_advance_amount)'),
      { where, transaction }
    );
    return toNum(posted);
  } catch (_) {
    // Backward compatibility when advance columns are not migrated yet.
    const postedLegacy = await db.ReceiptEntry.sum('paid_amount', { where, transaction });
    return toNum(postedLegacy);
  }
};

const getInvoiceWithOutstanding = async (invoiceId, excludeReceiptId = null, transaction = null) => {
  const invoice = await db.SalesInvoice.findByPk(invoiceId, {
    attributes: ['id', 'customer_id', 'invoice_number', 'total_amount', 'status'],
    include: [{ model: db.BusinessPartner, as: 'customer', attributes: ['id', 'bp_number', 'name'], required: false }],
    transaction
  });
  if (!invoice) throw new Error('Invoice not found');
  const postedApplied = await getPostedAppliedAmount(invoiceId, excludeReceiptId, transaction);
  const outstanding = Math.max(toNum(invoice.total_amount) - postedApplied, 0);
  return { invoice, postedApplied, outstanding };
};

const normalizePaymentItems = (items = []) => items.map((it, idx) => ({
  line_no: idx + 1,
  payment_mode: String(it.payment_mode || '').toUpperCase(),
  amount: toNum(it.amount),
  bank_name: it.bank_name || null,
  ifsc_code: it.ifsc_code || null,
  reference_no: it.reference_no || null,
  remarks: it.remarks || null
}));

const validatePaymentItems = (items) => {
  if (!Array.isArray(items) || items.length === 0) throw new Error('At least one payment item is required');
  items.forEach((it) => {
    if (!['CASH', 'BANK', 'UPI', 'CHEQUE', 'NEFT'].includes(it.payment_mode)) {
      throw new Error('Invalid payment mode in payment items');
    }
    if (toNum(it.amount) <= 0) throw new Error('Payment item amount should be greater than 0');
    if ((it.payment_mode === 'BANK' || it.payment_mode === 'NEFT' || it.payment_mode === 'CHEQUE') && (!it.bank_name || !it.ifsc_code)) {
      throw new Error('bank_name and ifsc_code are required for BANK/NEFT/CHEQUE payment items');
    }
    if (it.payment_mode !== 'CASH' && !it.reference_no) {
      throw new Error('reference_no is required for non-cash payment items');
    }
  });
};

const computeReceiptFigures = async ({
  customer,
  invoiceId,
  paidAmount,
  adjustAdvance,
  excludeReceiptId = null,
  transaction = null
}) => {
  const customerAdvance = toNum(customer?.customer_advance_balance);
  let invoice = null;
  let invoiceOutstanding = 0;

  if (invoiceId) {
    const data = await getInvoiceWithOutstanding(invoiceId, excludeReceiptId, transaction);
    invoice = data.invoice;
    invoiceOutstanding = toNum(data.outstanding);
    if (invoice.customer_id !== customer.id) {
      throw new Error('Selected invoice does not belong to the selected customer');
    }
  }

  const adjustedAdvanceAmount = invoice && adjustAdvance
    ? Math.min(customerAdvance, invoiceOutstanding)
    : 0;

  const outstandingAfterAdvance = Math.max(invoiceOutstanding - adjustedAdvanceAmount, 0);
  const paidToInvoiceAmount = invoice ? Math.min(paidAmount, outstandingAfterAdvance) : 0;
  const advanceAmount = invoice
    ? Math.max(paidAmount - paidToInvoiceAmount, 0)
    : paidAmount;
  const balanceAmount = invoice
    ? Math.max(outstandingAfterAdvance - paidToInvoiceAmount, 0)
    : 0;

  return {
    invoice,
    invoiceOutstanding,
    adjustedAdvanceAmount,
    paidToInvoiceAmount,
    advanceAmount,
    balanceAmount
  };
};

exports.getDeliveryChallanOptions = asyncHandler(async (req, res) => {
  const companyId = req.query.companyId || req.user?.companyId;
  const plantId = parseInt(req.query.plantId, 10);
  const fiscalYearId = parseInt(req.query.fiscalYearId, 10);
  if (!companyId || !Number.isFinite(plantId) || !Number.isFinite(fiscalYearId)) {
    return sendError(res, 'companyId, plantId and fiscalYearId are required', 400);
  }

  const challans = await db.DeliveryChallan.findAll({
    where: {
      company_id: companyId,
      plant_id: plantId,
      fiscal_year_id: fiscalYearId,
      [Op.or]: [
        { delivery_status: 'DELIVERED' },
        { status: { [Op.in]: ['DELIVERED', 'CLOSED'] } }
      ]
    },
    attributes: ['id', 'challan_no', 'invoice_id', 'customer_id', 'customer_po_no', 'payment_type'],
    include: [
      { model: db.SalesInvoice, as: 'invoice', attributes: ['id', 'invoice_number', 'total_amount'], required: false },
      { model: db.BusinessPartner, as: 'customer', attributes: ['id', 'bp_number', 'name'], required: false },
      { model: db.DeliveryChallanItem, as: 'items', attributes: ['delivered_qty'], required: false }
    ],
    order: [['id', 'DESC']]
  });

  const payload = [];
  for (const c of challans) {
    const invoiceId = parseInt(c.invoice_id, 10);
    if (!Number.isFinite(invoiceId) || invoiceId <= 0) continue;
    const postedApplied = await getPostedAppliedAmount(invoiceId);
    const invoiceTotal = toNum(c.invoice?.total_amount || 0);
    const outstanding = Math.max(invoiceTotal - postedApplied, 0);
    const deliveryQty = (c.items || []).reduce((sum, it) => sum + toNum(it.delivered_qty), 0);
    payload.push({
      id: c.id,
      challan_no: c.challan_no,
      invoice_id: invoiceId,
      invoice_no: c.invoice?.invoice_number || '',
      customer_po_no: c.customer_po_no || '',
      customer_name: c.customer?.name || '',
      customer_code: c.customer?.bp_number || '',
      payment_type: c.payment_type || '',
      delivery_qty: deliveryQty,
      invoice_total: invoiceTotal,
      paid_amount: postedApplied,
      balance_amount: outstanding
    });
  }

  sendResponse(res, 200, true, 'Delivery challan options retrieved successfully', payload);
});

exports.getInvoiceOptions = asyncHandler(async (req, res) => {
  const companyId = req.query.companyId || req.user?.companyId;
  const plantId = parseInt(req.query.plantId, 10);
  const fiscalYearId = parseInt(req.query.fiscalYearId, 10);
  const customerId = parseInt(req.query.customerId, 10);
  if (!companyId || !Number.isFinite(plantId) || !Number.isFinite(fiscalYearId)) {
    return sendError(res, 'companyId, plantId and fiscalYearId are required', 400);
  }

  const where = {
    company_id: companyId,
    plant_id: plantId,
    fiscal_year_id: fiscalYearId,
    status: { [Op.ne]: 'CANCELLED' }
  };
  if (Number.isFinite(customerId) && customerId > 0) where.customer_id = customerId;

  const invoices = await db.SalesInvoice.findAll({
    where,
    attributes: ['id', 'invoice_number', 'invoice_date', 'customer_id', 'total_amount'],
    include: [{ model: db.BusinessPartner, as: 'customer', attributes: ['id', 'bp_number', 'name'], required: false }],
    order: [['invoice_date', 'DESC'], ['id', 'DESC']]
  });

  const payload = [];
  for (const inv of invoices) {
    const postedApplied = await getPostedAppliedAmount(inv.id);
    const outstanding = Math.max(toNum(inv.total_amount) - postedApplied, 0);
    if (outstanding <= 0) continue;
    payload.push({
      id: inv.id,
      invoice_number: inv.invoice_number,
      invoice_date: inv.invoice_date,
      customer_id: inv.customer_id,
      customer_name: inv.customer?.name || '',
      customer_code: inv.customer?.bp_number || '',
      invoice_total: toNum(inv.total_amount),
      paid_amount: postedApplied,
      balance_amount: outstanding
    });
  }

  sendResponse(res, 200, true, 'Unpaid invoice options retrieved successfully', payload);
});

exports.getCustomerOptions = asyncHandler(async (req, res) => {
  const companyId = req.query.companyId || req.user?.companyId;
  if (!companyId) return sendError(res, 'companyId is required', 400);
  let rows = [];
  try {
    rows = await db.BusinessPartner.findAll({
      where: {
        company_id: companyId,
        status: 'ACTIVE',
        bp_type: { [Op.in]: ['CUSTOMER', 'BOTH'] }
      },
      attributes: ['id', 'bp_number', 'name', 'bp_type', 'customer_advance_balance'],
      order: [['name', 'ASC']]
    });
  } catch (_) {
    // Backward compatibility when advance balance column is not migrated yet.
    rows = await db.BusinessPartner.findAll({
      where: {
        company_id: companyId,
        status: 'ACTIVE',
        bp_type: { [Op.in]: ['CUSTOMER', 'BOTH'] }
      },
      attributes: ['id', 'bp_number', 'name', 'bp_type'],
      order: [['name', 'ASC']]
    });
  }
  sendResponse(res, 200, true, 'Customer options retrieved successfully', rows);
});

exports.getAll = asyncHandler(async (req, res) => {
  const { page = 1, limit = 25, search, status, companyId, plantId, fiscalYearId } = req.query;
  const deliveryChallanId = parseInt(req.query.deliveryChallanId, 10);
  const invoiceId = parseInt(req.query.invoiceId, 10);
  const { limit: limitNum, offset } = getPagination(page, limit);
  const where = {};
  if (companyId) where.company_id = companyId;
  else if (req.user && !req.user.isSuperAdmin) where.company_id = req.user.companyId;
  if (plantId) where.plant_id = plantId;
  if (fiscalYearId) where.fiscal_year_id = fiscalYearId;
  if (status) where.status = status;
  if (Number.isFinite(deliveryChallanId) && deliveryChallanId > 0) where.delivery_challan_id = deliveryChallanId;
  if (Number.isFinite(invoiceId) && invoiceId > 0) where.invoice_id = invoiceId;
  if (search) where[Op.or] = [{ receipt_no: { [Op.like]: `%${search}%` } }];

  const rows = await db.ReceiptEntry.findAndCountAll({
    where,
    include: [
      { model: db.BusinessPartner, as: 'customer', attributes: ['id', 'bp_number', 'name'], required: false },
      { model: db.SalesInvoice, as: 'invoice', attributes: ['id', 'invoice_number'], required: false },
      { model: db.DeliveryChallan, as: 'deliveryChallan', attributes: ['id', 'challan_no', 'customer_po_no', 'payment_type'], required: false }
    ],
    limit: limitNum,
    offset,
    order: [['id', 'DESC']]
  });
  const response = getPagingData(rows, page, limitNum);
  sendResponse(res, 200, true, 'Receipt entries retrieved successfully', response);
});

exports.getById = asyncHandler(async (req, res) => {
  const row = await db.ReceiptEntry.findByPk(req.params.id, {
    include: [
      { model: db.BusinessPartner, as: 'customer', attributes: ['id', 'bp_number', 'name'] },
      { model: db.SalesInvoice, as: 'invoice', attributes: ['id', 'invoice_number', 'invoice_date', 'total_amount'] },
      { model: db.DeliveryChallan, as: 'deliveryChallan', attributes: ['id', 'challan_no', 'payment_type', 'customer_po_no'] },
      { model: db.ReceiptEntryItem, as: 'items', required: false }
    ]
  });
  if (!row) return sendError(res, 'Receipt entry not found', 404);
  sendResponse(res, 200, true, 'Receipt entry retrieved successfully', row);
});

exports.create = asyncHandler(async (req, res) => {
  const data = req.body || {};
  const userId = req.user?.userId || null;
  const companyId = req.user?.companyId;
  let customerId = parseInt(data.customer_id, 10);
  let invoiceId = parseInt(data.invoice_id, 10);
  const deliveryChallanId = parseInt(data.delivery_challan_id, 10);
  const paymentItems = normalizePaymentItems(data.payment_items || []);
  const adjustAdvance = !!data.adjust_advance;

  if (!companyId) return sendError(res, 'Company ID is required', 400);
  if (!data.plant_id || !data.fiscal_year_id) return sendError(res, 'plant_id and fiscal_year_id are required', 400);
  if ((!Number.isFinite(customerId) || customerId <= 0) && Number.isFinite(deliveryChallanId) && deliveryChallanId > 0) {
    const challan = await db.DeliveryChallan.findByPk(deliveryChallanId);
    if (challan) {
      customerId = parseInt(challan.customer_id, 10);
      if (!Number.isFinite(invoiceId) || invoiceId <= 0) invoiceId = parseInt(challan.invoice_id, 10);
    }
  }
  if (!Number.isFinite(customerId) || customerId <= 0) return sendError(res, 'customer_id is required', 400);

  const t = await db.sequelize.transaction();
  try {
    const customer = await db.BusinessPartner.findByPk(customerId, { transaction: t });
    if (!customer) throw new Error('Customer not found');
    if (!['CUSTOMER', 'BOTH'].includes(String(customer.bp_type || '').toUpperCase())) {
      throw new Error('Selected business partner is not a customer');
    }

    validatePaymentItems(paymentItems);
    const paidAmount = paymentItems.reduce((sum, it) => sum + toNum(it.amount), 0);
    const receiptCalc = await computeReceiptFigures({
      customer,
      invoiceId: Number.isFinite(invoiceId) && invoiceId > 0 ? invoiceId : null,
      paidAmount,
      adjustAdvance,
      transaction: t
    });

    const status = String(data.status || 'DRAFT').toUpperCase();
    if (!['DRAFT', 'POSTED'].includes(status)) throw new Error('Invalid status');

    const nextAdvanceBalance = Math.max(
      toNum(customer.customer_advance_balance) - receiptCalc.adjustedAdvanceAmount + receiptCalc.advanceAmount,
      0
    );

    const rec = await db.ReceiptEntry.create({
      company_id: companyId,
      plant_id: data.plant_id,
      fiscal_year_id: data.fiscal_year_id,
      receipt_no: data.receipt_no || await generateReceiptNo(companyId, data.fiscal_year_id, t),
      receipt_date: data.receipt_date || new Date(),
      customer_id: customer.id,
      invoice_id: receiptCalc.invoice?.id || null,
      delivery_challan_id: Number.isFinite(parseInt(data.delivery_challan_id, 10)) ? parseInt(data.delivery_challan_id, 10) : null,
      invoice_total: toNum(receiptCalc.invoice?.total_amount || 0),
      paid_amount: paidAmount,
      advance_amount: receiptCalc.advanceAmount,
      adjusted_advance_amount: receiptCalc.adjustedAdvanceAmount,
      balance_amount: receiptCalc.balanceAmount,
      payment_mode: paymentItems[0]?.payment_mode || 'CASH',
      bank_account: data.bank_account || null,
      bank_name: paymentItems[0]?.bank_name || null,
      ifsc_code: paymentItems[0]?.ifsc_code || null,
      reference_no: paymentItems[0]?.reference_no || null,
      collected_from: String(data.collected_from || 'DELIVERY').toUpperCase(),
      remarks: data.remarks || null,
      status,
      posted_at: status === 'POSTED' ? new Date() : null,
      posted_by: status === 'POSTED' ? userId : null,
      created_by: userId,
      updated_by: userId
    }, { transaction: t });

    await Promise.all(paymentItems.map((it) => db.ReceiptEntryItem.create({
      receipt_entry_id: rec.id,
      line_no: it.line_no,
      payment_mode: it.payment_mode,
      amount: toNum(it.amount),
      bank_name: it.bank_name,
      ifsc_code: it.ifsc_code,
      reference_no: it.reference_no,
      remarks: it.remarks
    }, { transaction: t })));

    if (status === 'POSTED') {
      await customer.update({ customer_advance_balance: nextAdvanceBalance }, { transaction: t });
      await glPostingService.postCustomerReceiptToGL(rec, userId);
    }

    await t.commit();
    const created = await db.ReceiptEntry.findByPk(rec.id);
    sendResponse(res, 201, true, 'Receipt entry created successfully', created);
  } catch (e) {
    await t.rollback();
    return sendError(res, e.message || 'Failed to create receipt entry', 400);
  }
});

exports.update = asyncHandler(async (req, res) => {
  const id = parseInt(req.params.id, 10);
  const data = req.body || {};
  const userId = req.user?.userId || null;
  const rec = await db.ReceiptEntry.findByPk(id);
  if (!rec) return sendError(res, 'Receipt entry not found', 404);
  if (rec.status !== 'DRAFT') return sendError(res, 'Only DRAFT receipt can be updated', 400);

  const paidAmount = Array.isArray(data.payment_items)
    ? normalizePaymentItems(data.payment_items).reduce((sum, it) => sum + toNum(it.amount), 0)
    : toNum(data.paid_amount ?? rec.paid_amount);
  const paymentMode = data.payment_mode ? String(data.payment_mode).toUpperCase() : rec.payment_mode;
  const customerId = parseInt(data.customer_id ?? rec.customer_id, 10);
  const invoiceId = data.invoice_id === null || data.invoice_id === ''
    ? null
    : parseInt(data.invoice_id ?? rec.invoice_id, 10);
  const adjustAdvance = data.adjust_advance === undefined ? !!toNum(rec.adjusted_advance_amount) : !!data.adjust_advance;
  if (paidAmount <= 0) return sendError(res, 'Paid Amount should be greater than 0', 400);
  if (!['CASH', 'BANK', 'UPI', 'CHEQUE', 'NEFT'].includes(paymentMode)) {
    return sendError(res, 'Invalid payment_mode', 400);
  }
  if ((paymentMode === 'BANK' || paymentMode === 'NEFT' || paymentMode === 'CHEQUE') && (!(data.bank_name ?? rec.bank_name) || !(data.ifsc_code ?? rec.ifsc_code))) {
    return sendError(res, 'bank_name and ifsc_code are required for BANK/NEFT/CHEQUE payment', 400);
  }
  if (paymentMode !== 'CASH' && !(data.reference_no ?? rec.reference_no)) {
    return sendError(res, 'reference_no is required for selected payment mode', 400);
  }

  const t = await db.sequelize.transaction();
  try {
    const customer = await db.BusinessPartner.findByPk(customerId, { transaction: t });
    if (!customer) throw new Error('Customer not found');
    const receiptCalc = await computeReceiptFigures({
      customer,
      invoiceId: Number.isFinite(invoiceId) && invoiceId > 0 ? invoiceId : null,
      paidAmount,
      adjustAdvance,
      excludeReceiptId: rec.id,
      transaction: t
    });

    await rec.update({
      receipt_date: data.receipt_date || rec.receipt_date,
      customer_id: customer.id,
      invoice_id: receiptCalc.invoice?.id || null,
      paid_amount: paidAmount,
      advance_amount: receiptCalc.advanceAmount,
      adjusted_advance_amount: receiptCalc.adjustedAdvanceAmount,
      balance_amount: receiptCalc.balanceAmount,
      payment_mode: paymentMode,
      bank_account: data.bank_account ?? rec.bank_account,
      bank_name: data.bank_name ?? rec.bank_name,
      ifsc_code: data.ifsc_code ?? rec.ifsc_code,
      reference_no: data.reference_no ?? rec.reference_no,
      collected_from: data.collected_from ? String(data.collected_from).toUpperCase() : rec.collected_from,
      remarks: data.remarks ?? rec.remarks,
      invoice_total: toNum(receiptCalc.invoice?.total_amount || 0),
      updated_by: userId
    }, { transaction: t });

    if (Array.isArray(data.payment_items)) {
      const paymentItems = normalizePaymentItems(data.payment_items || []);
      validatePaymentItems(paymentItems);
      await db.ReceiptEntryItem.destroy({ where: { receipt_entry_id: rec.id }, transaction: t });
      await Promise.all(paymentItems.map((it) => db.ReceiptEntryItem.create({
        receipt_entry_id: rec.id,
        line_no: it.line_no,
        payment_mode: it.payment_mode,
        amount: toNum(it.amount),
        bank_name: it.bank_name,
        ifsc_code: it.ifsc_code,
        reference_no: it.reference_no,
        remarks: it.remarks
      }, { transaction: t })));
    }

    await t.commit();
    const updated = await db.ReceiptEntry.findByPk(rec.id);
    sendResponse(res, 200, true, 'Receipt entry updated successfully', updated);
  } catch (e) {
    await t.rollback();
    return sendError(res, e.message || 'Failed to update receipt entry', 400);
  }
});

exports.updateStatus = asyncHandler(async (req, res) => {
  const id = parseInt(req.params.id, 10);
  const userId = req.user?.userId || null;
  const status = String(req.body?.status || '').toUpperCase();
  if (status !== 'POSTED') return sendError(res, 'Only POSTED status is allowed', 400);
  const rec = await db.ReceiptEntry.findByPk(id);
  if (!rec) return sendError(res, 'Receipt entry not found', 404);
  if (rec.status !== 'DRAFT') return sendError(res, 'Only DRAFT receipt can be posted', 400);

  const t = await db.sequelize.transaction();
  try {
    const customer = await db.BusinessPartner.findByPk(rec.customer_id, { transaction: t });
    if (!customer) throw new Error('Customer not found');
    const nextAdvanceBalance = Math.max(
      toNum(customer.customer_advance_balance) - toNum(rec.adjusted_advance_amount) + toNum(rec.advance_amount),
      0
    );

    await rec.update({
      status: 'POSTED',
      posted_at: new Date(),
      posted_by: userId,
      updated_by: userId
    }, { transaction: t });
    await customer.update({ customer_advance_balance: nextAdvanceBalance }, { transaction: t });
    await glPostingService.postCustomerReceiptToGL(rec, userId);
    await t.commit();
    const updated = await db.ReceiptEntry.findByPk(rec.id);
    sendResponse(res, 200, true, 'Receipt entry posted successfully', updated);
  } catch (e) {
    await t.rollback();
    return sendError(res, e.message || 'Failed to post receipt entry', 400);
  }
});

exports.delete = asyncHandler(async (req, res) => {
  const id = parseInt(req.params.id, 10);
  const rec = await db.ReceiptEntry.findByPk(id);
  if (!rec) return sendError(res, 'Receipt entry not found', 404);
  if (rec.status !== 'DRAFT') return sendError(res, 'Only DRAFT receipt can be deleted', 400);

  const t = await db.sequelize.transaction();
  try {
    await db.ReceiptEntryItem.destroy({ where: { receipt_entry_id: rec.id }, transaction: t });
    await rec.destroy({ transaction: t });
    await t.commit();
    return sendResponse(res, 200, true, 'Receipt entry deleted successfully');
  } catch (e) {
    await t.rollback();
    return sendError(res, e.message || 'Failed to delete receipt entry', 400);
  }
});

