import UsersManager from "../Users/Users";
import CompaniesManager from "../Companies/Companies";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import EmployeesList from "../Employees/Employee";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import { WapServerAdapterMongoose } from "./WapServer.Adapter.Mongoose";
import {
    AssingAvailableServerParams,
    BotCheckSession,
    BotCheckStatus,
    BotEventParams,
    BotEventError,
    BotView,
    IWapServer,
    IWapServerManager,
    SendBotMessageParams,
    ValidateBotEventStatus,
    BOT_STATE,
    SERVER_STATE,
    BOT_SESSION_STATUS,
    VALIDATE_BOT_ENVENT_ERRORS,
    PaginateWapServerResults,
    PaginateWapServerParams,
} from "./WapServer.Interface";
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
import { PlanFeatures } from "../Plans/Plans.interface";
import axios, { AxiosError } from "axios";
import { isNull } from "../../helpers/IsNull";

export interface CreateBotResult {
    organizationId: string;
    port: number;
    error: string;
    message: string;
}

class WapServerManager implements IWapServerManager {
    servers: WapServerAdapterMongoose;

    constructor() {
        this.servers = new WapServerAdapterMongoose();
    }

    public async botView(data: AssingAvailableServerParams): Promise<BotView> {
        const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });

        if (!sessionUser) {
            throw new Error("El usuario no es valido.");
        }

        const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });

        if (!companyCheck) {
            throw new Error("La compañia no existe");
        }

        if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
            throw new Error(NoPermissionMessage());
        }

        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        if (!validationStatus.status) {
            switch (validationStatus.code) {
                case VALIDATE_BOT_ENVENT_ERRORS.NO_SERVER_ASSIGNED:
                    return {
                        runningState: "dead",
                        serverId: undefined,
                        serverName: undefined,
                        serverDescription: undefined,
                        serverState: SERVER_STATE.NOT_ASSIGNED,
                        exists: false,
                        automaticNotifications: isNull<boolean>(companyCheck.automaticNotifications, false),
                    };
                default:
                    throw new Error(validationStatus.error);
            }
        }

        if (!companyCheck.wapServerId) {
            return {
                runningState: "dead",
                serverId: undefined,
                serverName: undefined,
                serverDescription: undefined,
                serverState: SERVER_STATE.NOT_WORKING,
                exists: false,
                automaticNotifications: isNull<boolean>(companyCheck.automaticNotifications, false),
            };
        }

        const server = await this.servers.serverList.findOne({ _id: companyCheck.wapServerId });

        if (server) {
            try {
                const chkStatus = await this.checkBot({
                    sessionUser: data.sessionUser,
                    companyId: data.companyId,
                });
                let botSession = BOT_SESSION_STATUS.UNLOGED;

                if (chkStatus.status.indexOf("Up") >= 0) {
                    botSession = (await this.checkSession(data)).session;
                }

                return {
                    serverId: server.id,
                    serverName: server.name,
                    serverDescription: server.description,
                    serverState: chkStatus.serverState,
                    exists: chkStatus.state == BOT_STATE.EXISTS ? true : false,
                    runningState: chkStatus.status,
                    botSession: botSession,
                    automaticNotifications: isNull<boolean>(companyCheck.automaticNotifications, false),
                } as BotView;
            } catch (error) {
                return {
                    serverId: server.id,
                    serverName: server.name,
                    serverDescription: server.description,
                    serverState: SERVER_STATE.NOT_WORKING,
                    exists: false,
                    runningState: "dead",
                    automaticNotifications: isNull<boolean>(companyCheck.automaticNotifications, false),
                };
            }
        }

        return {
            serverId: undefined,
            serverName: undefined,
            serverDescription: undefined,
            exists: false,
            runningState: "dead",
            automaticNotifications: isNull<boolean>(companyCheck.automaticNotifications, false),
        };
    }

    public async findServers(): Promise<IWapServer[]> {
        const servidores = await this.servers.find({});

        return servidores;
    }

    private getBotAdminApiUrl(url: string): string {
        //return `https://${url}:${process.env.DEFAULT_BOT_ADMIN_PORT}/`;
        //return `https://${url}/`;
        return url;
    }

    public async getAvailableServer(): Promise<IWapServer> {
        const servidores = await this.servers.find({ active: true });

        if (servidores.length == 0) {
            throw new Error("No se encontraron servidores disponibles");
        }

        const servidoresConCapacidad = servidores.filter((s) => {
            return s.countBots < s.maxBots;
        });

        if (servidoresConCapacidad.length == 0) {
            throw new Error("No se encontraron servidores disponibles");
        }

        const returnServer = servidoresConCapacidad.sort((a, b) => a.countBots - b.countBots).shift();

        if (!returnServer) {
            throw new Error("No se encontraron servidores disponibles");
        }

        return returnServer;
    }

    public async quitServer(data: AssingAvailableServerParams): Promise<void> {
        const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });

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

        //Validar que la organizacion exista.
        const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });

        if (!companyCheck) {
            throw new Error("La compañia no existe");
        }

        //Validar que el usuario sea el administrador de la organizacion.
        if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
            throw new Error(NoPermissionMessage());
        }

        //Descontar 1 al contador de bots del servidor.

        if (companyCheck && companyCheck.wapServerId) {
            const server = await this.servers.serverList.findOne({
                _id: companyCheck.wapServerId,
            });

            if (server) {
                server.countBots = server.countBots - 1;
                if (server.countBots < 0) {
                    server.countBots = 0;
                }
                await server.save();
            }
        }

        companyCheck.wapServerId = undefined;
        await companyCheck.save();
    }

    public async assignAvailableServer(data: AssingAvailableServerParams): Promise<IWapServer> {
        const sessionUser = await UsersManager.users.findOne({ _id: data.sessionUser });

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

        //Validar que la organizacion exista.
        const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });

        if (!companyCheck) {
            throw new Error("La compañia no existe");
        }

        //Validar que el usuario sea el administrador de la organizacion.
        if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
            throw new Error(NoPermissionMessage());
        }

        //Obtener el id del usuario owner de la organizacion.
        const organizationOwner = companyCheck.ownerId;
        //Validar que el owner de la organizacion tenga un plan que permita bots.
        if (
            !PlanSubscriptionsList.checkFeature({
                userId: organizationOwner,
                feature: PlanFeatures.HAS_BOT,
            })
        ) {
            throw new Error("El plan de la organizacion no permite bots.");
        }
        //Validar que el plan este activo.
        if (!PlanSubscriptionsList.isSubscriptionActive({ sessionUser: organizationOwner })) {
            throw new Error("La organizacion no tiene una suscripcion activa.");
        }
        //Validar que la organizacion no tenga ningun servidor asignado.
        let organizationServerId = companyCheck.wapServerId;

        if (organizationServerId) {
            throw new Error("La organizacion ya tiene un servidor asignado.");
        }
        //Obtener el servidor con menos bots activos.
        const availableServer = await this.getAvailableServer();
        if (!availableServer) {
            throw new Error("No se encontraron servidores disponibles.");
        }

        const server = await this.servers.serverList.findOne({ _id: availableServer.id });

        if (!server) {
            throw new Error("No se encontro el servidor");
        }

        //Guardar en la coleccion Companies los datos del servidor asignado.
        await CompaniesManager.updateCompany({
            wapServerId: availableServer.id,
            id: data.companyId,
            sessionUser: data.sessionUser,
        });

        //Incrementear la cantidad de bots en el servidor elegido.
        server.countBots = server.countBots + 1;
        await server.save();

        return server;
    }

    private async validateBotEvent(
        sessionUserId: string,
        companyId?: string,
    ): Promise<ValidateBotEventStatus> {
        if (!sessionUserId) {
            return {
                status: false,
                error: "No se ha encontrado el usuario",
                code: VALIDATE_BOT_ENVENT_ERRORS.INVALID_USER,
            };
        }

        const sessionUser = await UsersManager.users.findOne({ _id: sessionUserId });

        if (!sessionUser) {
            return {
                status: false,
                error: "No se ha encontrado el usuario o no tiene permisos para realizar esta acción",
                code: VALIDATE_BOT_ENVENT_ERRORS.INVALID_USER,
            };
        }

        if (!companyId) {
            return {
                status: false,
                error: "No se ha encontrado la compañia",
                code: VALIDATE_BOT_ENVENT_ERRORS.INVALID_ORG,
            };
        }

        //Validar que la organizacion exista.
        const companyCheck = await CompaniesManager.companies.findOne({ _id: companyId });

        if (!companyCheck) {
            return {
                status: false,
                error: "La compañia no existe",
                code: VALIDATE_BOT_ENVENT_ERRORS.INVALID_ORG,
            };
        }

        //Validar que el usuario sea el administrador de la organizacion.
        if (!(await EmployeesList.checkPermission(companyCheck, sessionUserId, EmployeeRoles.ADMIN))) {
            return {
                status: false,
                error: NoPermissionMessage(),
                code: VALIDATE_BOT_ENVENT_ERRORS.NO_PERMISSION,
            };
        }

        //Obtener el id del usuario owner de la organizacion.
        const organizationOwner = companyCheck.ownerId;
        const checkFeature = await PlanSubscriptionsList.checkFeature({
            userId: organizationOwner,
            feature: PlanFeatures.WAP_NOTIFICATIONS,
        });

        //Validar que el owner de la organizacion tenga un plan que permita bots.
        if (!checkFeature) {
            const serverAssigned = await this.servers.serverList.findOne({
                _id: companyCheck.wapServerId,
            });

            if (!serverAssigned) {
                return {
                    status: false,
                    error: "No se ha asignado un servidor a la compañia",
                    code: VALIDATE_BOT_ENVENT_ERRORS.NO_SERVER_ASSIGNED,
                };
            }

            return {
                status: false,
                error: "El plan de la organizacion no permite bots.",
                code: VALIDATE_BOT_ENVENT_ERRORS.NO_BOT_ALLOWED,
                serverUrl: serverAssigned.ipv4,
            };
        }

        //Validar que el plan este activo.
        const checkSubscriptionActive = await PlanSubscriptionsList.isSubscriptionActive({
            sessionUser: organizationOwner,
        });

        if (!checkSubscriptionActive) {
            return {
                status: false,
                error: "La  organizacion no tiene una suscripcion activa.",
                code: VALIDATE_BOT_ENVENT_ERRORS.NO_SUBSCRIPTION_ACTIVE,
            };
        }

        const serverAssigned = await this.servers.serverList.findOne({
            _id: companyCheck.wapServerId,
        });

        if (!serverAssigned) {
            return {
                status: false,
                error: "No se ha asignado un servidor a la compañia",
                code: VALIDATE_BOT_ENVENT_ERRORS.NO_SERVER_ASSIGNED,
            };
        }

        return {
            status: true,
            error: "",
            serverUrl: serverAssigned.ipv4,
        };
    }

    public async createBot(data: BotEventParams): Promise<void> {
        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        if (!validationStatus.status) {
            throw new Error(validationStatus.error);
        }

        const url = validationStatus.serverUrl;

        if (!url) {
            throw new Error("No se ha asignado un servidor a la organizacion");
        }

        try {
            const dataApi = {
                organizationId: data.companyId,
            };
            const response = await axios.post(this.getBotAdminApiUrl(url) + "containers/create", dataApi, {
                headers: {
                    "Content-Type": "application/json",
                },
            });

            const result: CreateBotResult = response.data;

            if (result.error) {
                throw new Error(result.error);
            }

            //Podria guardar el puerto asignado al bot.
            //Pero en realidad no hace falta, ya que conociendo el servidor
            //Donde se ejecuta el bot puedo averiguar el puerto si el que se encarga
            //de enviar el mensaje de wap es el mismo bot admin.

            //La idea seria que el server de tx le envia un post al bot admin para enviar un mensaje de wap y luego
            //el bot admin averigua cual es el puerto donde se esta ejecutando el bot y envia la solicitud.
            //De esta manera nunca hay una conexion directa entre la api principal (expuesta) y el bot.

            //Por lo tanto por el momento solo retorno sin novedades...
            return;
        } catch (error) {
            console.log(error);
            throw new Error("Ha ocurrido un error al crear el bot.");
        }
    }

    public async startBot(data: BotEventParams): Promise<void> {
        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        if (!validationStatus.status) {
            throw new Error(validationStatus.error);
        }

        const url = validationStatus.serverUrl;

        if (!url) {
            throw new Error("No se ha asignado un servidor a la organizacion");
        }

        try {
            const dataApi = {};
            const response = await axios.post(
                this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/start",
                dataApi,
                {
                    headers: {
                        "Content-Type": "application/json",
                    },
                },
            );

            const result: CreateBotResult = response.data;

            if (result.error) {
                throw new Error(result.error);
            }

            return;
        } catch (error) {
            if (error instanceof AxiosError) {
                const axiosError = error as AxiosError;
                if (axiosError.response) {
                    const responseError = axiosError.response;
                    console.log(responseError.data);
                }
            }
            throw new Error("Ha ocurrido un error al iniciar el bot.");
        }
    }

    public async stopBot(data: BotEventParams): Promise<void> {
        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        if (!validationStatus.status) {
            throw new Error(validationStatus.error);
        }

        const url = validationStatus.serverUrl;

        if (!url) {
            throw new Error("No se ha asignado un servidor a la organizacion");
        }

        try {
            const dataApi = {};
            const response = await axios.post(
                this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/stop",
                dataApi,
                {
                    headers: {
                        "Content-Type": "application/json",
                    },
                },
            );

            const result: CreateBotResult = response.data;

            if (result.error) {
                throw new Error(result.error);
            }

            return;
        } catch (error) {
            console.log(error);
            throw new Error("Ha ocurrido un error al detener el bot.");
        }
    }

    public async deleteBot(data: BotEventParams): Promise<void> {
        //Como quiere borrarlo no voy a validar si tiene una suscripcion activa.

        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        /*if (!validationStatus.status) {
            throw new Error(validationStatus.error);
        }*/

        const url = validationStatus.serverUrl;

        if (!url) {
            throw new Error("No se ha asignado un servidor a la organizacion");
        }

        try {
            const response = await axios.delete(this.getBotAdminApiUrl(url) + "containers/" + data.companyId);

            const result: CreateBotResult = response.data;

            if (result.error) {
                throw new Error(result.error);
            }

            return;
        } catch (error) {
            throw new Error("Ha ocurrido un error al detener el bot.");
        }
    }

    public async checkBot(data: BotEventParams): Promise<BotCheckStatus> {
        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        if (!validationStatus.status) {
            throw new Error(validationStatus.error);
        }

        const url = validationStatus.serverUrl;

        if (!url) {
            throw new Error("No se ha asignado un servidor a la organizacion");
        }

        try {
            const dataApi = {};
            const response = await axios.post(
                this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/status",
                dataApi,
                {
                    headers: {
                        "Content-Type": "application/json",
                    },
                },
            );

            return response.data as BotCheckStatus;
        } catch (error) {
            if (error instanceof AxiosError) {
                const errorAxios: AxiosError = error;
                if (errorAxios.response) {
                    const errorData = errorAxios.response.data as BotEventError;
                    if (errorData.code == 404) {
                        return {
                            state: BOT_STATE.NOT_EXISTS,
                            status: "dead",
                            serverState: SERVER_STATE.RUNNING,
                        };
                    } else {
                        throw new Error("Ha ocurrido un error al obtener el estado del bot.");
                    }
                } else {
                    //no hay comunicacion con el bot-api-admin.
                    return {
                        state: BOT_STATE.NOT_EXISTS,
                        status: "dead",
                        serverState: SERVER_STATE.NOT_WORKING,
                    };
                    throw new Error("Ha ocurrido un error al obtener el estado del bot.");
                }
            } else {
                throw new Error("Ha ocurrido un error al obtener el estado del bot.");
            }
        }
    }

    public async checkSession(data: BotEventParams): Promise<BotCheckSession> {
        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        if (!validationStatus.status) {
            throw new Error(validationStatus.error);
        }

        const url = validationStatus.serverUrl;

        if (!url) {
            throw new Error("No se ha asignado un servidor a la organizacion");
        }

        try {
            const dataApi = {};
            const response = await axios.post(
                this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/session",
                dataApi,
                {
                    headers: {
                        "Content-Type": "application/json",
                    },
                },
            );

            if (response.data.message == "ok") {
                return {
                    session: BOT_SESSION_STATUS.LOGED,
                };
            }

            return {
                session: BOT_SESSION_STATUS.UNLOGED,
            };
        } catch (error) {
            console.log(error);
            throw new Error("Ha ocurrido un error al obtener el estado del bot.");
        }
    }

    public async sendMessage(data: SendBotMessageParams): Promise<void> {
        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        if (!validationStatus.status) {
            throw new Error(validationStatus.error);
        }

        const url = validationStatus.serverUrl;

        if (!url) {
            throw new Error("No se ha asignado un servidor a la organizacion");
        }

        try {
            const dataApi = {
                number: data.number,
                message: data.message,
            };
            const response = await axios.post(
                this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/send",
                dataApi,
                {
                    headers: {
                        "Content-Type": "application/json",
                    },
                },
            );

            const result: CreateBotResult = response.data;

            if (result.error) {
                throw new Error(result.error);
            }

            return;
        } catch (error) {
            throw new Error("Ha ocurrido un error al enviar el mensaje.");
        }
    }

    public async getQr(data: BotEventParams): Promise<string> {
        const validationStatus = await this.validateBotEvent(data.sessionUser, data.companyId);

        if (!validationStatus.status) {
            throw new Error(validationStatus.error);
        }

        const url = validationStatus.serverUrl;

        if (!url) {
            throw new Error("No se ha asignado un servidor a la organizacion");
        }

        try {
            const response = await axios.get(
                this.getBotAdminApiUrl(url) + "containers/" + data.companyId + "/qr",
                { responseType: "arraybuffer" },
            );

            const base64Image = Buffer.from(response.data, "binary").toString("base64");

            return base64Image;
        } catch (error) {
            throw new Error("Ha ocurrido un error al obtener el estado del bot.");
        }
    }

    public async sysAdminGetWapServers(data: PaginateWapServerParams): Promise<PaginateWapServerResults> {
        const servers = await this.servers.paginate(data);

        return servers;
    }
}

const WapServerList = new WapServerManager();

export default WapServerList;
