import { Request, Response } from 'express';
import Appointments from '../schema/appointments.schema';
import { AppointmentStatus } from '../enums/appointments.enum';
import { isPublishedBookingService } from '../../../helpers/booking-services.helper';
import { BlockedDay, FulfilledDay, WeeklySchedule } from '../../availability/schema/availability.schema';
import { generateSlots } from '../../../helpers/availability.helper';
import { logActivity } from '../../../helpers/activityLog.helper';
import { buildTextSearch, parsePagination, paginationMeta } from '../../../helpers/query.helper';
import PaymentLink from '../../payment-links/schema/payment-link.schema';
import { autoVerifyAppointments, autoVerifySingleAppointment } from '../../payment-links/services/payment-settlement.service';
import {
  applyProductLineItems,
  buildServiceSummary,
  computeAmountDue,
  normalizeLineItems,
  NormalizedLineItem,
} from '../../../helpers/appointment-items.helper';
import { upsertCustomer, resolveAppointmentCustomer } from '../../../helpers/customer.helper';
import { sendBookingNotifications } from '../../../helpers/booking-notifications.helper';

function resolvePaymentStatus(amountPaid: number, amountDue: number, amountRefunded: number) {
  if (amountRefunded > 0 && amountPaid <= 0) return 'REFUNDED';
  if (amountRefunded > 0 && amountPaid > 0) return 'PARTIALLY_REFUNDED';
  if (amountPaid >= amountDue && amountDue > 0) return 'PAID';
  if (amountPaid > 0 && amountDue > 0) return 'PARTIAL';
  if (amountPaid > 0 && amountDue === 0) return 'PAID';
  return 'UNPAID';
}

async function validateSlot(date: string, time: string, excludeAppointmentId?: string) {
  const blocked = await BlockedDay.findOne({ date });
  if (blocked) return { valid: false, message: 'This date is not available' };

  const fulfilled = await FulfilledDay.findOne({ date });
  if (fulfilled) return { valid: false, message: 'This date is fully booked' };

  const day = new Date(`${date}T12:00:00`).getDay();
  const schedule = await WeeklySchedule.findOne({ dayOfWeek: day });
  if (!schedule?.isOpen) return { valid: false, message: 'We are closed on this day' };

  const slots = generateSlots(schedule.openTime, schedule.closeTime, schedule.slotIntervalMinutes || 60);
  if (!slots.includes(time)) return { valid: false, message: 'Invalid time slot' };

  const existingQuery: Record<string, unknown> = {
    date,
    time,
    status: { $nin: [AppointmentStatus.CANCELLED] },
  };
  if (excludeAppointmentId) {
    existingQuery._id = { $ne: excludeAppointmentId };
  }

  const existing = await Appointments.findOne(existingQuery);
  if (existing) return { valid: false, message: 'This time slot is already booked' };

  return { valid: true };
}

function resolveLineItemsPayload(body: Record<string, unknown>): NormalizedLineItem[] {
  const rawItems = Array.isArray(body.lineItems) ? body.lineItems : [];
  const normalized = normalizeLineItems(rawItems as never[]);
  if (normalized.length) return normalized;

  if (body.service) {
    return normalizeLineItems([
      {
        type: 'SERVICE',
        name: String(body.service),
        quantity: 1,
        unitPrice: Number(body.amountDue) || 0,
      },
    ]);
  }

  return [];
}

function nextDocNumber(prefix: string) {
  return `${prefix}-${Date.now().toString().slice(-8)}`;
}

export class AppointmentsController {
  static async create(req: Request, res: Response) {
    try {
      const { fullName, email, phone, service, date, time, notes } = req.body;

      if (!fullName || !phone || !service || !date || !time) {
        return res.status(400).json({
          success: false,
          message: 'Please provide name, phone, service, date and time',
        });
      }

      const normalizedEmail = String(email || '').trim().toLowerCase();

      const serviceIsValid = await isPublishedBookingService(String(service));
      if (!serviceIsValid) {
        return res.status(400).json({
          success: false,
          message: 'Invalid service selected',
        });
      }

      const slotCheck = await validateSlot(date, time);
      if (!slotCheck.valid) {
        return res.status(409).json({ success: false, message: slotCheck.message });
      }

      const appointment = await Appointments.create({
        fullName,
        email: normalizedEmail,
        phone,
        service,
        date,
        time,
        notes,
        lineItems: normalizeLineItems([
          { type: 'SERVICE', name: service, quantity: 1, unitPrice: 0 },
        ]),
        source: 'WEB',
        createdBy: req['staffUser']?._id?.toString() || null,
      });

      try {
        const customer = await upsertCustomer({ fullName, email: normalizedEmail, phone });
        appointment.customerId = customer._id.toString();
        await appointment.save();
      } catch {
        // Customer sync is best-effort for public bookings.
      }

      try {
        await sendBookingNotifications(appointment);
      } catch (error) {
        console.error('Booking notification error:', error);
      }

      return res.status(201).json({
        success: true,
        message: 'Appointment booked successfully',
        response: appointment,
      });
    } catch {
      return res.status(500).json({
        success: false,
        message: 'System error',
      });
    }
  }

  static async list(req: Request, res: Response) {
    try {
      const { page, limit, skip } = parsePagination(req, 10);
      const filter: Record<string, unknown> = {};

      const status = String(req.query.status || '').trim();
      const paymentStatus = String(req.query.paymentStatus || '').trim();
      const customerId = String(req.query.customerId || '').trim();
      if (status) filter.status = status;
      if (paymentStatus) filter.paymentStatus = paymentStatus;
      if (customerId) filter.customerId = customerId;

      const search = buildTextSearch(
        ['fullName', 'email', 'phone', 'service'],
        String(req.query.q || '')
      );
      if (search.$or) Object.assign(filter, search);

      const [appointments, total, summaryAgg] = await Promise.all([
        Appointments.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(),
        Appointments.countDocuments(filter),
        Appointments.aggregate([
          { $match: filter },
          {
            $group: {
              _id: null,
              collected: { $sum: '$amountPaid' },
              refunded: { $sum: '$amountRefunded' },
              due: { $sum: '$amountDue' },
              paidCount: {
                $sum: { $cond: [{ $eq: ['$paymentStatus', 'PAID'] }, 1, 0] },
              },
            },
          },
        ]),
      ]);

      let summaryRow = summaryAgg[0] || { collected: 0, refunded: 0, due: 0, paidCount: 0 };

      const linkIds = appointments
        .map((item) => item.paymentLinkId)
        .filter(Boolean) as string[];
      const paymentLinks = linkIds.length
        ? await PaymentLink.find({ _id: { $in: linkIds } }).lean()
        : [];
      const linkById = new Map(paymentLinks.map((link) => [String(link._id), link]));
      let response = appointments.map((item) => ({
        ...item,
        paymentLink: item.paymentLinkId ? linkById.get(String(item.paymentLinkId)) || null : null,
      }));

      const paymentUpdates = await autoVerifyAppointments(response);
      if (paymentUpdates.size > 0) {
        const updatedLinkIds = new Set<string>();
        response = response.map((item) => {
          const updated = paymentUpdates.get(String(item._id));
          if (!updated) return item;
          const plain = typeof updated.toObject === 'function' ? updated.toObject() : updated;
          if (plain.paymentLinkId) updatedLinkIds.add(String(plain.paymentLinkId));
          return {
            ...item,
            ...plain,
            paymentLink: item.paymentLink,
          };
        });

        if (updatedLinkIds.size > 0) {
          const refreshedLinks = await PaymentLink.find({
            _id: { $in: Array.from(updatedLinkIds) },
          }).lean();
          const refreshedById = new Map(refreshedLinks.map((link) => [String(link._id), link]));
          response = response.map((item) => ({
            ...item,
            paymentLink: item.paymentLinkId
              ? refreshedById.get(String(item.paymentLinkId)) || item.paymentLink
              : null,
          }));
        }
      }

      if (paymentUpdates.size > 0) {
        const refreshedSummary = await Appointments.aggregate([
          { $match: filter },
          {
            $group: {
              _id: null,
              collected: { $sum: '$amountPaid' },
              refunded: { $sum: '$amountRefunded' },
              due: { $sum: '$amountDue' },
              paidCount: {
                $sum: { $cond: [{ $eq: ['$paymentStatus', 'PAID'] }, 1, 0] },
              },
            },
          },
        ]);
        summaryRow = refreshedSummary[0] || summaryRow;
      }

      return res.status(200).json({
        success: true,
        message: 'Appointments retrieved successfully',
        response,
        summary: {
          collected: summaryRow.collected || 0,
          refunded: summaryRow.refunded || 0,
          outstanding: Math.max((summaryRow.due || 0) - (summaryRow.collected || 0), 0),
          paidCount: summaryRow.paidCount || 0,
        },
        pagination: paginationMeta(page, limit, total),
      });
    } catch {
      return res.status(500).json({
        success: false,
        message: 'System error',
      });
    }
  }

  static async getOne(req: Request, res: Response) {
    try {
      await autoVerifySingleAppointment(req.params.id);

      const appointment = await Appointments.findById(req.params.id).lean();
      if (!appointment) {
        return res.status(404).json({ success: false, message: 'Appointment not found' });
      }

      const paymentLink = appointment.paymentLinkId
        ? await PaymentLink.findById(appointment.paymentLinkId).lean()
        : null;

      return res.status(200).json({
        success: true,
        response: { ...appointment, paymentLink },
      });
    } catch {
      return res.status(500).json({ success: false, message: 'System error' });
    }
  }

  static async createAdmin(req: Request, res: Response) {
    try {
      const {
        customerId,
        fullName,
        email,
        phone,
        date,
        time,
        notes,
        status,
        skipSlotCheck = false,
      } = req.body;

      if (!date || !time) {
        return res.status(400).json({
          success: false,
          message: 'Please provide date and time',
        });
      }

      if (!customerId && (!fullName || !phone)) {
        return res.status(400).json({
          success: false,
          message: 'Please provide customer name and phone',
        });
      }

      const lineItems = resolveLineItemsPayload(req.body);
      if (!lineItems.length) {
        return res.status(400).json({
          success: false,
          message: 'Add at least one service or product',
        });
      }

      if (!skipSlotCheck) {
        const slotCheck = await validateSlot(date, time);
        if (!slotCheck.valid) {
          return res.status(409).json({ success: false, message: slotCheck.message });
        }
      }

      const computedDue = computeAmountDue(lineItems);
      const amountDue =
        req.body.amountDue !== undefined && req.body.amountDue !== null
          ? Number(req.body.amountDue)
          : computedDue;

      let linkedCustomer;
      try {
        linkedCustomer = await resolveAppointmentCustomer({
          customerId,
          fullName,
          phone,
          email,
          notes,
        });
      } catch (error) {
        return res.status(400).json({
          success: false,
          message: error instanceof Error ? error.message : 'Invalid customer',
        });
      }

      const serviceSummary = buildServiceSummary(lineItems);

      const appointment = await Appointments.create({
        fullName: linkedCustomer.fullName,
        email: linkedCustomer.email || '',
        phone: linkedCustomer.phone,
        customerId: linkedCustomer._id.toString(),
        service: serviceSummary,
        date,
        time,
        notes,
        status: status && Object.values(AppointmentStatus).includes(status) ? status : AppointmentStatus.CONFIRMED,
        lineItems,
        amountDue,
        paymentStatus: resolvePaymentStatus(0, amountDue, 0),
        source: 'ADMIN',
        createdBy: req['staffUser']?._id?.toString() || null,
      });

      try {
        await applyProductLineItems(
          appointment._id.toString(),
          lineItems,
          [],
          req['staffUser']?._id?.toString() || null
        );
      } catch (error) {
        await Appointments.findByIdAndDelete(appointment._id);
        return res.status(400).json({
          success: false,
          message: error instanceof Error ? error.message : 'Could not apply product line items',
        });
      }

      await logActivity(req, 'CREATE', 'APPOINTMENT', appointment._id.toString(), serviceSummary);

      return res.status(201).json({
        success: true,
        message: 'Appointment created',
        response: appointment,
      });
    } catch {
      return res.status(500).json({ success: false, message: 'System error' });
    }
  }

  static async update(req: Request, res: Response) {
    try {
      const appointment = await Appointments.findById(req.params.id);
      if (!appointment) {
        return res.status(404).json({ success: false, message: 'Appointment not found' });
      }

      const {
        fullName,
        email,
        phone,
        date,
        time,
        notes,
        status,
        skipSlotCheck = false,
      } = req.body;

      const nextDate = date || appointment.date;
      const nextTime = time || appointment.time;

      if ((date && date !== appointment.date) || (time && time !== appointment.time)) {
        if (!skipSlotCheck) {
          const slotCheck = await validateSlot(nextDate, nextTime, appointment._id.toString());
          if (!slotCheck.valid) {
            return res.status(409).json({ success: false, message: slotCheck.message });
          }
        }
        appointment.date = nextDate;
        appointment.time = nextTime;
      }

      if (fullName !== undefined) appointment.fullName = String(fullName).trim();
      if (email !== undefined) appointment.email = String(email || '').trim().toLowerCase();
      if (phone !== undefined) appointment.phone = String(phone).trim();
      if (notes !== undefined) appointment.notes = notes;

      if (status && Object.values(AppointmentStatus).includes(status)) {
        appointment.status = status;
      }

      if (Array.isArray(req.body.lineItems)) {
        const previousItems = normalizeLineItems((appointment.lineItems || []) as never[]);
        const lineItems = normalizeLineItems(req.body.lineItems);
        if (!lineItems.length) {
          return res.status(400).json({ success: false, message: 'Add at least one service or product' });
        }

        try {
          await applyProductLineItems(
            appointment._id.toString(),
            lineItems,
            previousItems,
            req['staffUser']?._id?.toString() || null
          );
        } catch (error) {
          return res.status(400).json({
            success: false,
            message: error instanceof Error ? error.message : 'Could not update product line items',
          });
        }

        appointment.lineItems = lineItems as never;
        appointment.service = buildServiceSummary(lineItems, appointment.service);
        const computedDue = computeAmountDue(lineItems);
        appointment.amountDue =
          req.body.amountDue !== undefined && req.body.amountDue !== null
            ? Number(req.body.amountDue)
            : computedDue;
      } else if (req.body.amountDue !== undefined && req.body.amountDue !== null) {
        appointment.amountDue = Number(req.body.amountDue);
      }

      appointment.paymentStatus = resolvePaymentStatus(
        appointment.amountPaid || 0,
        appointment.amountDue || 0,
        appointment.amountRefunded || 0
      );

      if (req.body.customerId || (appointment.fullName && appointment.phone)) {
        try {
          const customer = await resolveAppointmentCustomer({
            customerId: req.body.customerId || appointment.customerId,
            fullName: appointment.fullName,
            email: appointment.email,
            phone: appointment.phone,
            notes,
          });
          appointment.customerId = customer._id.toString();
        } catch {
          // Keep appointment update successful even if customer sync fails.
        }
      }

      await appointment.save();
      await logActivity(req, 'UPDATE', 'APPOINTMENT', appointment._id.toString(), appointment.service);

      return res.status(200).json({
        success: true,
        message: 'Appointment updated',
        response: appointment,
      });
    } catch {
      return res.status(500).json({ success: false, message: 'System error' });
    }
  }

  static async setAmountDue(req: Request, res: Response) {
    try {
      const amountDue = Number(req.body.amountDue);
      if (!amountDue || amountDue <= 0) {
        return res.status(400).json({ success: false, message: 'Valid amount due required' });
      }

      const appointment = await Appointments.findById(req.params.id);
      if (!appointment) return res.status(404).json({ success: false, message: 'Not found' });

      appointment.amountDue = amountDue;
      appointment.paymentStatus = resolvePaymentStatus(
        appointment.amountPaid || 0,
        amountDue,
        appointment.amountRefunded || 0
      );
      await appointment.save();

      await logActivity(req, 'UPDATE', 'APPOINTMENT', appointment._id.toString(), `Due GHS ${amountDue}`);

      return res.status(200).json({ success: true, response: appointment });
    } catch {
      return res.status(500).json({ success: false, message: 'System error' });
    }
  }

  static async updateStatus(req: Request, res: Response) {
    try {
      const { id } = req.params;
      const { status } = req.body;

      if (!Object.values(AppointmentStatus).includes(status)) {
        return res.status(400).json({
          success: false,
          message: 'Invalid status',
        });
      }

      const existing = await Appointments.findById(id);
      if (!existing) {
        return res.status(404).json({
          success: false,
          message: 'Appointment not found',
        });
      }

      const previousStatus = existing.status;
      const appointment = await Appointments.findByIdAndUpdate(
        id,
        { status },
        { new: true }
      );

      return res.status(200).json({
        success: true,
        message: 'Appointment status updated',
        response: appointment,
        meta: {
          previousStatus,
          currentStatus: status,
        },
      });
    } catch {
      return res.status(500).json({
        success: false,
        message: 'System error',
      });
    }
  }

  static async recordPayment(req: Request, res: Response) {
    try {
      const { amount, method } = req.body;
      const appointment = await Appointments.findById(req.params.id);
      if (!appointment) return res.status(404).json({ success: false, message: 'Not found' });

      const paid = (appointment.amountPaid || 0) + Number(amount || 0);
      const due = appointment.amountDue || 0;

      appointment.amountPaid = paid;
      appointment.paymentMethod = method || 'CASH';
      appointment.paymentStatus = resolvePaymentStatus(
        paid,
        due,
        appointment.amountRefunded || 0
      );
      if (method === 'CASH') appointment.cashRecordedAt = new Date();
      await appointment.save();

      await logActivity(req, 'PAYMENT', 'APPOINTMENT', appointment._id.toString(), `GHS ${amount}`);

      return res.status(200).json({ success: true, response: appointment });
    } catch {
      return res.status(500).json({ success: false, message: 'System error' });
    }
  }

  static async recordRefund(req: Request, res: Response) {
    try {
      const { amount, reason } = req.body;
      const refundAmount = Number(amount);

      if (!refundAmount || refundAmount <= 0) {
        return res.status(400).json({ success: false, message: 'Valid refund amount required' });
      }

      const appointment = await Appointments.findById(req.params.id);
      if (!appointment) return res.status(404).json({ success: false, message: 'Not found' });

      const currentPaid = appointment.amountPaid || 0;
      if (refundAmount > currentPaid) {
        return res.status(400).json({
          success: false,
          message: `Refund cannot exceed amount paid (GHS ${currentPaid})`,
        });
      }

      const nextPaid = currentPaid - refundAmount;
      const nextRefunded = (appointment.amountRefunded || 0) + refundAmount;

      appointment.amountPaid = nextPaid;
      appointment.amountRefunded = nextRefunded;
      appointment.paymentStatus = resolvePaymentStatus(
        nextPaid,
        appointment.amountDue || 0,
        nextRefunded
      );

      appointment.refunds = appointment.refunds || [];
      appointment.refunds.push({
        amount: refundAmount,
        reason: reason || '',
        method: 'CASH',
        performedBy: req['staffUser']?._id?.toString() || null,
        refundNumber: nextDocNumber('RFD'),
        createdAt: new Date(),
      } as never);

      await appointment.save();

      await logActivity(
        req,
        'REFUND',
        'APPOINTMENT',
        appointment._id.toString(),
        `GHS ${refundAmount}${reason ? ` — ${reason}` : ''}`
      );

      return res.status(200).json({
        success: true,
        message: 'Refund recorded',
        response: appointment,
      });
    } catch {
      return res.status(500).json({ success: false, message: 'System error' });
    }
  }

  static async generateInvoice(req: Request, res: Response) {
    try {
      const appointment = await Appointments.findById(req.params.id);
      if (!appointment) return res.status(404).json({ success: false, message: 'Not found' });

      if (!appointment.invoiceNumber || appointment.invoiceArchivedAt) {
        appointment.invoiceNumber = nextDocNumber('INV');
        appointment.invoiceArchivedAt = null;
        await appointment.save();
      }

      return res.status(200).json({
        success: true,
        response: {
          appointment,
          documentType: 'invoice',
          documentNumber: appointment.invoiceNumber,
        },
      });
    } catch {
      return res.status(500).json({ success: false, message: 'System error' });
    }
  }

  static async generateReceipt(req: Request, res: Response) {
    try {
      const appointment = await Appointments.findById(req.params.id);
      if (!appointment) return res.status(404).json({ success: false, message: 'Not found' });

      if (!(appointment.amountPaid > 0)) {
        return res.status(400).json({
          success: false,
          message: 'Receipt can only be generated after a payment has been recorded',
        });
      }

      if (!appointment.receiptNumber || appointment.receiptArchivedAt) {
        appointment.receiptNumber = nextDocNumber('RCT');
        appointment.receiptArchivedAt = null;
        await appointment.save();
      }

      return res.status(200).json({
        success: true,
        response: {
          appointment,
          documentType: 'receipt',
          documentNumber: appointment.receiptNumber,
        },
      });
    } catch {
      return res.status(500).json({ success: false, message: 'System error' });
    }
  }
}
