import { SubscribeMessage, WebSocketGateway, WebSocketServer, ConnectedSocket, MessageBody } from '@nestjs/websockets';
import { Socket, Server } from "socket.io";
import { ChatAuthGuard } from 'src/auth/auth.guard';
import { ModelsService } from 'src/models/models.service';
import { CommonService } from 'src/common/common.service';
import { BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
import { CallService } from './call.service';
import { AcceptRejectDto, JoinCallConnectionDto } from './dto/call.dto';
import { CallingStatus } from './schema/calling.schema';
import * as moment from 'moment';
import { Types } from 'mongoose';


interface CustomSocket extends Socket {
    user_data: any; // Add your desired type for user_data property
}
@WebSocketGateway({
    cors: {
        origin: "*",
        credentials: true
    },
})
export class CallGateway {
    private projection = { __v: 0 } as const;
    private lean_options = { lean: true };
    private new_options = { new: true };
    @WebSocketServer() server: Server;
    constructor(
        private readonly callService: CallService,
        private readonly chatAuthService: ChatAuthGuard,
        private readonly common: CommonService,
        private readonly model: ModelsService,
    ) { }


    //You can handle their multiple operations like user online status
    // in this function we are just verify user is valid or not 
    // @WebSocketServer() server: Server;
    async handleConnection(socket: Socket) {
        try {
            let data = await this.chatAuthService.validateSocket(socket)
            socket['user_data'] = data
            socket.to(socket?.id).emit("listening_event", { data })
        }
        catch (error) {
            console.log(error, "error will be occured while connecting socket..........");
            this.handleDisconnect(socket)
            socket.to(socket?.id?.toString()).emit("listening_event", {
                event_type: "CONNECTING_SOCKET_ERROR",
                data: error
            })
        }
    }

    @SubscribeMessage("update_call_status")
    async updateCallStatus(socket: CustomSocket, payload: AcceptRejectDto) {
        try {
            return await this.acceptRejectCall(socket?.user_data?._id, payload, socket)
        }
        catch (error) {
            throw error;
        }
    }


    @SubscribeMessage("join_call_connection")
    async joinCallConnection(socket: CustomSocket, payload: JoinCallConnectionDto) {
        try {
            let { call_id } = payload
            let { _id: user_id } = socket.user_data;
            socket.join(call_id)
            socket.to(call_id).emit("listening_event", { event_type: "JOIN_CALL_CONNECTION", data: { call_id, user_id } });
        }
        catch (error) {
            throw error;
        }
    }

    @SubscribeMessage("leave_call_connection")
    async leaveCallConnection(socket: CustomSocket, payload: JoinCallConnectionDto) {
        try {
            let { call_id } = payload
            let { _id: user_id } = socket.user_data;
            socket.leave(call_id)
            socket.to(call_id).emit("listening_event", { event_type: "LEAVE_CALL_CONNECTION", data: { call_id, user_id } });
        }
        catch (error) {
            throw error;
        }
    }

    async acceptRejectCall(user_id: Types.ObjectId, payload: AcceptRejectDto, socket?: CustomSocket) {
        try {
            let { call_id, status } = payload
            let calling_id: Types.ObjectId = new Types.ObjectId(call_id)
            let data;
            switch (status) {
                case "ACCEPT":
                    data = await this.acceptCall(user_id, calling_id, socket)
                    break;
                case "NOT_ANSWERE":
                    data = await this.notAnswerCall(user_id, calling_id, socket)
                    break;
                case "DECLINE":
                    data = await this.rejectCall(user_id, calling_id, socket)
                    break;

                case "END":
                    data = await this.endCall(user_id, calling_id, socket)
                    break;
                default:
                    throw new HttpException({ message: "Please provide valid status" }, HttpStatus.BAD_REQUEST)
            }
            return { data: data ? data : null }

        }
        catch (error) {
            throw error;
        }
    }

    async acceptCall(user_id: Types.ObjectId, call_id: Types.ObjectId, socket?: CustomSocket) {
        const call: any = await this.model.callings.findOne({ _id: call_id });
        if (!call) throw new BadRequestException('Call not found');
        const now = () => moment().utc().valueOf();

        if (call?.call_by?.toString() === user_id?.toString())
            throw new BadRequestException("You can't accept your own call");

        if (call.call_type === 'GROUP') {
            const member = await this.model.callJoinedMembers.findOneAndUpdate(
                { call_id, joined_by: user_id },
                { status: CallingStatus.ACCEPT, accepted_at: now() },
            );

            if (!member) throw new BadRequestException('Not a group member');

            const activeCount = await this.model.callJoinedMembers.countDocuments({
                call_id,
                status: CallingStatus.ACCEPT,
            });

            if (activeCount === 1) {
                await this.model.callings.updateOne(
                    { _id: call_id },
                    { status: CallingStatus.ACCEPT, start_time: now() },
                );
            }
        } else {
            await this.model.callings.updateOne(
                { _id: call_id },
                { status: CallingStatus.ACCEPT, start_time: now() },
            );
        }

        const io: any = this.server ?? (socket as any)?.server;
        if (!io) {
            console.error("Socket server instance not available to emit message");
            return;
        }

        let dataToSend = {
            message: "Call accepted",
            call_id: call_id,
            user_id: user_id,
        }
        io.to(String(call_id)).emit("listening_event", {
            event_type: "ACCEPT_CALL",
            call_id: call_id,
            data: dataToSend,
        });
        return dataToSend;
    }

    async rejectCall(user_id: Types.ObjectId, call_id: Types.ObjectId, socket?: CustomSocket) {
        const call: any = await this.model.callings.findOne({ _id: call_id });
        console.log(call, "call");
        if (!call) throw new BadRequestException('Call not found');
        const now = () => moment().utc().valueOf();

        const endTime = moment().utc().valueOf();

        /* ================= GROUP CALL ================= */
        if (call.call_type === 'GROUP') {
            const acceptedCount = await this.model.callJoinedMembers.countDocuments({
                call_id,
                status: CallingStatus.ACCEPT,
            });
            if (user_id.toString() === call.call_by.toString() && acceptedCount === 0) {
                await this.model.callings.updateOne(
                    { _id: call_id },
                    {
                        status: CallingStatus.DECLINE,
                        end_time: endTime,
                        total_duration_in_ms: endTime - call.created_at,
                    },
                );

                await this.model.callJoinedMembers.updateMany(
                    { call_id, status: CallingStatus.PENDING },
                    { status: CallingStatus.NOT_ANSWERE },
                );

                // return { message: 'Call cancelled by caller' };
                const io: any = this.server ?? (socket as any)?.server;
                if (!io) {
                    console.error("Socket server instance not available to emit message");
                    return;
                }
                let dataToSend = {
                    message: "Call rejected",
                    call_id: call_id,
                    user_id: user_id,
                }
                io.to(String(call_id)).emit("listening_event", {
                    event_type: "REJECT_CALL",
                    call_id: call_id,
                    data: dataToSend,
                });
                return dataToSend;
            }
            /* 🔹 Update member status (if member exists) */
            await this.model.callJoinedMembers.findOneAndUpdate(
                { call_id, joined_by: user_id },
                { status: CallingStatus.DECLINE, declined_at: endTime },
            );

            const pendingCount = await this.model.callJoinedMembers.countDocuments({
                call_id,
                status: CallingStatus.PENDING,
            });

            /*  CASE: Caller cancels & nobody accepted */
            if (user_id === call.call_by.toString() && acceptedCount === 0) {
                await this.model.callings.updateOne(
                    { _id: call_id },
                    {
                        status: CallingStatus.DECLINE,
                        end_time: endTime,
                        total_duration_in_ms: endTime - call.created_at,
                    },
                );

                await this.model.callJoinedMembers.updateMany(
                    { call_id, status: CallingStatus.PENDING },
                    { status: CallingStatus.NOT_ANSWERE },
                );

                // return { message: 'Call cancelled by caller' };
                const io: any = this.server ?? (socket as any)?.server;
                if (!io) {
                    console.error("Socket server instance not available to emit message");
                    return;
                }
                let dataToSend = {
                    message: "Call rejected",
                    call_id: call_id,
                    user_id: user_id,
                }
                io.to(String(call_id)).emit("listening_event", {
                    event_type: "REJECT_CALL",
                    call_id: call_id,
                    data: dataToSend,
                });
                return dataToSend;
            }

            /* 🔹 CASE: All members rejected / missed */
            if (pendingCount === 0 && acceptedCount === 0) {
                await this.model.callings.updateOne(
                    { _id: call_id },
                    {
                        status: CallingStatus.DECLINE,
                        end_time: endTime,
                        total_duration_in_ms: endTime - call.created_at,
                    },
                );
            }

            // Send silent notification to caller when member rejects NORMAL calls (not when caller cancels)
            if (user_id.toString() !== call.call_by.toString() && call.call_type === 'NORMAL') {
                const notificationData = {
                    type: "CALL_DECLINED",
                    title: "Call Declined",
                    message: "Call was declined by a participant",
                    call_id: call_id,
                    declined_by: user_id,
                    call_mode: call.call_mode,
                    call_type: call.call_type,
                    connection_id: call.connection_id,
                };
                await this.sendCallDeclinedNotification(call.call_by, notificationData);
            }

            // return { message: 'Call rejected' };
            const io: any = this.server ?? (socket as any)?.server;
            if (!io) {
                console.error("Socket server instance not available to emit message");
                return;
            }
            let dataToSend = {
                message: "Call rejected",
                call_id: call_id,
                user_id: user_id,
            }
            io.to(String(call_id)).emit("listening_event", {
                event_type: "REJECT_CALL",
                call_id: call_id,
                data: dataToSend,
            });
            return dataToSend;
        }

        /* ================= ONE TO ONE ================= */
        await this.model.callings.updateOne(
            { _id: call_id },
            {
                status: CallingStatus.DECLINE,
                end_time: endTime,
                total_duration_in_ms: endTime - call.created_at,
            },
        );

        console.log("here")

        // Send silent notification to caller when receiver rejects NORMAL call
        if (call.call_type === 'ONE_TO_ONE') {
            console.log("Normal call")
            const notificationData = {
                type: "CALL_DECLINED",
                title: "Call Declined",
                message: "Call was declined",
                call_id: call_id,
                declined_by: user_id,
                call_mode: call.call_mode,
                call_type: call.call_type,
                connection_id: call.connection_id,
            };
            console.log(notificationData, "notificationData")
            await this.sendCallDeclinedNotification(call.call_by, notificationData);
        }

        const io: any = this.server ?? (socket as any)?.server;
        if (!io) {
            console.error("Socket server instance not available to emit message");
            return;
        }
        let dataToSend = {
            message: "Call rejected",
            call_id: call_id,
            user_id: user_id,
        }
        io.to(String(call_id)).emit("listening_event", {
            event_type: "REJECT_CALL",
            call_id: call_id,
            data: dataToSend
        });
        return dataToSend;
    }

    async endCall(user_id: Types.ObjectId, call_id: Types.ObjectId, socket?: CustomSocket) {
        const call: any = await this.model.callings.findOne({ _id: call_id });
        if (!call) throw new BadRequestException('Call not found');
        const now = () => moment().utc().valueOf();

        const endTime = now();

        if (call.call_type === 'GROUP') {
            await this.model.callJoinedMembers.findOneAndUpdate(
                { call_id, joined_by: user_id, status: CallingStatus.ACCEPT },
                { status: CallingStatus.END, ended_at: endTime },
            );

            const activeLeft = await this.model.callJoinedMembers.countDocuments({
                call_id,
                status: CallingStatus.ACCEPT,
            });

            if (activeLeft === 0) {
                await this.model.callings.updateOne(
                    { _id: call_id },
                    {
                        status: CallingStatus.END,
                        end_time: endTime,
                        duration_in_ms: endTime - call.start_time,
                        total_duration_in_ms: endTime - call.created_at,
                    },
                );
            }
        } else {
            await this.model.callings.updateOne(
                { _id: call_id },
                {
                    status: CallingStatus.END,
                    end_time: endTime,
                    duration_in_ms: endTime - call.start_time,
                    total_duration_in_ms: endTime - call.created_at,
                },
            );
        }

        // Send silent push notifications to other participants
        if (call.call_type === 'GROUP') {
            console.log("Group call")
            // Send to all other active group members (if any)
            const otherActiveMembers = await this.model.callJoinedMembers.find({
                call_id,
                joined_by: { $ne: user_id },
                status: CallingStatus.ACCEPT
            }).select('joined_by');

            // Also send to pending members if caller is ending the call before anyone accepted
            const isCallerEndingUnansweredCall = user_id.toString() === call.call_by.toString() && otherActiveMembers.length === 0;
            let pendingMembers = [];
            if (isCallerEndingUnansweredCall) {
                pendingMembers = await this.model.callJoinedMembers.find({
                    call_id,
                    joined_by: { $ne: user_id },
                    status: CallingStatus.PENDING
                }).select('joined_by');
            }

            // Combine active and pending members
            const allRecipients = [...otherActiveMembers, ...pendingMembers];

            for (const member of allRecipients) {
                const notificationData = {
                    type: "CALL_ENDED",
                    title: "Call Ended",
                    message: isCallerEndingUnansweredCall ? "Call was cancelled" : "Call was ended by a participant",
                    call_id: call_id,
                    ended_by: user_id,
                    call_mode: call.call_mode,
                    call_type: call.call_type
                };
                await this.sendCallEndNotification(member.joined_by, notificationData);
            }
        } else {
            console.log("One to one call")
            // Send to the other participant in one-to-one call
            console.log(call, "call")
            const otherParticipantId = call.call_to;
            console.log(otherParticipantId, "otherParticipantId")
            if (otherParticipantId) {
                const notificationData = {
                    type: "CALL_ENDED",
                    title: "Call Ended",
                    message: "Call was ended",
                    call_id: call_id,
                    ended_by: user_id,
                    call_mode: call.call_mode,
                    call_type: call.call_type
                };
                console.log(notificationData, "notificationData")
                await this.sendCallEndNotification(otherParticipantId, notificationData);
            }
        }

        const io: any = this.server ?? (socket as any)?.server;
        if (!io) {
            console.error("Socket server instance not available to emit message");
            return;
        }
        let dataToSend = {
            message: "Call ended",
            call_id: call_id,
            user_id: user_id,
        }
        io.to(String(call_id)).emit("listening_event", {
            event_type: "END_CALL",
            call_id: call_id,
            data: dataToSend,
        });
        return dataToSend;
    }

    async notAnswerCall(user_id: Types.ObjectId, call_id: Types.ObjectId, socket?: CustomSocket) {
        const call: any = await this.model.callings.findOne({ _id: call_id });
        if (!call || call.status !== CallingStatus.PENDING) return;

        const endTime = moment().utc().valueOf();

        /* 🔹 GROUP CALL */
        if (call.call_type === 'GROUP') {

            /* Update pending members first */
            await this.model.callJoinedMembers.updateMany(
                { call_id, status: CallingStatus.PENDING, joined_by: user_id },
                { status: CallingStatus.NOT_ANSWERE },
            );

            const totalMembers = await this.model.callJoinedMembers.countDocuments({
                call_id,
            });

            const notAnsweredCount = await this.model.callJoinedMembers.countDocuments({
                call_id,
                status: CallingStatus.NOT_ANSWERE,
            });
            let is_all_member_not_answered = totalMembers === notAnsweredCount;
            /* ✅ Update MAIN call ONLY if everyone missed */
            if (is_all_member_not_answered) {
                await this.model.callings.updateOne(
                    { _id: call_id },
                    {
                        status: CallingStatus.NOT_ANSWERE,
                        end_time: endTime,
                        total_duration_in_ms: endTime - call.created_at,
                    },
                );
            }

            const io: any = this.server ?? (socket as any)?.server;
            if (!io) {
                console.error("Socket server instance not available to emit message");
                return;
            }
            let dataToSend = {
                message: "Call not answered",
                call_id: call_id,
                user_id: user_id,
                is_all_member_not_answered
            }
            io.to(String(call_id)).emit("listening_event", {
                event_type: "NOT_ANSWER_CALL",
                call_id: call_id,
                data: dataToSend,
            });
            return dataToSend
        }

        /* 🔹 ONE TO ONE */
        await this.model.callings.updateOne(
            { _id: call_id },
            {
                status: CallingStatus.NOT_ANSWERE,
                end_time: endTime,
                total_duration_in_ms: endTime - call.created_at,
            },
        );

        const io: any = this.server ?? (socket as any)?.server;
        if (!io) {
            console.error("Socket server instance not available to emit message");
            return;
        }
        let dataToSend = {
            message: "Call not answered",
            call_id: call_id,
            user_id: user_id,
            is_all_member_not_answered: true
        }
        io.to(String(call_id)).emit("listening_event", {
            event_type: "NOT_ANSWER_CALL",
            call_id: call_id,
            data: dataToSend,
        });
        return dataToSend
    }

    afterInit(server: Server) {
        console.log("after init log");
    }

    //You can handle their multiple operations like user ofline status and etc
    async handleDisconnect(socket: Socket) {
        try {
            let token = socket.handshake.headers.token;
            console.log(socket.id, "user disconnected successfully");
            await this.callService.handleDisconnect(token as string, this.server);
        }
        catch (error) {
            socket.to(socket?.id?.toString()).emit("listening_event", {
                event_type: "DISCONNECTING_SOCKET_ERROR",
                data: error
            })
        }
    }

    /**
     * Rejoin call event - called by frontend when user reconnects
     * Cancels the scheduled call end timeout
     */
    @SubscribeMessage("rejoin_call")
    async handleRejoinCall(
        @ConnectedSocket() socket: CustomSocket,
        @MessageBody() payload: { channel_name: string }
    ) {
        try {
            const { _id: user_id , language} = socket.user_data;
            const { channel_name } = payload;

            console.log(`User ${user_id} requesting to rejoin call with channel ${channel_name}`);

            const result = await this.callService.rejoinCall(channel_name, user_id.toString() , language);

            // Emit success to the user
            socket.emit("listening_event", {
                event_type: "REJOIN_CALL_SUCCESS",
                channel_name: channel_name,
                data: result
            });

            // Notify other user that the disconnected user is back
            const call = await this.model.callings.findOne({ channel_name: channel_name });
            if (call) {
                const otherUserId = call.call_by.toString() === user_id.toString() 
                    ? call.call_to 
                    : call.call_by;

                if (otherUserId) {
                    const otherUser = await this.model.UserModel.findById(otherUserId, { socket_id: 1 });
                    if (otherUser?.socket_id) {
                        this.server.to(otherUser.socket_id).emit("listening_event", {
                            event_type: "CALL_USER_REJOINED",
                            call_id: call._id,
                            channel_name: channel_name,
                            user_id: user_id,
                            message: "User has reconnected to the call"
                        });
                    }
                }
            }
        } catch (error) {
            console.error('Error in rejoin_call:', error);
            socket.emit("listening_event", {
                event_type: "REJOIN_CALL_ERROR",
                data: error.message || "Failed to rejoin call"
            });
        }
    }

    private async sendCallEndNotification(userId: Types.ObjectId, notificationData: any) {
        try {
            console.log("sendCallEndNotification")
            // Get user FCM tokens
            let query = {
                user_id: new Types.ObjectId(userId),
                fcm_token: { $ne: null },
            };
            console.log(query, "query++++++++++++++++++++++")
            let sessions = await this.model.SessionModel.find(query, 'fcm_token device_type');
            console.log(sessions, "sessions")
            if (sessions && sessions.length > 0) {
                // Separate iOS and Android tokens
                let iosTokens: string[] = [];
                let androidTokens: string[] = [];

                sessions.forEach(session => {
                    if (session.device_type === 'IOS') {
                        iosTokens.push(session.fcm_token);
                    } else if (session.device_type === 'ANDROID' || session.device_type === "WEB") {
                        androidTokens.push(session.fcm_token);
                    }
                });
                console.log(iosTokens, "iosTokens")
                console.log(androidTokens, "androidTokens")
                // Send silent notifications to iOS devices
                if (iosTokens.length > 0) {
                    await this.common.sendSilentPushNotification(iosTokens, notificationData);
                }
                
                // Send silent notifications to Android devices
                if (androidTokens.length > 0) {
                    await this.common.sendSilentPushNotification(androidTokens, notificationData);
                }
            }
        } catch (error) {
            console.error('Error sending call end notification:', error);
            // Don't throw error to avoid breaking call end flow
        }
    }

    private async sendCallDeclinedNotification(userId: Types.ObjectId, notificationData: any) {
        try {
            console.log("sendCallDeclinedNotification")
            // Get user FCM tokens
            let query = {
                user_id: new Types.ObjectId(userId),
                fcm_token: { $ne: null },
            };
            console.log(query, "query++++++++++++++++++++++ 2")
            let sessions = await this.model.SessionModel.find(query, 'fcm_token device_type');
            console.log(sessions, "sessions")
            if (sessions && sessions.length > 0) {
                // Separate iOS and Android tokens
                let iosTokens: string[] = [];
                let androidTokens: string[] = [];

                sessions.forEach(session => {
                    if (session.device_type === 'IOS') {
                        iosTokens.push(session.fcm_token);
                    } else if (session.device_type === 'ANDROID' || session.device_type === "WEB") {
                        androidTokens.push(session.fcm_token);
                    }
                });

                // Send silent notifications to iOS devices
                if (iosTokens.length > 0) {
                    await this.common.sendSilentPushNotification(iosTokens, notificationData);
                }

                // Send silent notifications to Android devices
                if (androidTokens.length > 0) {
                    await this.common.sendSilentPushNotification(androidTokens, notificationData);
                }
            }
        } catch (error) {
            console.error('Error sending call declined notification:', error);
            // Don't throw error to avoid breaking call rejection flow
        }
    }
}
