import mongoose from 'mongoose';
import Shift from '../models/Shift.js';
import CashMovement from '../models/CashMovement.js';
import Counter from '../models/Counter.js';
import Sale from '../models/Sale.js';
import SaleReturn from '../models/SaleReturn.js';
import logAudit from '../utils/audit.js';

const round2 = (n) => Math.round((Number(n) || 0) * 100) / 100;

const assertOpenShift = async (shiftId) => {
  const shift = await Shift.findById(shiftId);
  if (!shift) throw Object.assign(new Error('Shift not found'), { statusCode: 404 });
  if (shift.status !== 'OPEN') {
    throw Object.assign(new Error('This shift is already closed'), { statusCode: 400 });
  }
  return shift;
};

const computeTotals = async (shift, session = null) => {
  const end = shift.closedAt || new Date();

  const salesAgg = await Sale.aggregate(
    [
      {
        $match: {
          createdAt: { $gte: shift.openedAt, $lte: end },
          'cashier.id': shift.cashier.id,
          register: shift.register,
        },
      },
      { $group: { _id: '$paymentMethod', total: { $sum: '$grandTotal' }, count: { $sum: 1 } } },
    ],
    session ? { session } : undefined
  );

  const refundsAgg = await SaleReturn.aggregate(
    [
      {
        $match: {
          createdAt: { $gte: shift.openedAt, $lte: end },
          'processedBy.id': shift.cashier.id,
        },
      },
      { $group: { _id: '$refundMethod', total: { $sum: '$totalRefund' }, count: { $sum: 1 } } },
    ],
    session ? { session } : undefined
  );

  const movements = await CashMovement.aggregate(
    [
      { $match: { shift: shift._id } },
      { $group: { _id: '$type', total: { $sum: '$amount' } } },
    ],
    session ? { session } : undefined
  );

  const byMethod = Object.fromEntries(salesAgg.map((r) => [r._id, r]));
  const refundsByMethod = Object.fromEntries(refundsAgg.map((r) => [r._id, r]));
  const movByType = Object.fromEntries(movements.map((r) => [r._id, r.total]));

  const cashSales = round2(byMethod.CASH?.total || 0);
  const upiSales = round2(byMethod.UPI?.total || 0);
  const cardSales = round2(byMethod.CARD?.total || 0);
  const cashRefunds = round2(refundsByMethod.CASH?.total || 0);
  const upiRefunds = round2(refundsByMethod.UPI?.total || 0);
  const cardRefunds = round2(refundsByMethod.CARD?.total || 0);
  const cashIn = round2(movByType.CASH_IN || 0);
  const cashOut = round2(movByType.CASH_OUT || 0);

  const totals = {
    cashSales,
    upiSales,
    cardSales,
    totalSales: round2(cashSales + upiSales + cardSales),
    saleCount: salesAgg.reduce((s, r) => s + r.count, 0),
    cashRefunds,
    upiRefunds,
    cardRefunds,
    totalRefunds: round2(cashRefunds + upiRefunds + cardRefunds),
    refundCount: refundsAgg.reduce((s, r) => s + r.count, 0),
    cashIn,
    cashOut,
  };

  // Physical cash math: opening + cash sales - cash refunds + cash in - cash out
  totals.expectedCash = round2(
    shift.openingCash + cashSales - cashRefunds + cashIn - cashOut
  );
  return totals;
};

const openShift = async (data, req) => {
  const register = String(data.register || '').trim();
  if (!register) throw Object.assign(new Error('Register/counter name is required'), { statusCode: 400 });
  if (data.openingCash == null || Number(data.openingCash) < 0) {
    throw Object.assign(new Error('Opening cash must be zero or more'), { statusCode: 400 });
  }

  const clash = await Shift.findOne({
    status: 'OPEN',
    $or: [{ register }, { 'cashier.id': req.user._id }],
  });
  if (clash) {
    const why = clash.register === register ? `register "${register}"` : 'this cashier';
    throw Object.assign(
      new Error(`An active shift already exists for ${why} (shift ${clash.shiftNumber}). Close it first.`),
      { statusCode: 409 }
    );
  }

  const seq = await Counter.getNextSequence('shiftNumber');
  const shift = await Shift.create({
    shiftNumber: `SHIFT-${String(seq).padStart(6, '0')}`,
    register,
    cashier: { id: req.user._id, name: req.user.name },
    openingCash: Number(data.openingCash),
    openingNote: data.openingNote || '',
    openedAt: data.openedAt || new Date(),
  });

  logAudit(req, 'SHIFT_OPENED', 'shift', shift._id, {
    shiftNumber: shift.shiftNumber,
    register,
    openingCash: shift.openingCash,
  }, { description: `Shift ${shift.shiftNumber} opened for register ${register}` });
  return shift;
};

const addMovement = async (shiftId, data, req) => {
  const shift = await assertOpenShift(shiftId);
  const amount = Number(data.amount);
  if (!amount || amount <= 0) {
    throw Object.assign(new Error('Amount must be greater than zero'), { statusCode: 400 });
  }
  const movement = await CashMovement.create({
    shift: shift._id,
    type: data.type,
    amount,
    reason: data.reason,
    note: data.note || '',
    createdBy: { id: req.user._id, name: req.user.name },
  });

  logAudit(req, data.type === 'CASH_IN' ? 'CASH_IN' : 'CASH_OUT', 'shift', shift._id, {
    shiftNumber: shift.shiftNumber,
    amount,
    reason: data.reason,
  }, { description: `${data.type === 'CASH_IN' ? 'Cash in' : 'Cash out'} ${amount} on shift ${shift.shiftNumber} (${shift.register})` });

  const totals = await computeTotals(shift);
  return { movement, totals };
};

const listMovements = async (shiftId) => {
  return CashMovement.find({ shift: shiftId }).sort({ createdAt: -1 });
};

const closeShift = async (shiftId, data, req) => {
  const shift = await assertOpenShift(shiftId);

  // Only the shift owner or someone with shift.close.any may close
  const isOwner = shift.cashier.id && req.user._id.equals(shift.cashier.id);
  if (!isOwner && !['SUPER_ADMIN', 'ADMIN', 'MANAGER'].includes(req.user.role)) {
    throw Object.assign(new Error('Only the cashier or a manager can close this shift'), { statusCode: 403 });
  }

  const actualCash = Number(data.actualCash);
  if (actualCash == null || Number.isNaN(actualCash) || actualCash < 0) {
    throw Object.assign(new Error('Actual counted cash is required'), { statusCode: 400 });
  }

  const totals = await computeTotals(shift);
  shift.totals = totals;
  shift.closedAt = new Date();
  shift.status = 'CLOSED';
  shift.closing = {
    expectedCash: totals.expectedCash,
    actualCash: round2(actualCash),
    difference: round2(actualCash - totals.expectedCash),
    note: data.closingNote || '',
  };
  await shift.save();

  logAudit(req, 'SHIFT_CLOSED', 'shift', shift._id, {
    shiftNumber: shift.shiftNumber,
    expectedCash: shift.closing.expectedCash,
    actualCash: shift.closing.actualCash,
    difference: shift.closing.difference,
  }, { description: `Shift ${shift.shiftNumber} closed for register ${shift.register} (difference ${shift.closing.difference})` });
  return shift;
};

const getActiveShift = async (req) => {
  // A cashier sees their own open shift; managers/admins can pass ?register=
  const filter = { status: 'OPEN' };
  if (!['SUPER_ADMIN', 'ADMIN', 'MANAGER'].includes(req.user.role)) {
    filter['cashier.id'] = req.user._id;
  } else if (req.query.register) {
    filter.register = String(req.query.register).trim();
  }
  const shift = await Shift.findOne(filter).sort({ openedAt: -1 });
  if (!shift) return null;
  const totals = await computeTotals(shift);
  return { ...shift.toObject(), totals };
};

const listShifts = async (query = {}) => {
  const filter = {};
  if (query.status) filter.status = query.status;
  if (query.register) filter.register = query.register;
  const limit = Math.min(Number(query.limit) || 30, 100);
  const shifts = await Shift.find(filter).sort({ openedAt: -1 }).limit(limit);
  return shifts;
};

const getZReport = async (shiftId) => {
  const shift = await Shift.findById(shiftId);
  if (!shift) throw Object.assign(new Error('Shift not found'), { statusCode: 404 });
  const movements = await CashMovement.find({ shift: shift._id }).sort({ createdAt: 1 });
  const totals = shift.status === 'OPEN' ? await computeTotals(shift) : shift.totals;
  return { shift, movements, totals };
};

export default {
  openShift,
  addMovement,
  listMovements,
  closeShift,
  getActiveShift,
  listShifts,
  getZReport,
  computeTotals,
};
