import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import EmployeesList from "../Employees/Employee";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import UserList from "../Users/Users";
import { IUserDocument } from "../Users/Users.Adapter.Mongoose";
import { PaymentsAdapterMongoose } from "./Payments.Adapter.Mongoose";
import {
    CreatePaymentParams,
    DeletePaymentParams,
    DeletePaymentsByClientParams,
    DeletePaymentsByCompanyParams,
    FindPaymentsParams,
    GetAppointmentPaymentsParams,
    IPayment,
    IPaymentsManager,
    MarkAsPayedParams,
    PaginateNotPayedAppointmentsParams,
    PaginateNotPayedAppointmentsResult,
    NotPayedReportResult,
    PaginatePaymentsView,
    PaginatePaymentsViewParams,
    PaginatePaymentsViewResults,
    PaymentsReportResult,
    PAYMENT_METHODS_CLIENT_ACCOUNT,
    PAYMENT_PROCCESS_DATE,
} from "./Payments.Interface";
import AppointmentList from "../Appointments/Appointments";
import {
    CLIENT_ACCOUNT_MOVEMENT_STATUS,
    CLIENT_ACCOUNT_MOVEMENT_TYPES,
    CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "../ClientAccountMovements/ClientAccountMovements.Interface";
import ClientsList from "../Clients/Clients";
import ServicesList from "../Services/Service";
import ClientAccount from "../ClientAccounts/ClientAccount";
import ClientAccountMovements from "../ClientAccountMovements/ClientAccountMovements";
import CashFlowBalance from "../CashFlowBalance/CashFlowBalance";
import dayjs from "dayjs";
import { FilterQuery } from "mongoose";
import { IAppointment } from "../Appointments/Appointments.Interface";
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
import CompaniesList from "../Companies/Companies";
import { PlanFeatures } from "../Plans/Plans.interface";

class PaymentsManager implements IPaymentsManager {
    public async getPaymentsReport(
        filters: FindPaymentsParams
    ): Promise<PaymentsReportResult> {
        await this.validateSessionUser(((filters.sessionUser || "") || ""));
        await this.validatePermissions(((filters.sessionUser || "") || ""), String(filters.companyId));

        const results = await this.payments.find(filters);
        // Sort descending by createdAt since find() in adapter doesn't sort by default
        results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());

        const contentData: PaginatePaymentsView[] = [];
        let totalAmount = 0;

        for (const pago of results) {
            const appointment = await AppointmentList.Appointments.findOne({
                _id: pago.appointmentId,
            });

            if (!appointment) continue;

            const client = await ClientsList.clients.findOne({
                _id: appointment.clientId,
            });

            if (!client) continue;

            const employee = await EmployeesList.employees.findOne({
                _id: appointment.employeeId,
            });

            if (!employee) continue;

            const employeeUser = await UserList.users.findOne({
                _id: String(employee.userId),
            });

            if (!employeeUser) continue;

            const service = await ServicesList.services.findOne({
                _id: appointment.serviceId,
            });

            const clientName = await ClientsList.getClientFullName(client);
            const employeeName = await UserList.getUserFullName(employeeUser);

            totalAmount += pago.amount;

            contentData.push({
                id: pago.id || "",
                appointmentId: appointment.id,
                                appointmentDate: appointment.start as Date,
                clientId: String(client._id),
                clientName: clientName || "",
                employeeId: employee?.id || "",
                employeeName: employeeName || "",
                serviceId: service?.id || "",
                serviceName: service?.name || "",
                price: pago.amount,
                method: pago.method || "",
                status: pago.status as any,
                description: pago.description || "",
                createdAt: pago.createdAt as Date,
            });
        }

        return {
            items: contentData,
            totalAmount
        };
    }

    payments: PaymentsAdapterMongoose;

    constructor() {
        this.payments = new PaymentsAdapterMongoose();
    }

    public async deletePaymentsByCompany(data: DeletePaymentsByCompanyParams): Promise<void> {
        await this.payments.paymentsList.deleteMany({
            companyId: data.companyId,
        });
    }

    public async deletePaymentsByClient(data: DeletePaymentsByClientParams): Promise<void> {
        await this.payments.paymentsList.deleteMany({
            clientId: data.clientId,
        });
    }

    public async paginatedPaymentsViewTotal(data: FindPaymentsParams): Promise<number> {
        const pagos = await this.payments.find(data);

        const pagosPrice = pagos.map((pago) => pago.amount);
        return pagosPrice.reduce((previous, current) => current + previous, 0);
    }

    public async paginatePaymentsView(
        data: PaginatePaymentsViewParams
    ): Promise<PaginatePaymentsViewResults> {
        const userSession = await this.validateSessionUser((data.sessionUser || ""));
        await this.validatePermissions(String(userSession.id), String(data.companyId));

        const results: Array<PaginatePaymentsView> = [];

        const pagos = await this.payments.paginate(data);

        for (const pago of pagos.data) {
            const appointment = await AppointmentList.Appointments.findOne({
                _id: pago.appointmentId,
            });

            if (!appointment) {
                continue;
            }

            const client = await ClientsList.clients.findOne({
                _id: appointment.clientId,
            });

            if (!client) {
                continue;
            }

            const employee = await EmployeesList.employees.findOne({
                _id: appointment.employeeId,
            });

            if (!employee) {
                continue;
            }

            const employeeUser = await UserList.users.findOne({
                _id: String(employee.userId),
            });

            if (!employeeUser) {
                continue;
            }

            const service = await ServicesList.services.findOne({
                _id: appointment.serviceId,
            });

            if (!service) {
                continue;
            }

            if (!pago._id) {
                continue;
            }

            results.push({
                id: String(pago.id),
                appointmentId: String(pago.appointmentId),
                appointmentDate: appointment.dateDay,
                clientId: String(pago.clientId),
                clientName: ClientsList.getClientFullName(client),
                employeeId: String(pago.employeeId),
                employeeName: UserList.getUserFullName(employeeUser),
                serviceId: String(pago.serviceId),
                serviceName: service.name,
                price: Number(pago.amount),
                status: pago.status,
                method: pago.method || "",
                description: pago.description,
                createdAt: pago.createdAt,
            });
        }

        return {
            data: results,
            page: data.page,
            pages: pagos.pages,
        };
    }

    private async validateSessionUser(sessionUserId: string): Promise<IUserDocument> {
        const sessionUser = await UserList.users.findOne({
            _id: sessionUserId,
        });

        if (!sessionUser) {
            throw new Error(
                "No se ha encontrado el usuario o no tiene permisos para realizar esta acción"
            );
        }

        return sessionUser;
    }

    private async validatePermissions(sessionUserId: string, companyId: string): Promise<void> {
        // Verificar si el usuario de sesión es un empleado con permisos de administrador
        const hasAdminPermission = await EmployeesList.checkRoleById(
            companyId,
            sessionUserId,
            EmployeeRoles.ADMIN
        );

        if (!hasAdminPermission) {
            throw new Error(NoPermissionMessage());
        }

        const company = await CompaniesList.companies.findOne({
            _id: companyId,
        });

        if (!company) {
            throw new Error("No se ha encontrado la organización");
        }

        const canAccess = await PlanSubscriptionsList.checkFeature({
            userId: String(company.ownerId),
            feature: PlanFeatures.PAYMENTS,
        });

        if (!canAccess) {
            throw new Error("La organizacion no tiene un plan que soporte el módulo de pagos");
        }
    }

    private async getPayedAmount(appointmentId: string): Promise<number> {
        const payments = await this.payments.find({
            appointmentId: appointmentId,
        });

        const payedAmount = payments.reduce((acc, payment) => acc + payment.amount, 0);

        return payedAmount;
    }

    public async createPayment(data: CreatePaymentParams): Promise<IPayment> {
        const userSession = await this.validateSessionUser((data.sessionUser || ""));

        const appointment = await AppointmentList.Appointments.findOne({
            _id: data.appointmentId,
        });

        if (!appointment) {
            throw new Error("No se encontró la reserva");
        }

        if (appointment.payment) {
            throw new Error("La reserva ya fue pagada");
        }

        await this.validatePermissions(String(userSession.id), String(appointment.companyId));

        const toPayAmount = appointment.price;
        const payedAmount = await this.getPayedAmount(data.appointmentId);
        const remainingAmount = toPayAmount - payedAmount;

        if (remainingAmount <= 0) {
            throw new Error("La reserva ya está pagada");
        }

        if (data.amount > remainingAmount) {
            throw new Error("El monto del pago no puede ser mayor al monto restante");
        }

        if (data.amount <= 0) {
            throw new Error("El monto del pago no puede ser menor o igual a 0");
        }

        let availableClientAccountAmount = 0;

        if (data.method == PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) {
            availableClientAccountAmount = await ClientAccount.getAvailableAmount({
                clientId: String(appointment.clientId),
                sessionUser: (data.sessionUser || ""),
            });

            if (availableClientAccountAmount < data.amount) {
                throw new Error("El saldo de la cuenta del cliente no es suficiente");
            }
        }

        let proccessDate = new Date();

        if (data.proccessDate == PAYMENT_PROCCESS_DATE.NOW) {
            proccessDate = new Date();
        } else if (data.proccessDate == PAYMENT_PROCCESS_DATE.APPOINTMENT) {
            proccessDate = new Date(appointment.start);
        }

        const payment = await this.payments.create({
            ...data,
            clientId: String(appointment.clientId),
            companyId: String(appointment.companyId),
            employeeId: String(appointment.employeeId),
            serviceId: String(appointment.serviceId),
            ...{ createdAt: proccessDate },
        });

        const eventData = await AppointmentList.getAppointmentEvent({
            appointmentId: data.appointmentId,
            sessionUser: (data.sessionUser || ""),
        });

        const reservaDesc =
            eventData.title +
            " - " +
            eventData.serviceName +
            " (" +
            eventData.collaboratorName +
            ")";

        //si el pago es con cuenta de cliente, se debe descontar el dinero de la cuenta del cliente
        if (data.method == PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) {
            await ClientAccountMovements.add({
                clientId: String(appointment.clientId),
                amount: data.amount,
                description: reservaDesc,
                sessionUser: (data.sessionUser || ""),
                type: CLIENT_ACCOUNT_MOVEMENT_TYPES.WITHDRAWAL,
                status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED,
                paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH,
            });
        }

        if (remainingAmount === data.amount) {
            await AppointmentList.setPaymentStatus({
                appointmentId: data.appointmentId,
                status: true,
                sessionUser: (data.sessionUser || ""),
            });
        }

        //Creamos el movimiento de caja si es un pago que no viene de la cuenta corriente del cliente.
        if (data.method != PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) {
            const paymentDate = dayjs(payment.createdAt);
            const paymentYear = paymentDate.year();
            const paymentMonth = paymentDate.month();
            await CashFlowBalance.createCashFlowMovement({
                amount: data.amount,
                description: reservaDesc,
                year: paymentYear,
                month: paymentMonth,
                companyId: String(appointment.companyId),
                paymentId: String(payment._id),
                employeeId: String(appointment.employeeId),
                clientId: String(appointment.clientId),
                appointmentId: String(appointment._id),
                paymentMethod: data.method,
                reference: "",
                type: CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT,
                proccessDate: data.proccessDate,
                sessionUser: (data.sessionUser || ""),
            });
        }

        return payment;
    }

    public async markAsPayed(data: MarkAsPayedParams): Promise<void> {
        const userSession = await this.validateSessionUser((data.sessionUser || ""));

        const appointment = await AppointmentList.Appointments.findOne({
            _id: data.appointmentId,
        });

        if (!appointment) {
            throw new Error("No se encontró la reserva");
        }

        await this.validatePermissions(String(userSession.id), String(appointment.companyId));

        const toPayAmount = appointment.price;
        const payedAmount = await this.getPayedAmount(data.appointmentId);
        const remainingAmount = toPayAmount - payedAmount;

        if (remainingAmount == 0) {
            throw new Error("La reserva ya está pagada");
        }

        await this.createPayment({
            appointmentId: data.appointmentId,
            amount: remainingAmount,
            status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED,
            description: "Pago total de la reserva",
            method: data.method,
            proccessDate: PAYMENT_PROCCESS_DATE.NOW,
            sessionUser: (data.sessionUser || ""),
        });
    }

    public async deletePayment(data: DeletePaymentParams): Promise<void> {
        await this.validateSessionUser((data.sessionUser || ""));

        // Primero buscamos el movimiento por su ID
        const payment = await this.payments.paymentsList.findOne({
            _id: String(data.id),
        });

        if (!payment) {
            throw new Error("No se encontró el pago");
        }

        await this.validatePermissions((data.sessionUser || ""), payment.companyId);

        const appointment = await AppointmentList.Appointments.findOne({
            _id: payment.appointmentId,
        });

        if (!appointment) {
            throw new Error("No se encontró la reserva");
        }

        const toPayAmount = appointment.price;
        const payedAmount = await this.getPayedAmount(payment.appointmentId);
        const remainingAmount = toPayAmount - (payedAmount - payment.amount);

        //si el pago es con cuenta de cliente, se debe agregar el dinero a la cuenta del cliente
        if (payment.method == PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) {
            await ClientAccountMovements.add({
                clientId: String(appointment.clientId),
                amount: payment.amount,
                description: "Reembolso de la reserva - " + appointment.id,
                sessionUser: (data.sessionUser || ""),
                type: CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT,
                status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED,
                paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH,
            });
        }

        //Si el pago que se borra no viene de la cuenta del cliente, borramos el movimiento de la caja.
        if (payment.method != PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT) {
            await CashFlowBalance.deleteByPayment({
                companyId: String(appointment.companyId),
                paymentId: String(payment._id),
                sessionUser: (data.sessionUser || ""),
            });
        }

        await this.payments.delete(data.id);

        if (remainingAmount > 0) {
            await AppointmentList.setPaymentStatus({
                appointmentId: String(appointment._id),
                status: false,
                sessionUser: (data.sessionUser || ""),
            });
        }
    }

    public async getAppointmentPayments(data: GetAppointmentPaymentsParams): Promise<IPayment[]> {
        await this.validateSessionUser((data.sessionUser || ""));

        const appointment = await AppointmentList.Appointments.findOne({
            _id: data.appointmentId,
        });

        if (!appointment) {
            throw new Error("No se encontró la reserva");
        }

        await this.validatePermissions((data.sessionUser || ""), String(appointment.companyId));

        return await this.payments.find({
            appointmentId: data.appointmentId,
        });
    }

    private getNotPayedSearchCriteria(filters: FindPaymentsParams): FilterQuery<IAppointment> {
        const searchCriteria: FilterQuery<IAppointment> = {};

        if (filters.startDate) {
            searchCriteria.start = {
                $gte: filters.startDate,
                $lte: filters.endDate,
            };
        }

        searchCriteria.payment = false;

        if (filters.clientIds && filters.clientIds.length > 0) {
            searchCriteria.clientId = { $in: filters.clientIds };
        } else if (filters.clientId) {
            searchCriteria.clientId = filters.clientId;
        }

        if (filters.method) {
            searchCriteria.method = filters.method;
        }

        if (filters.companyId) {
            searchCriteria.companyId = filters.companyId;
        }

        if (filters.employeeId) {
            searchCriteria.employeeId = filters.employeeId;
        }

        if (filters.serviceId) {
            searchCriteria.serviceId = filters.serviceId;
        }

        return searchCriteria;
    }

    public async paginateNotPayedAppointments(
        filters: PaginateNotPayedAppointmentsParams
    ): Promise<PaginateNotPayedAppointmentsResult> {
        await this.validateSessionUser(((filters.sessionUser || "") || ""));
        await this.validatePermissions(((filters.sessionUser || "") || ""), String(filters.companyId));

        const searchCriteria = this.getNotPayedSearchCriteria(filters);

        const count = await AppointmentList.Appointments.AppointmentList.countDocuments(
            searchCriteria
        ).exec();
        const skip = (filters.page - 1) * filters.limit;

        const results = await AppointmentList.Appointments.AppointmentList.find(searchCriteria)
            .skip(skip)
            .limit(filters.limit)
            .sort({ start: -1 })
            .exec();

        const contentData: PaginatePaymentsView[] = [];

        for (const item of results) {
            const client = await ClientsList.clients.findOne({
                _id: item.clientId,
            });

            const employee = await EmployeesList.employees.findOne({
                _id: item.employeeId,
            });

            const service = await ServicesList.services.findOne({
                _id: item.serviceId,
            });

            if (!client) {
                continue;
            }

            if (!employee) {
                continue;
            }

            if (!service) {
                continue;
            }

            const employeeUser = await UserList.users.findOne({
                _id: String(employee.userId),
            });

            if (!employeeUser) {
                continue;
            }

            const clientName = await ClientsList.getClientFullName(client);
            const employeeName = await UserList.getUserFullName(employeeUser);

            contentData.push({
                id: item.id,
                appointmentId: item.id,
                appointmentDate: item.start,
                clientId: item.clientId,
                clientName: clientName || "",
                employeeId: item.employeeId,
                employeeName: employeeName || "",
                serviceId: item.serviceId,
                serviceName: service.name,
                price: item.price,
                status: CLIENT_ACCOUNT_MOVEMENT_STATUS.PENDING,
                method: CLIENT_ACCOUNT_PAYMENT_METHODS.OTHER,
                description: "",
                createdAt: item.start,
            });
        }

        let pages = Math.ceil(count / filters.limit);

        if (pages < 1) {
            pages = 1;
        }

        return {
            data: contentData,
            page: filters.page,
            pages: pages,
        };
    }

    public async paginatedNotPayedAppiontmentsTotal(data: FindPaymentsParams): Promise<number> {
        await this.validateSessionUser((data.sessionUser || ""));
        await this.validatePermissions((data.sessionUser || ""), String(data.companyId));
        const searchCriteria = this.getNotPayedSearchCriteria(data);
        const pagos = await AppointmentList.Appointments.AppointmentList.find(searchCriteria);
        return pagos.reduce((previous, current) => current.price + previous, 0);
    }

    
    public async getNotPayedReport(
        filters: FindPaymentsParams
    ): Promise<NotPayedReportResult> {
        await this.validateSessionUser(((filters.sessionUser || "") || ""));
        await this.validatePermissions(((filters.sessionUser || "") || ""), String(filters.companyId));

        const searchCriteria = this.getNotPayedSearchCriteria(filters);

        const results = await AppointmentList.Appointments.AppointmentList.find(searchCriteria)
            .sort({ start: -1 })
            .exec();

        const contentData: PaginatePaymentsView[] = [];
        let totalAmount = 0;

        for (const item of results) {
            const client = await ClientsList.clients.findOne({
                _id: item.clientId,
            });

            const employee = await EmployeesList.employees.findOne({
                _id: item.employeeId,
            });

            const service = await ServicesList.services.findOne({
                _id: item.serviceId,
            });

            if (!client || !employee || !service) {
                continue;
            }

            const employeeUser = await UserList.users.findOne({
                _id: String(employee.userId),
            });

            if (!employeeUser) {
                continue;
            }

            const clientName = await ClientsList.getClientFullName(client);
            const employeeName = await UserList.getUserFullName(employeeUser);

            contentData.push({
                id: item.id,
                appointmentId: item.id,
                appointmentDate: item.start,
                clientId: item.clientId,
                clientName: clientName || "",
                employeeId: item.employeeId,
                employeeName: employeeName || "",
                serviceId: item.serviceId,
                serviceName: service.name,
                price: item.price,
                status: CLIENT_ACCOUNT_MOVEMENT_STATUS.PENDING,
                method: CLIENT_ACCOUNT_PAYMENT_METHODS.OTHER,
                description: "",
                createdAt: item.start,
            });
            
            totalAmount += item.price;
        }

        return {
            items: contentData,
            totalAmount: totalAmount,
        };
    }

    public async payNotPayedAppointments(data: FindPaymentsParams): Promise<void> {
        await this.validateSessionUser((data.sessionUser || ""));
        await this.validatePermissions((data.sessionUser || ""), String(data.companyId));
        const { method, ...searchCriteria } = this.getNotPayedSearchCriteria(data); //Estraigo la propiedad method de searchCriteria
        const toPayAppointments = await AppointmentList.Appointments.AppointmentList.find(
            searchCriteria
        );

        if (!method) {
            throw new Error("No se ha especidicado un método de pago");
        }

        if (!data.proccessDate) {
            throw new Error("No se ha especidicado una fecha de pago");
        }

        for (const item of toPayAppointments) {
            try {
                if (dayjs(item.dateDay).isAfter(dayjs())) {
                    continue; // No se pueden pagar reservas futuras de forma masiva.
                }

                await this.createPayment({
                    appointmentId: item.id,
                    method: method,
                    status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED,
                    description: "Pago completo de la reserva",
                    amount: item.price,
                    proccessDate: data.proccessDate,
                    sessionUser: (data.sessionUser || ""),
                });
            } catch (e) {
                console.log(e);
            }
        }
    }
}

export default new PaymentsManager();
