import { BadRequestException, Injectable } from '@nestjs/common';
// import { RtcTokenBuilder, RtcRole } from 'agora-access-token';
import { RtcTokenBuilder, RtcRole } from 'agora-token';
import { ConfigService } from '@nestjs/config';
import { AgoraRole, MakeCallDto, RequestDto } from './dto/call.dto';
import { ModelsService } from 'src/models/models.service';
import { CallingStatus, CallMode, CallType } from './schema/calling.schema';
import { Types } from 'mongoose';
import { CommonService } from 'src/common/common.service';
import { ChatService } from 'src/chat/chat.service';
import * as moment from 'moment';
import { AnyAaaaRecord } from 'dns';
import { TranslationService } from 'src/common/services/translation.service';

@Injectable()
export class CallService {
    private readonly AGORA_APP_ID: string;
    private readonly AGORA_APP_CERTIFICATE: string;
    private readonly TOKEN_EXPIRATION: number;
    
    // Store pending timeout IDs for call cleanup
    private callEndTimeouts: Map<string, NodeJS.Timeout> = new Map();

    constructor(
        private readonly configService: ConfigService,
        private readonly model: ModelsService,
        private readonly commonServices: CommonService,
        private readonly chatService: ChatService,
        private readonly translationService: TranslationService,
    ) {
        this.AGORA_APP_ID = this.configService.get<string>('AGORA_APP_ID', '');
        this.AGORA_APP_CERTIFICATE = this.configService.get<string>('AGORA_APP_CERTIFICATE', '');
        this.TOKEN_EXPIRATION = Number(this.configService.get<string>('AGORA_TOKEN_EXPIRATION', '3600'));
    }

    async generateToken(dto: MakeCallDto) {
        try {
            const { channelName, role, call_to, connection_id } = dto;
            const setRole = role === AgoraRole.PUBLISHER
                ? RtcRole.PUBLISHER
                : RtcRole.SUBSCRIBER;


            // const agoraToken = RtcTokenBuilder.buildTokenWithUid(
            //   this.AGORA_APP_ID,
            //   this.AGORA_APP_CERTIFICATE,
            //   channelName,
            //   Number(uid),
            //   setRole,
            //   this.TOKEN_EXPIRATION
            // );

            let agora_token = RtcTokenBuilder.buildTokenWithUserAccount(
                this.AGORA_APP_ID,
                this.AGORA_APP_CERTIFICATE,
                channelName,
                "",
                RtcRole.PUBLISHER,
                18000,
                18000
            )

            // this.startRecording(channelName);

            return {
                channelName,
                token: agora_token,
                appId: this.AGORA_APP_ID,
                uid: Number(call_to ?? connection_id),
            };
        } catch (error) {
            throw error
        }
    }

    private buildCallNotification(
        name: string,
        call_mode: string,
        call_type: string,
        connection: any,
        data: any,
        call_by: any,
    ) {
        data = data.toJSON();
        return {
            title: 'Incoming Call',
            body:
                call_type === 'GROUP'
                    ? `${name} calling you in ${connection?.group_name} ${call_mode === CallMode.AUDIO ? 'Audio' : 'Video'} Call`
                    : `${name} calling you in ${call_mode === CallMode.AUDIO ? 'Audio' : 'Video'} Call`,
            type: call_mode === CallMode.AUDIO ? 'audio_call' : 'video_call',
            call_mode: call_mode.toLowerCase(),
            data: {
                ...data,
                call_by: call_by,
                group_name:  call_type === 'GROUP' ? connection?.group_name : null,
            },
        };
    }

    private async createCallLogMessage(call: any, connection: any, call_by: any, call_type: string) {
        try {
            const callLogPayload = {
                connection_id: connection._id,
                sent_by: call_by._id,
                sent_to: call_type === 'GROUP' ? null : call.call_to,
                message: `${call_by.name} started a ${call.call_mode.toLowerCase()} call`,
                message_type: 'CALL',
                type: 'NORMAL',
                call_data: {
                    call_id: call._id,
                    call_mode: call.call_mode,
                    call_type: call.call_type,
                    channel_name: call.channel_name,
                    started_at: moment().utc().valueOf()
                }
            };

            // Save the call log message using chat service
            await this.chatService.saveSingleMessage(call_by._id, callLogPayload);
        } catch (error) {
            console.error('Error creating call log message:', error);
            // Don't throw error to avoid breaking call creation
        }
    }

    private async isUserOnCall(userId: any): Promise<boolean> {
        return !!(
            await this.model.callings.findOne({
                $or: [{ call_by: userId }, { call_to: userId }],
                status: { $in: [CallingStatus.PENDING, CallingStatus.ACCEPT] },
            }) ||
            await this.model.callJoinedMembers.findOne({
                joined_by: userId,
                status: { $in: [CallingStatus.PENDING, CallingStatus.ACCEPT] },
            })
        );
    }
    private async sendCallPush(userId: any, payload: any) {
        const session = await this.model.SessionModel.find({ user_id: new Types.ObjectId(userId) }).sort({ _id: -1 }).limit(1);
        console.log(session, "ssion++++++++++++++++");

        // Fetch user to get user_type
        const user = await this.model.UserModel.findById(userId, { user_type: 1 }).lean();
        const userType = user?.user_type || 'USER';

        payload.data.call_to = userId.toString();

        if (session && session[0] && session[0].device_type === 'IOS' && session[0].voip_token) {
            return await this.commonServices.sendPushNotificaitonOnIOSDevices(
                session[0].voip_token,
                payload,
                userType, // Pass user type to select correct APNS topic
            );
        }

        if (session && session[0] && session[0].fcm_token) {
            return await this.commonServices.sendHighPriorityNotificationOnANDROIDDevices(
                [session[0].fcm_token],
                payload,
            );
        }
    }

    async makeCall(req: RequestDto, dto: MakeCallDto) {
        try {
            const { call_to, connection_id, call_mode, call_type } = dto;
            const { _id: call_by, name, language } = req.user_data;
            const lang = language || 'en';

            // Check if receiver has blocked caller before sending push
            const hasReceiverBlockedCaller = await this.model.blockUsers.findOne({
                blocked_by: call_to,
                blocked_to: new Types.ObjectId(call_by)
            });

            /* 🔹 Call checks */
            // Check if caller is already on a call
            if (await this.isUserOnCall(call_by))
                throw new BadRequestException(this.translationService.translate('CALLER_ALREADY_ON_CALL', lang));

            // Check if receiver is already on a call (only for one-to-one calls)
            if (call_type !== 'GROUP') {
                if (!call_to) {
                    throw new BadRequestException(this.translationService.translate('RECEIVER_ID_REQUIRED', lang));
                }
                
                const isReceiverOnCall = await this.isUserOnCall(call_to);
                console.log(`Checking if receiver ${call_to} is on call:`, isReceiverOnCall);
                if (isReceiverOnCall)
                    throw new BadRequestException(this.translationService.translate('RECEIVER_ALREADY_ON_CALL', lang));
                
                // Check if caller has blocked receiver
                const hasCallerBlockedReceiver = await this.model.blockUsers.findOne({
                    blocked_by: new Types.ObjectId(call_by),
                    blocked_to: call_to
                });
                
                if (hasCallerBlockedReceiver) {
                    throw new BadRequestException(this.translationService.translate('CALLER_BLOCKED_RECEIVER', lang));
                }
                
                
                if (hasReceiverBlockedCaller) {
                    console.log(`User ${call_by} tried to call ${call_to}, but they are blocked. Call created but no push sent.`);
                    // Continue without throwing error - call will be created but no push will be sent
                }
            }

            /* 🔹 Connection */
            const connection = await this.model.Connections.findOne({ _id: connection_id });
            if (!connection) throw new BadRequestException(this.translationService.translate('CONNECTION_NOT_FOUND', lang));

            /* 🔹 Agora token */
            const agora = await this.generateToken(dto);

            /* 🔹 Create call */
            let data_to_save = {
                call_by,
                call_to: call_type === 'GROUP' ? null : call_to,
                connection_id,
                call_mode,
                call_type,
                agora_token: agora.token,
                channel_name: agora.channelName,
                status: CallingStatus.PENDING,
                start_time: 0,
                end_time: 0,
                duration_in_ms: 0,
                total_duration_in_ms: 0,
                created_at: moment().utc().valueOf(),
            }
            const call = await this.model.callings.create(data_to_save);

            console.log(data_to_save, "data+trooooooooooooooooo");

            if (!hasReceiverBlockedCaller) {
                await this.createCallLogMessage(call, connection, req.user_data, call_type);
            }

            /* 🔹 Create call log message */

            /* 🔹 Notification payload */
            const notification = this.buildCallNotification(
                name,
                call_mode,
                call_type,
                connection,
                call,
                req.user_data,
            );
            console.log(notification, "data+notification");

            /* 🔹 Schedule timeout job to mark call as NOT_ANSWERE after 1 minute */
            this.scheduleCallTimeout(call._id, call_type);

            /* 🔹 GROUP CALL */
            if (call_type === 'GROUP') {
                const members: Types.ObjectId[] =
                    await this.model.GroupMembers.distinct('user_id', {
                        connection_id: new Types.ObjectId(connection_id),
                        user_id: { $ne: new Types.ObjectId(call_by) },
                    });

                await this.model.callJoinedMembers.insertMany(
                    members.map(userId => ({
                        call_id: new Types.ObjectId(call._id),
                        joined_by: userId,
                        status: CallingStatus.PENDING,
                        accepted_at: 0,
                        ended_at: 0,
                        declined_at: 0,
                    })),
                );

                await Promise.all(
                    members.map(userId =>
                        this.sendCallPush(userId, notification),
                    ),
                );
            }

            /* 🔹 ONE TO ONE */
            else {
                console.log("call type one to one found");

            
                // Only send push if receiver hasn't blocked the caller
                if (!hasReceiverBlockedCaller) {
                    await this.sendCallPush(call_to, notification);
                } else {
                    console.log(`Push notification not sent to ${call_to} because they blocked ${call_by}`);
                }
            }

            return {
                message: this.translationService.translate('CALL_INITIATED_SUCCESS', lang),
                call_id: call._id,
                channel_name: call.channel_name,
                agora_token: call.agora_token,
            };
        } catch (error) {
            console.log(error)
            throw error;
        }
    }

    /**
     * Schedule a timeout job to mark call as NOT_ANSWERE if still PENDING after 1 minute
     * @param callId - The call ID to monitor
     * @param callType - GROUP or ONE_TO_ONE
     */
    private scheduleCallTimeout(callId: Types.ObjectId, callType: string): void {
        const TIMEOUT_DURATION = 60 * 1000; // 1 minute in milliseconds
        
        setTimeout(async () => {
            try {
                console.log(`Checking call ${callId} for timeout...`);
                
                // Fetch the call to check its current status
                const call = await this.model.callings.findById(callId);
                
                if (!call) {
                    console.log(`Call ${callId} not found, skipping timeout check`);
                    return;
                }
                
                // If call is still PENDING, mark it as NOT_ANSWERE
                if (call.status === CallingStatus.PENDING) {
                    console.log(`Call ${callId} is still PENDING, marking as NOT_ANSWERE`);
                    
                    call.status = CallingStatus.NOT_ANSWERE;
                    call.end_time = moment().utc().valueOf();
                    await call.save();
                    
                    // For group calls, also update all joined members
                    if (callType === 'GROUP') {
                        await this.model.callJoinedMembers.updateMany(
                            { 
                                call_id: callId, 
                                status: CallingStatus.PENDING 
                            },
                            {
                                $set: {
                                    status: CallingStatus.NOT_ANSWERE,
                                    ended_at: moment().utc().valueOf()
                                }
                            }
                        );
                    }
                    
                    console.log(`Call ${callId} marked as NOT_ANSWERE successfully`);
                } else {
                    console.log(`Call ${callId} status is ${call.status}, no timeout action needed`);
                }
            } catch (error) {
                console.error(`Error processing timeout for call ${callId}:`, error);
            }
        }, TIMEOUT_DURATION);
        
        console.log(`Timeout job scheduled for call ${callId} (${TIMEOUT_DURATION / 1000} seconds)`);
    }

    handleDisconnect = async (token: string, server?: any) => {
        try {
            let payload = await this.commonServices.verify_token(token);
            await this.model.UserModel.findOneAndUpdate({ _id: payload._id }, { is_online: false });

            // End all active or pending one-to-one calls for this user
            await this.endUserOneToOneCalls(payload._id, server);
        }
        catch (error) {
            console.log(error, "error will be occured while disconnecting socket..........");
        }
    }

    /**
     * End all active or pending one-to-one calls for a disconnected user
     * @param userId - The user ID who disconnected
     * @param server - Socket server instance
     */
    private async endUserOneToOneCalls(userId: Types.ObjectId, server?: any): Promise<void> {
        try {
            // Find all PENDING or ACCEPT one-to-one calls where user is caller or receiver
            const activeCalls = await this.model.callings.find({
                call_type: "ONE_TO_ONE",
                $or: [
                    { call_by: new Types.ObjectId(userId) },
                    { call_to: userId?.toString() }
                ],
                status: { $in: ["PENDING", "ACCEPT"] },
                is_deleted: false
            });

            if (activeCalls.length === 0) {
                console.log(`No active one-to-one calls found for user ${userId}`);
                return;
            }

            console.log(`Found ${activeCalls.length} active one-to-one calls for user ${userId}, scheduling end in 30 seconds...`);

            // Schedule call end after 30 seconds
            const REJOIN_WINDOW = 30 * 1000; // 30 seconds in milliseconds

            for (const call of activeCalls) {
                const callId = call._id.toString();
                
                // Set a timeout to end this specific call after 30 seconds
                const timeoutId = setTimeout(async () => {
                    try {
                        // Remove from map after execution
                        this.callEndTimeouts.delete(callId);

                        // Re-check if the call still exists
                        const currentCall = await this.model.callings.findById(callId);
                        
                        if (!currentCall) {
                            console.log(`Call ${callId} no longer exists, skipping end`);
                            return;
                        }

                        // If call status changed, skip
                        if (!["PENDING", "ACCEPT"].includes(currentCall.status)) {
                            console.log(`Call ${callId} status changed to ${currentCall.status}, skipping end`);
                            return;
                        }

                        // User still disconnected, proceed to end the call
                        const currentTime = moment().utc().valueOf();
                        const newStatus = currentCall.status === "PENDING" ? "NOT_ANSWERE" : "END";

                        let durationInMs = 0;
                        let totalDurationInMs = 0;
                        
                        if (currentCall.status === "ACCEPT" && currentCall.start_time > 0) {
                            durationInMs = currentTime - currentCall.start_time;
                            totalDurationInMs = currentCall.total_duration_in_ms + durationInMs;
                        }

                        // Update the call record
                        await this.model.callings.findByIdAndUpdate(
                            callId,
                            {
                                $set: {
                                    status: newStatus,
                                    end_time: currentTime,
                                    duration_in_ms: durationInMs,
                                    total_duration_in_ms: totalDurationInMs,
                                    updated_at: currentTime
                                }
                            }
                        );

                        console.log(`Call ${callId} ended after 30s timeout with status: ${newStatus}`);

                        // Notify the other user about the call end
                        const otherUserId = currentCall.call_by.toString() === userId.toString() 
                            ? currentCall.call_to 
                            : currentCall.call_by;

                        if (otherUserId && server) {
                            const otherUser = await this.model.UserModel.findById(otherUserId, { socket_id: 1 });
                            if (otherUser?.socket_id) {
                                server.to(otherUser.socket_id).emit("listening_event", {
                                    event_type: "CALL_ENDED_BY_DISCONNECT",
                                    call_id: callId,
                                    call_status: newStatus,
                                    duration: durationInMs,
                                    message: "Call ended because user disconnected for more than 30 seconds"
                                });
                            }
                        }
                    } catch (callError) {
                        console.error(`Error ending call ${callId} after timeout:`, callError);
                    }
                }, REJOIN_WINDOW);

                // Store timeout ID so we can cancel it if user rejoins
                this.callEndTimeouts.set(callId, timeoutId);
                console.log(`Scheduled end for call ${callId} in 30 seconds (timeoutId: ${timeoutId})`);
            }

        } catch (error) {
            console.error(`Error in endUserOneToOneCalls for user ${userId}:`, error);
        }
    }

    /**
     * Rejoin a call after reconnection - cancels the scheduled call end
     * @param channelName - The channel name to rejoin
     * @param userId - The user ID who is rejoining
     */
    async rejoinCall(channelName: string, userId: string, language?: string): Promise<any> {
        try {
            let lang = language || 'en';
            console.log(`User ${userId} attempting to rejoin call with channel ${channelName}`);

            // Find the call by channel_name
            const call = await this.model.callings.findOne({ channel_name: channelName });
            
            if (!call) {
                throw new BadRequestException(this.translationService.translate('CALL_NOT_FOUND', lang));
            }

            const callId = call._id.toString();

            // Check if user is part of this call
            const isPartOfCall = 
                call.call_by.toString() === userId || 
                call.call_to?.toString() === userId;

            if (!isPartOfCall) {
                throw new BadRequestException(this.translationService.translate('NOT_PART_OF_CALL', lang));
            }

            // Check if call is still active
            if (!["PENDING", "ACCEPT"].includes(call.status)) {
                throw new BadRequestException(this.translationService.translate('CALL_ALREADY_ENDED', lang).replace('{status}', call.status));
            }

            // Cancel the scheduled end timeout for this call
            const timeoutId = this.callEndTimeouts.get(callId);
            if (timeoutId) {
                clearTimeout(timeoutId);
                this.callEndTimeouts.delete(callId);
                console.log(`Cancelled scheduled end for call ${callId} (channel: ${channelName})`);
            }

            console.log(`User ${userId} successfully rejoined call ${callId}`);

            return {
                message: this.translationService.translate('CALL_REJOINED_SUCCESS', lang),
                call: {
                    call_id: call._id,
                    status: call.status,
                    call_mode: call.call_mode,
                    channel_name: call.channel_name,
                    agora_token: call.agora_token
                }
            };
        } catch (error) {
            console.error(`Error rejoining call with channel ${channelName}:`, error);
            throw error;
        }
    }


}
