import { ApiError } from '@/shared/utils/errors';
import { defaultStatus } from '@/shared/utils/responseCode/httpStatusAlias';
import { CustomerPayment } from '@/modules/customer/payment/payment.model';

import responseCodes from '@/shared/utils/responseCode/responseCode';
import { UpsertPaymentTimelineInput } from '@/modules/customer/payment/payment.interface';
import { Customer } from '@/modules/customer/customer.model';
import { Company } from '@/modules/company/company.model';
import { sendEmailWithActiveTemplate } from '@/modules/communication/email/email.helper';
import { TemplateName } from '@/shared/constants';
import { getObjectId } from '@/shared/utils/commonHelper';
import {
  calculateOverallStatus,
  calculateStageStatus,
  PaymentStatus,
} from './payment.helper';
import { sendWhatsappMessage } from '@/shared/communicationAlerts/sendMetaMsg';
import { TriggerPoint } from '@/shared/constants/enum.constant';

const { CustomerResponseCodes } = responseCodes;

export const sendPaymentDueReminder = async ({
  customerId,
  companyId,
  stageId,
}: {
  customerId: string;
  companyId: string;
  stageId: string;
}) => {
  const customer = await Customer.findOne({
    _id: getObjectId(customerId as any),
    company: getObjectId(companyId as any),
  })
    .select('name email company phone unitBookingOrHold')
    .populate({
      path: 'unitBookingOrHold',
      select: 'action bookingAmount createdAt',
      populate: [
        {
          path: 'project',
          select: 'projectName',
        },
        {
          path: 'unit',
          select: 'unitNumber',
        },
        {
          path: 'soldBy',
          select: 'firstName lastName phone',
        },
      ],
    })
    .lean();

  if (!customer?.email)
    throw new ApiError(
      defaultStatus.BAD_REQUEST,
      'Customer email not found',
      true,
      '',
      CustomerResponseCodes.COMPANY_ERROR,
    );

  const payment = await CustomerPayment.findOne({
    customerId: getObjectId(customerId as any),
    company: getObjectId(companyId as any),
  })
    .select('timeline')
    .lean();

  const stage = (payment as any)?.timeline?.find(
    (s: any) => String(s?._id) === String(stageId),
  );

  if (!stage)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment stage not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const company = await Company.findById(getObjectId(companyId as any))
    .select('name')
    .lean();

  const dueDate = stage?.dueDate
    ? new Date(stage.dueDate).toLocaleDateString('en-IN')
    : '—';

  const dueAmount =
    typeof stage?.dueAmount === 'number' ? String(stage.dueAmount) : '0';

  await sendEmailWithActiveTemplate({
    to: customer.email,
    companyId,
    scenario: TemplateName.PaymentDueReminder,
    templateParams: {
      customerName: customer.name || 'Customer',
      paymentStage: stage.label || 'Payment',
      dueDate,
      dueAmount,
      companyName: (company as any)?.name || 'Makanify',
    },
  });
  const soldBy = [
    (customer as any)?.unitBookingOrHold?.soldBy?.firstName,
    (customer as any)?.unitBookingOrHold?.soldBy?.lastName,
  ]
    .filter(Boolean)
    .join(' ');

  let whatsappVariables = [
    customer.name,
    dueAmount,
    (customer as any)?.unitBookingOrHold?.unit?.unitNumber || 'Unknown unit',
    (customer as any)?.unitBookingOrHold?.project?.projectName ||
      'Unknown project',
    soldBy,
    (customer as any).phone?.dialCode + (customer as any).phone?.number,
    (company as any)?.name,
  ];

  await sendWhatsappMessage({
    companyId: companyId,
    triggerPoint: TriggerPoint['WhenBuilderRequestPayment'],
    toNumber: String(customer.phone),
    variables: whatsappVariables,
    isNamedParams: false,
  });

  return { success: true };
};

export const upsertPaymentTimeline = async (
  input: UpsertPaymentTimelineInput,
) => {
  try {
    const { customerId, company, timeline, userId, totalAmount, paidAmount } =
      input;

    const query = { customerId, company };
    const existingPayment = await CustomerPayment.findOne(query);

    // Calculate totals
    // const totalAmount = timeline.reduce(
    //   (sum, stage) => sum + stage.dueAmount,
    //   0,
    // );

    // Process timeline stages
    const processedTimeline = timeline.map((stage) => {
      let paid = typeof paidAmount === 'number' ? paidAmount : 0;
      let lastPaidAt: Date | undefined = stage.lastPaidAt;
      let method = stage.method;
      let bankAccountId: any = (stage as any).bankAccountId;
      let payments: any[] | undefined = (stage as any).payments;

      if (existingPayment) {
        const existingStage = existingPayment.timeline.find(
          (ts) => ts.label === stage.label,
        );
        if (existingStage)
          if (typeof stage.paid !== 'number')
            // Preserve `paid` only when caller didn't send an explicit paid value.
            // We intentionally DO NOT carry forward historical payment entries when editing schedule,
            // to avoid double-counting old recorded payments after a schedule edit.
            paid = existingStage.paid;
      }

      // allow overwriting paid even if it's 0
      if (typeof stage.paid === 'number') paid = stage.paid;

      // Schedule editor does not send `payments[]` history. When a schedule is edited,
      // we intentionally clear historical payment entries so subsequent "Record Payment"
      // doesn't bring back old amounts.
      if (existingPayment && !Array.isArray((stage as any).payments))
        payments = [];

      return {
        label: stage.label,
        dueDate: stage.dueDate,
        dueAmount: stage.dueAmount,
        paid,
        ...(lastPaidAt ? { lastPaidAt } : {}),
        ...(bankAccountId
          ? { bankAccountId: getObjectId(bankAccountId as any) }
          : {}),
        ...(Array.isArray(payments) ? { payments } : {}),
        ...(method ? { method } : {}),
        status: (stage.status ||
          calculateStageStatus(
            paid,
            stage.dueAmount,
            stage.dueDate,
          )) as PaymentStatus,
      };
    });

    const paidAmountStage = processedTimeline.reduce(
      (sum, stage) => sum + stage.paid,
      0,
    );
    const overallStatus = calculateOverallStatus(processedTimeline);

    /* ------------------ VALIDATION START (only this block added) ------------------ */
    // Broker flow can model stages as "remaining balance" (e.g. next stage due = total - alreadyPaid).
    // In that model, sum(dueAmount) can exceed totalAmount. So validate against the max dueAmount.
    const maxDueFromTimeline = processedTimeline.reduce(
      (mx, s) => Math.max(mx, Number(s?.dueAmount || 0)),
      0,
    );

    // total to validate against: prefer input.totalAmount, else existing doc's totalAmount,
    // else derive from max dueAmount (works for both installment-style and remaining-balance style)
    const effectiveTotalAmount =
      (typeof totalAmount === 'number'
        ? totalAmount
        : existingPayment?.totalAmount) ?? maxDueFromTimeline;

    // 1) Largest stage dueAmount must not exceed totalAmount
    if (maxDueFromTimeline > effectiveTotalAmount)
      throw new ApiError(
        defaultStatus.BAD_REQUEST,
        'Stage dueAmount exceeds totalAmount',
        true,
        '',
        CustomerResponseCodes.COMPANY_ERROR,
      );

    // 2) paid amounts must not exceed due amounts
    for (const st of processedTimeline)
      if ((st.paid || 0) > (st.dueAmount || 0))
        throw new ApiError(
          defaultStatus.BAD_REQUEST,
          `Paid amount cannot exceed due amount for stage: ${st.label}`,
          true,
          '',
          CustomerResponseCodes.COMPANY_ERROR,
        );

    // 3) total paid across all stages must not exceed totalAmount
    if (paidAmountStage > effectiveTotalAmount)
      throw new ApiError(
        defaultStatus.BAD_REQUEST,
        'Total paid amount cannot exceed totalAmount',
        true,
        '',
        CustomerResponseCodes.COMPANY_ERROR,
      );
    /* ------------------ VALIDATION END ------------------ */

    let isNew = false;

    if (existingPayment) {
      (existingPayment as UpsertPaymentTimelineInput).timeline =
        processedTimeline;
      existingPayment.paidAmount = paidAmount || paidAmountStage;
      existingPayment.overallStatus = overallStatus as PaymentStatus;
      existingPayment.updatedBy = userId;

      // allow updating totalAmount for cases where it was not set earlier (broker flow)
      if (
        typeof existingPayment.totalAmount !== 'number' ||
        existingPayment.totalAmount <= 0
      )
        existingPayment.totalAmount = effectiveTotalAmount;

      await existingPayment.save();
    } else {
      const newPayment = new CustomerPayment({
        company,
        customerId,

        totalAmount: effectiveTotalAmount,
        paidAmount: paidAmount || paidAmountStage,
        overallStatus,
        timeline: processedTimeline,
        createdBy: userId,
        updatedBy: userId,
      });

      await newPayment.save();
      isNew = true;
    }

    return {
      success: true,
      isNew,
      message: isNew
        ? 'Payment timeline created successfully'
        : 'Payment timeline updated successfully',
    };
  } catch (error) {
    console.error('Error in upsertPaymentTimeline:', error);
    if (error instanceof ApiError) throw error;
    throw new ApiError(
      defaultStatus.INTERNAL_SERVER_ERROR,
      'Failed to upsert payment timeline',
      true,
      '',
      CustomerResponseCodes.COMPANY_ERROR,
    );
  }
};

export const addPaymentEntryToStage = async ({
  customerId,
  companyId,
  stageId,
  amount,
  receivedAt,
  bankAccountId,
  method,
  note,
  userId,
}: {
  customerId: string;
  companyId: string;
  stageId: string;
  amount: number;
  receivedAt?: Date;
  bankAccountId?: string;
  method?: string;
  note?: string;
  userId?: any;
}) => {
  const paymentDoc = await CustomerPayment.findOne({
    customerId: getObjectId(customerId as any),
    company: getObjectId(companyId as any),
  }).populate([
    {
      path: 'company',
      select: 'name',
    },
    {
      path: 'customerId',
      select: 'name email phone unitBookingOrHold',
      populate: {
        path: 'unitBookingOrHold',
        select: 'action bookingAmount createdAt',
        populate: [
          {
            path: 'project',
            select: 'projectName',
          },
          {
            path: 'unit',
            select: 'unitNumber',
          },
          {
            path: 'soldBy',
            select: 'firstName lastName phone',
          },
        ],
      },
    },
  ]);

  if (!paymentDoc)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment timeline not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const stage: any = paymentDoc.timeline.id(stageId);
  if (!stage)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment stage not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const entry = {
    amount,
    receivedAt: receivedAt || new Date(),
    ...(bankAccountId
      ? { bankAccountId: getObjectId(bankAccountId as any) }
      : {}),
    method,
    note,
  };

  stage.payments = Array.isArray(stage.payments) ? stage.payments : [];
  stage.payments.push(entry);


  if (stage.dueAmount !== undefined)
    stage.dueAmount = Math.max(stage.dueAmount - amount, 0);

  const stagePaid = (stage.payments || []).reduce(
    (sum: number, p: any) => sum + Number(p?.amount || 0),
    0,
  );
  stage.paid = stagePaid;

  const last = (stage.payments || []).reduce((latest: any, p: any) => {
    const d = p?.receivedAt ? new Date(p.receivedAt).getTime() : 0;
    const l = latest?.receivedAt ? new Date(latest.receivedAt).getTime() : 0;
    return d >= l ? p : latest;
  }, null);

  if (last?.receivedAt) stage.lastPaidAt = last.receivedAt;
  if (last?.method) stage.method = last.method;

  stage.status = calculateStageStatus(
    stage.paid,
    stage.dueAmount,
    stage.dueDate,
  ) as PaymentStatus;

  paymentDoc.paidAmount = ((paymentDoc.timeline || []) as any[]).reduce(
    (sum: number, s: any) => sum + Number(s?.paid || 0),
    0,
  );
  paymentDoc.overallStatus = calculateOverallStatus(paymentDoc.timeline) as any;
  if (userId) paymentDoc.updatedBy = userId;

  await paymentDoc.save();

  const customer = paymentDoc?.customerId as any;
  const booking = customer?.unitBookingOrHold as any;
  const company = paymentDoc?.company as any;

  const whatsappVariables = [
    customer?.name ?? 'Customer',
    amount,
    booking?.unit?.unitNumber ?? 'Unknown unit',
    booking?.project?.projectName ?? 'Unknown project',
    company?.name ?? 'Company',
  ];

  await sendWhatsappMessage({
    companyId,
    triggerPoint: TriggerPoint.WhenPaymentReceived,
    toNumber: String(customer?.phone ?? ''),
    variables: whatsappVariables,
    isNamedParams: false,
  });

  return {
    success: true,
    message: 'Payment entry added successfully',
  };
};

const recalculateStageFromPaymentEntries = (stage: any) => {
  stage.payments = Array.isArray(stage.payments) ? stage.payments : [];
  const stagePaid = stage.payments.reduce(
    (sum: number, p: any) => sum + Number(p?.amount || 0),
    0,
  );
  stage.paid = stagePaid;

  const last = (stage.payments || []).reduce((latest: any, p: any) => {
    const d = p?.receivedAt ? new Date(p.receivedAt).getTime() : 0;
    const l = latest?.receivedAt ? new Date(latest.receivedAt).getTime() : 0;
    return d >= l ? p : latest;
  }, null);

  if (last?.receivedAt) stage.lastPaidAt = last.receivedAt;
  else stage.lastPaidAt = undefined;

  if (last?.method) stage.method = last.method;

  const dueDate = stage.dueDate ? new Date(stage.dueDate) : new Date();
  stage.status = calculateStageStatus(
    stage.paid,
    Number(stage.dueAmount ?? 0),
    dueDate,
  ) as PaymentStatus;
};

const recalculatePaymentDocumentTotals = (paymentDoc: any, userId?: any) => {
  paymentDoc.paidAmount = ((paymentDoc.timeline || []) as any[]).reduce(
    (sum: number, s: any) => sum + Number(s?.paid || 0),
    0,
  );
  paymentDoc.overallStatus = calculateOverallStatus(paymentDoc.timeline) as any;
  if (userId) paymentDoc.updatedBy = userId;
};

export const updatePaymentEntry = async ({
  customerId,
  companyId,
  stageId,
  entryId,
  updates,
  userId,
}: {
  customerId: string;
  companyId: string;
  stageId: string;
  entryId: string;
  updates: {
    amount?: number;
    receivedAt?: Date;
    bankAccountId?: string | null;
    method?: string;
    note?: string;
  };
  userId?: any;
}) => {
  const paymentDoc = await CustomerPayment.findOne({
    customerId: getObjectId(customerId as any),
    company: getObjectId(companyId as any),
  });

  if (!paymentDoc)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment timeline not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const stage: any = paymentDoc.timeline.id(stageId);
  if (!stage)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment stage not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const entry: any = stage.payments?.id?.(entryId);
  if (!entry)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment entry not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const oldAmount = Number(entry.amount || 0);
  const currentDue = Number(stage.dueAmount ?? 0);
  const currentPaid = Number(stage.paid ?? 0);
  /** Original installment for this stage (remaining + recorded) */
  const installmentCeiling = currentDue + currentPaid;

  if (updates.amount !== undefined) {
    const newAmount = Number(updates.amount);
    if (!Number.isFinite(newAmount) || newAmount < 0)
      throw new ApiError(
        defaultStatus.BAD_REQUEST,
        'Invalid amount',
        true,
        '',
        CustomerResponseCodes.COMPANY_ERROR,
      );

    const newTotalPaidForStage = currentPaid - oldAmount + newAmount;
    if (newTotalPaidForStage > installmentCeiling)
      throw new ApiError(
        defaultStatus.BAD_REQUEST,
        'Updated amounts would exceed the amount due for this stage',
        true,
        '',
        CustomerResponseCodes.COMPANY_ERROR,
      );

    if (stage.dueAmount !== undefined)
      stage.dueAmount = Math.max(currentDue + oldAmount - newAmount, 0);
    entry.amount = newAmount;
  }

  if (updates.receivedAt !== undefined)
    entry.receivedAt = updates.receivedAt;

  if (updates.bankAccountId !== undefined) {
    if (updates.bankAccountId === null || updates.bankAccountId === '')
      entry.bankAccountId = undefined;
    else entry.bankAccountId = getObjectId(updates.bankAccountId as any);
  }

  if (updates.method !== undefined) entry.method = updates.method;

  if (updates.note !== undefined) entry.note = updates.note;

  recalculateStageFromPaymentEntries(stage);
  recalculatePaymentDocumentTotals(paymentDoc, userId);

  await paymentDoc.save();

  return {
    success: true,
    message: 'Payment entry updated successfully',
  };
};

export const deletePaymentEntry = async ({
  customerId,
  companyId,
  stageId,
  entryId,
  userId,
}: {
  customerId: string;
  companyId: string;
  stageId: string;
  entryId: string;
  userId?: any;
}) => {
  const paymentDoc = await CustomerPayment.findOne({
    customerId: getObjectId(customerId as any),
    company: getObjectId(companyId as any),
  });

  if (!paymentDoc)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment timeline not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const stage: any = paymentDoc.timeline.id(stageId);
  if (!stage)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment stage not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const entry: any = stage.payments?.id?.(entryId);
  if (!entry)
    throw new ApiError(
      defaultStatus.NOT_FOUND,
      'Payment entry not found',
      true,
      '',
      CustomerResponseCodes.CUSTOMER_NOT_FOUND,
    );

  const removedAmount = Number(entry.amount || 0);
  const currentDue = Number(stage.dueAmount ?? 0);

  if (stage.dueAmount !== undefined)
    stage.dueAmount = currentDue + removedAmount;

  entry.deleteOne();

  recalculateStageFromPaymentEntries(stage);
  recalculatePaymentDocumentTotals(paymentDoc, userId);

  await paymentDoc.save();

  return {
    success: true,
    message: 'Payment entry deleted successfully',
  };
};

export const getPaymentTimelines = async (
  input: UpsertPaymentTimelineInput,
) => {
  try {
    const { customerId, company } = input;
    const payment = await CustomerPayment.findOne({ customerId, company });
    if (!payment)
      return {
        totalAmount: 0,
        paidAmount: 0,
        overallStatus: PaymentStatus.PENDING,
        timeline: [],
      };

    const { totalAmount, paidAmount, overallStatus, timeline } = payment || {};

    const safeTimeline = (timeline || []).map((stage: any) => {
      const payments = Array.isArray(stage?.payments) ? stage.payments : [];
      if (payments.length === 0 && Number(stage?.paid || 0) > 0)
        return {
          ...(stage.toObject?.() ?? stage),
          payments: [
            {
              amount: Number(stage.paid || 0),
              receivedAt: stage.lastPaidAt,
              method: stage.method,
              bankAccountId: stage.bankAccountId,
            },
          ],
        };

      return stage;
    });

    return {
      totalAmount,
      paidAmount,
      overallStatus,
      timeline: safeTimeline,
    };
  } catch (error) {
    if (error instanceof ApiError) throw error;
    throw new ApiError(
      defaultStatus.INTERNAL_SERVER_ERROR,
      'Failed to get payment timelines',
      true,
      '',
      CustomerResponseCodes.COMPANY_ERROR,
    );
  }
};
