import { Request, Response } from 'express'
import { IUser, User } from '../users/models'
import { Chat, IChatParticipant } from './models'
import { Connection, IConnection } from '../connections/models'
import { Follower } from '../followers/models'
import { IFiles } from '../../utils/types'
import helpers from '../../utils/helpers'
import services from '../../utils/services'
import { IMessage, Message } from '../messages/models'
import { Schema, Types } from 'mongoose'
import { Status } from '../statuses/models'
import { IEvent, Event } from '../events/models'
import reelControllers from '../reels/controllers'
import { IScore } from '../scores/models'
import constants from '../../utils/constants'

async function getChats(req: Request, res: Response) {
	interface Payload {
		limit?: string | number
		page?: string | number
		search?: string
		group?: 'true' | 'false'
		meetMeOnly?: 'true' | 'false'
	}

	try {
		const user = res.locals.user as IUser
		let {
			limit = '10',
			page = '1',
			search,
			group,
			meetMeOnly,
		} = req.query as unknown as Payload
		limit = +limit
		page = +page
		const skip = (page - 1) * limit

		const match: any = {
			'participants.user': user._id,
			...(meetMeOnly === 'true' ? { meetMe: { $exists: true } } : {}),
		}

		if (group === 'true') {
			match.isGroup = true
		} else if (group === 'false') {
			match.$or = [
				{ isGroup: false },
				{ isGroup: { $exists: false } },
			]
		}

		const pipeline: any = [
			// match
			{ $match: match },

			// populate creator
			{
				$lookup: {
					from: 'users',
					let: { creatorId: '$creator' },
					pipeline: [
						{ $match: { $expr: { $eq: ['$_id', '$$creatorId'] } } },
						{ $project: { name: 1, username: 1, photo: 1 } }
					],
					as: 'creator'
				}
			},
			{ $unwind: '$creator' },

			// populate user (optional)
			{
				$lookup: {
					from: 'users',
					let: { userId: '$user' },
					pipeline: [
						{ $match: { $expr: { $eq: ['$_id', '$$userId'] } } },
						{ $project: { name: 1, username: 1, photo: 1 } }
					],
					as: 'user'
				}
			},
			{ $unwind: { path: '$user', preserveNullAndEmptyArrays: true } },

			// dynamic computation: name and photo
			{
				$addFields: {
					name: {
						$cond: {
							if: {
								$or: [{ $not: ['$isGroup'] }, { $eq: ['$isGroup', false] }],
							},
							then: {
								$cond: {
									if: { $eq: ['$creator._id', user._id] },
									then: { $ifNull: ['$user.name', '$creator.name'] },
									else: '$creator.name',
								},
							},
							else: '$name',
						},
					},
					photo: {
						$cond: {
							if: {
								$or: [{ $not: ['$isGroup'] }, { $eq: ['$isGroup', false] }],
							},
							then: {
								$cond: {
									if: { $eq: ['$creator._id', user._id] },
									then: { $ifNull: ['$user.photo', '$creator.photo'] },
									else: '$creator.photo',
								},
							},
							else: '$photo',
						},
					},
				},
			},

			// search with name
			...(search
				? [
					{
						$match: {
							name: { $regex: search, $options: 'i' },
						},
					},
				]
				: []),

			{
				$facet: {
					data: [
						// populate participants.user
						{
							$lookup: {
								from: 'users',
								let: { participantIds: '$participants.user' },
								pipeline: [
									{
										$match: {
											$expr: { $in: ['$_id', '$$participantIds'] }
										}
									},
									{
										$project: { name: 1, username: 1, photo: 1, isOnline: 1, lastOnlineAt: 1, isManuallyOffline: 1 }
									}
								],
								as: 'participantsUsers'
							}
						},
						{
							$addFields: {
								participants: {
									$map: {
										input: '$participants',
										as: 'p',
										in: {
											$mergeObjects: [
												'$$p',
												{
													user: {
														$arrayElemAt: [
															{
																$filter: {
																	input: '$participantsUsers',
																	as: 'u',
																	cond: { $eq: ['$$p.user', '$$u._id'] },
																},
															},
															0,
														],
													},
												},
											],
										},
									},
								},
							},
						},
						{ $project: { participantsUsers: 0 } },

						// populate unread count
						{
							$lookup: {
								from: 'messages',
								let: {
									chatId: '$_id',
									lastReadAt: {
										$let: {
											vars: {
												currentParticipant: {
													$arrayElemAt: [
														{
															$filter: {
																input: '$participants',
																as: 'p',
																cond: { $eq: ['$$p.user._id', user._id] }
															}
														},
														0
													]
												}
											},
											in: '$$currentParticipant.lastReadAt'
										}
									}
								},
								pipeline: [
									{
										$match: {
											$expr: {
												$and: [
													{ $eq: ['$chat', '$$chatId'] },
													{ $gt: ['$at', '$$lastReadAt'] }
												]
											}
										}
									},
									{ $count: 'count' }
								],
								as: 'unreadArray'
							}
						},
						{
							$addFields: {
								unread: {
									$ifNull: [{ $arrayElemAt: ['$unreadArray.count', 0] }, 0]
								}
							}
						},
						{ $project: { unreadArray: 0 } },

						// populate lastMessage
						{
							$lookup: {
								from: 'messages',
								localField: 'lastMessage',
								foreignField: '_id',
								as: 'lastMessage',
							},
						},
						{
							$unwind: {
								path: '$lastMessage',
								preserveNullAndEmptyArrays: true,
							},
						},

						// populate lastMessage.user
						{
							$lookup: {
								from: 'users',
								let: { userId: '$lastMessage.user' },
								pipeline: [
									{ $match: { $expr: { $eq: ['$_id', '$$userId'] } } },
									{ $project: { name: 1, username: 1, photo: 1 } }
								],
								as: 'lastMessage.user'
							}
						},
						{
							$unwind: {
								path: '$lastMessage.user',
								preserveNullAndEmptyArrays: true,
							},
						},
						{
							$replaceRoot: {
								newRoot: {
									$mergeObjects: [
										'$$ROOT',
										{
											lastMessage: {
												$cond: [{ $eq: ['$lastMessage', {}] }, null, '$lastMessage']
											}
										}
									]
								}
							}
						},

						// pagination
						{ $sort: { updatedAt: -1 } },
						{ $skip: skip },
						{ $limit: limit },
					],
					count: [{ $count: 'total' }],
				},
			},
		]

		const result = await Chat.aggregate(pipeline)

		const chats = result[0]?.data || []
		const count = result[0]?.count[0]?.total || 0

		for (const chat of chats) {
			chat.participantsCount = chat.participants.length
			for (const p of chat.participants) {
				const participant = p.user
				if(chat.isGroup && `${participant._id}` === `${user._id}`) {
					chat.isJoined = true
					if(p.isAdmin) {
						chat.areYouAdmin = true
					}
				}
				if (participant.isManuallyOffline) {
					participant.isOnline = false
				} else if (!participant.isOnline && participant.lastOnlineAt) {
					participant.lastOnlineAt.setMinutes(participant.lastOnlineAt.getMinutes() + constants.maxOnlineVisibilityInMinutes)
					if (participant.lastOnlineAt >= new Date()) {
						participant.isOnline = true
					}
				}
				delete participant.lastOnlineAt
				delete participant.isManuallyOffline
			}
			if (chat.user) {
				const p = chat.participants.find((e: IChatParticipant) => `${e.user._id}` === `${chat.user._id}`)
				chat.user.isOnline = p?.user?.isOnline || false
			}
			const p = chat.participants.find((e: IChatParticipant) => `${e.user._id}` === `${chat.creator._id}`)
			chat.creator.isOnline = p?.user?.isOnline || false
		}

		return res.json({ chats, count })
	} catch (error) {
		console.error(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function getExploreChats(req: Request, res: Response) {
	interface Payload {
		limit?: string | number
		page?: string | number
		search?: string
		category?: string
		reelsLimit?: string | number
	}
	try {
		let {
			limit = '10',
			page = '1',
			search = '',
			category,
			reelsLimit = '5'
		} = req.query as unknown as Payload
		limit = +limit
		page = +page
		reelsLimit = +reelsLimit
		const skip = (page - 1) * limit
		const user = res.locals.user as IUser

		const pipeline: any = [
			// match
			{
				$match: {
					isGroup: true,
					...(search ? { name: { $regex: search, $options: 'i' } } : {}),
					...(category ? { category } : {}),
				},
			},

			// participants count
			{
				$addFields: {
					participantsCount: { $size: '$participants' },
				},
			},

			// lookup top N reels just to count them (not full reels yet)
			{
				$lookup: {
					from: 'reels',
					let: { chatId: '$_id' },
					pipeline: [
						{ $match: { $expr: { $eq: ['$chat', '$$chatId'] } } },
						{ $count: 'reelsCount' }
					],
					as: 'reelsCount'
				}
			},
			{ $addFields: { reelsCount: { $arrayElemAt: ['$reelsCount.reelsCount', 0] } } },

			// filter chats with at least 1 reel
			// { $match: { reelsCount: { $gt: 0 } } },

			{
				$facet: {
					data: [
						// pagination
						{ $sort: { participantsCount: -1 } },
						{ $skip: skip },
						{ $limit: limit },
						{ $project: { participantsCount: 0 } },

						// populate creator
						{
							$lookup: {
								from: 'users',
								let: { creatorId: '$creator' },
								pipeline: [
									{ $match: { $expr: { $eq: ['$_id', '$$creatorId'] } } },
									{ $project: { name: 1, username: 1, photo: 1 } }
								],
								as: 'creator'
							}
						},
						{ $unwind: '$creator' },

						// populate request
						{
							$lookup: {
								from: 'requests',
								let: { chatId: '$_id', userId: user._id },
								pipeline: [
									{
										$match: {
											$expr: {
												$and: [
													{ $eq: ['$chat', '$$chatId'] },
													{ $eq: ['$user', '$$userId'] },
													{ $eq: ['$status', 'pending'] },
												]
											}
										}
									},
									{ $limit: 1 }
								],
								as: 'request'
							}
						},
						{ $unwind: { path: '$request', preserveNullAndEmptyArrays: true } },

						// populate top N reels
						{
							$lookup: {
								from: 'reels',
								let: { chatId: '$_id', currentUserId: user._id },
								pipeline: [
									{ $match: { $expr: { $eq: ['$chat', '$$chatId'] } } },
									{ $sort: { views: -1 } },
									{ $limit: reelsLimit },

									// populate reel owner
									{
										$lookup: {
											from: 'users',
											let: { userId: '$user' },
											pipeline: [
												{ $match: { $expr: { $eq: ['$_id', '$$userId'] } } },
												{ $project: { name: 1, username: 1, photo: 1 } }
											],
											as: 'user'
										}
									},
									{ $unwind: { path: '$user', preserveNullAndEmptyArrays: true } },

									// isLiked
									{
										$lookup: {
											from: 'likes',
											let: { reelId: '$_id', userId: '$$currentUserId' },
											pipeline: [
												{
													$match: {
														$expr: {
															$and: [
																{ $eq: ['$reel', '$$reelId'] },
																{ $eq: ['$user', '$$userId'] }
															]
														}
													}
												},
												{ $limit: 1 }
											],
											as: 'liked'
										}
									},
									// isSuperLiked
									{
										$lookup: {
											from: 'super-likes',
											let: { reelId: '$_id', userId: '$$currentUserId' },
											pipeline: [
												{
													$match: {
														$expr: {
															$and: [
																{ $eq: ['$reel', '$$reelId'] },
																{ $eq: ['$user', '$$userId'] }
															]
														}
													}
												},
												{ $limit: 1 }
											],
											as: 'superLiked'
										}
									},
									// isFavorite
									{
										$lookup: {
											from: 'favorites',
											let: { reelId: '$_id', userId: '$$currentUserId' },
											pipeline: [
												{
													$match: {
														$expr: {
															$and: [
																{ $eq: ['$reel', '$$reelId'] },
																{ $eq: ['$user', '$$userId'] }
															]
														}
													}
												},
												{ $limit: 1 }
											],
											as: 'favorite'
										}
									},
									// user.isFollowed
									{
										$lookup: {
											from: 'followers',
											let: { currentUserId: '$$currentUserId', reelOwnerId: '$user._id' },
											pipeline: [
												{
													$match: {
														$expr: {
															$and: [
																{ $eq: ['$follower', '$$currentUserId'] },
																{ $eq: ['$following', '$$reelOwnerId'] }
															]
														}
													}
												},
												{ $limit: 1 }
											],
											as: 'follow'
										}
									},
									{
										$addFields: {
											isLiked: { $gt: [{ $size: '$liked' }, 0] },
											isSuperLiked: { $gt: [{ $size: '$superLiked' }, 0] },
											isFavorite: { $gt: [{ $size: '$favorite' }, 0] },
											'user.isFollowed': { $gt: [{ $size: '$follow' }, 0] }
										}
									},
									{
										$project: reelControllers.reelFieldsToProject
									}
								],
								as: 'reels'
							}
						}
					],
					count: [{ $count: 'total' }],
				},
			},
		]

		const result = await Chat.aggregate(pipeline)
		const chats = result[0]?.data || []
		const count = result[0]?.count[0]?.total || 0

		for (const chat of chats) {
			for (const p of chat.participants) {
				if(chat.isGroup && `${p.user}` === `${user._id}`) {
					chat.isJoined = true
					if(p.isAdmin) {
						chat.areYouAdmin = true
					}
				}
			}
			chat.participantsCount = chat.participants.length
		}

		return res.json({ chats, count })
	} catch (error) {
		console.error(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function getChat(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const id = new Types.ObjectId(req.params.id)

		const pipeline: any = [
			// match
			{
				$match: {
					'participants.user': user._id,
					_id: id,
				},
			},

			// populate creator
			{
				$lookup: {
					from: 'users',
					let: { creatorId: '$creator' },
					pipeline: [
						{ $match: { $expr: { $eq: ['$_id', '$$creatorId'] } } },
						{ $project: { name: 1, username: 1, photo: 1 } }
					],
					as: 'creator'
				}
			},
			{ $unwind: '$creator' },

			// populate user (optional)
			{
				$lookup: {
					from: 'users',
					let: { userId: '$user' },
					pipeline: [
						{ $match: { $expr: { $eq: ['$_id', '$$userId'] } } },
						{ $project: { name: 1, username: 1, photo: 1 } }
					],
					as: 'user'
				}
			},
			{ $unwind: { path: '$user', preserveNullAndEmptyArrays: true } },

			// dynamic computation: name and photo
			{
				$addFields: {
					name: {
						$cond: {
							if: {
								$or: [{ $not: ['$isGroup'] }, { $eq: ['$isGroup', false] }],
							},
							then: {
								$cond: {
									if: { $eq: ['$creator._id', user._id] },
									then: { $ifNull: ['$user.name', '$creator.name'] },
									else: '$creator.name',
								},
							},
							else: '$name',
						},
					},
					photo: {
						$cond: {
							if: {
								$or: [{ $not: ['$isGroup'] }, { $eq: ['$isGroup', false] }],
							},
							then: {
								$cond: {
									if: { $eq: ['$creator._id', user._id] },
									then: { $ifNull: ['$user.photo', '$creator.photo'] },
									else: '$creator.photo',
								},
							},
							else: '$photo',
						},
					},
				},
			},

			// populate participants.user
			{
				$lookup: {
					from: 'users',
					let: { participantIds: '$participants.user' },
					pipeline: [
						{
							$match: {
								$expr: { $in: ['$_id', '$$participantIds'] }
							}
						},
						{
							$project: { name: 1, username: 1, photo: 1, isOnline: 1, lastOnlineAt: 1, isManuallyOffline: 1 }
						}
					],
					as: 'participantsUsers'
				}
			},
			{
				$addFields: {
					participants: {
						$map: {
							input: '$participants',
							as: 'p',
							in: {
								$mergeObjects: [
									'$$p',
									{
										user: {
											$arrayElemAt: [
												{
													$filter: {
														input: '$participantsUsers',
														as: 'u',
														cond: { $eq: ['$$p.user', '$$u._id'] },
													},
												},
												0,
											],
										},
									},
								],
							},
						},
					},
				},
			},
			{ $project: { participantsUsers: 0 } },

			// populate unread count
			{
				$lookup: {
					from: 'messages',
					let: {
						chatId: '$_id',
						lastReadAt: {
							$let: {
								vars: {
									currentParticipant: {
										$arrayElemAt: [
											{
												$filter: {
													input: '$participants',
													as: 'p',
													cond: { $eq: ['$$p.user._id', user._id] }
												}
											},
											0
										]
									}
								},
								in: '$$currentParticipant.lastReadAt'
							}
						}
					},
					pipeline: [
						{
							$match: {
								$expr: {
									$and: [
										{ $eq: ['$chat', '$$chatId'] },
										{ $gt: ['$at', '$$lastReadAt'] }
									]
								}
							}
						},
						{ $count: 'count' }
					],
					as: 'unreadArray'
				}
			},
			{
				$addFields: {
					unread: {
						$ifNull: [{ $arrayElemAt: ['$unreadArray.count', 0] }, 0]
					}
				}
			},
			{ $project: { unreadArray: 0 } },

			// populate lastMessage
			{
				$lookup: {
					from: 'messages',
					localField: 'lastMessage',
					foreignField: '_id',
					as: 'lastMessage',
				},
			},
			{
				$unwind: {
					path: '$lastMessage',
					preserveNullAndEmptyArrays: true,
				},
			},

			// populate lastMessage.user
			{
				$lookup: {
					from: 'users',
					let: { userId: '$lastMessage.user' },
					pipeline: [
						{ $match: { $expr: { $eq: ['$_id', '$$userId'] } } },
						{ $project: { name: 1, username: 1, photo: 1 } }
					],
					as: 'lastMessage.user'
				}
			},
			{
				$unwind: {
					path: '$lastMessage.user',
					preserveNullAndEmptyArrays: true,
				},
			},
			{
				$replaceRoot: {
					newRoot: {
						$mergeObjects: [
							'$$ROOT',
							{
								lastMessage: {
									$cond: [{ $eq: ['$lastMessage', {}] }, null, '$lastMessage']
								}
							}
						]
					}
				}
			},
		]

		const result = await Chat.aggregate(pipeline)
		const chat = result[0]
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		chat.participantsCount = chat.participants.length
		for (const p of chat.participants) {
			const participant = p.user
			if(chat.isGroup && `${participant._id}` === `${user._id}` && p.isAdmin) {
				chat.areYouAdmin = true
			}
			if (participant.isManuallyOffline) {
				participant.isOnline = false
			} else if (!participant.isOnline && participant.lastOnlineAt) {
				participant.lastOnlineAt.setMinutes(participant.lastOnlineAt.getMinutes() + constants.maxOnlineVisibilityInMinutes)
				if (participant.lastOnlineAt >= new Date()) {
					participant.isOnline = true
				}
			}
			delete participant.lastOnlineAt
			delete participant.isManuallyOffline
		}
		if (chat.user) {
			const p = chat.participants.find((e: IChatParticipant) => `${e.user._id}` === `${chat.user._id}`)
			chat.user.isOnline = p?.user?.isOnline || false
		}
		const p = chat.participants.find((e: IChatParticipant) => `${e.user._id}` === `${chat.creator._id}`)
		chat.creator.isOnline = p?.user?.isOnline || false

		return res.json({ chat })
	} catch (error) {
		console.error(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function getChatFiles(req: Request, res: Response) {
	interface Payload {
		limit?: string | number
		page?: string | number
		tag?: string
		type?: 'video' | 'audio' | 'image' | 'document' | 'other'
	}
	try {
		const user = res.locals.user as IUser
		let {
			limit = '10',
			page = '1',
			tag = 'others',
			type = 'video',
		} = req.query as unknown as Payload
		limit = +limit
		page = +page
		const skip = (page - 1) * limit
		const id = new Types.ObjectId(req.params.id)

		const chat = await Chat.findById(id)
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		const participant = chat.participants.find(e => `${e.user}` === `${user._id}`)
		if (!participant) {
			return res.status(404).json({ message: 'permission denied' })
		}

		const extFilters: Record<string, RegExp> = {
			video: /\.(mp4|mkv|mov|avi|webm)$/i,
			audio: /\.(mp3|wav|aac|ogg|flac)$/i,
			image: /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i,
			document: /\.(pdf|html?|docx?|xlsx?|pptx?|txt)$/i,
			other: /\.(mp4|mkv|mov|avi|webm|mp3|wav|aac|ogg|flac|jpg|jpeg|png|gif|webp|bmp|svg|pdf|html?|docx?|xlsx?|pptx?|txt)$/i,
		}

		let matchStage: any = { 'files.url': { $regex: extFilters[type] } }
		if (type === 'other') {
			matchStage = {
				'files.url': {
					$not: {
						$regex: extFilters['other'],
					},
				},
			}
		}

		const pipeline: any = [
			{
				$match: {
					files: { $exists: true },
					chat: chat._id,
					tag: tag === 'others' ? { $exists: false } : tag
				},
			},
			{ $unwind: '$files' },
			{
				$match: matchStage
			},
			{
				$sort: { at: -1 }
			},
			{
				$facet: {
					data: [
						{ $skip: skip },
						{ $limit: limit }
					],
					totalCount: [
						{ $count: 'count' }
					],
				},
			},
		]

		const result = await Message.aggregate(pipeline)
		const files = result[0]?.data || []
		const count = result[0]?.totalCount[0]?.count || 0

		for (const file of files) {
			file.message = file._id
			file._id = file.files._id
			file.file = file.files
			delete file.files
		}

		return res.json({ files, count })
	} catch (error) {
		console.error(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function postChat(req: Request, res: Response) {
	interface Payload {
		user?: string
		meetMe?: string
		isGroup?: boolean
		category?: string
		name?: string
		participants?: string[]
		restricted?: boolean
		socket?: string
	}
	try {
		const user = res.locals.user as IUser
		let {
			user: userId,
			meetMe,
			isGroup,
			category,
			name,
			participants,
			restricted,
			socket: socketId
		} = req.body as Payload
		let connection: IConnection | null = null

		if (!isGroup) {
			name = undefined
			participants = undefined
			restricted = undefined
			category = undefined

			// if userId is same as current user
			if (userId && userId === `${user._id}`) {
				return res
					.status(403)
					.json({ message: 'Can not create chat with yourself' })
			}

			// if one-to-one chat already exist
			const chat = await Chat.findOne({
				$and: [
					{
						$or: [
							{ creator: user._id, user: userId },
							{ creator: userId, user: user._id },
						],
					},
					{
						$or: [{ isGroup: false }, { isGroup: { $exists: false } }],
					},
				],
			})
			if (chat) {
				return res.status(200).json({ message: 'chat already exist', chat })
			}

			// checking meetMe
			if (meetMe) {
				const status = await Status.findOne({ _id: meetMe, user: userId })
				if (!status) {
					return res.status(404).json({ message: 'status not found' })
				}
				connection = await Connection.findOne({
					$or: [
						{ from: user._id, to: userId },
						{ from: userId, to: user._id },
					],
					// statusDetail: meetMe,
				})
				if (!connection) {
					return res.status(404).json({ message: 'connection not found' })
				}
				if (connection.status !== 'accepted') {
					return res
						.status(403)
						.json({ message: 'connection is not accepted yet' })
				}
			}
		} else {
			userId = undefined
			meetMe = undefined
		}

		if (category === 'Courses') {
			restricted = false
		}

		// removing duplicates
		participants = [...(participants ?? []), `${user._id}`, ...(userId ? [userId] : [])]
		participants = [...new Set(participants)]

		// checking if participants are in friend circle
		const remainings = await checkIfUsersInFriendCircle(participants, user)
		if (remainings.length) {
			return res.status(403).json({
				message:
					(remainings.length === 1 ? 'a user' : 'some users') +
					' does not belong to either followings list or connections',
				remainings,
			})
		}

		// prevent to create one-to-one chat if blocked
		if(userId) {
			const isBlocked = await services.isUserBlocked(user._id, new Types.ObjectId(userId))
			if(isBlocked) {
				return res.status(403).json({message: "Can't create chat when its blocked"})
			}
		}

		const chat = await Chat.create({
			creator: user._id,
			user: userId,
			isGroup,
			category,
			meetMe,
			name,
			restricted: restricted || undefined,
			participants: participants.map((e) => ({
				user: e,
				isAdmin: `${user._id}` === e || undefined
			})),
		});

		// socket notify
		(async () => {
			try {
				// notify other participants with event "chat-created"
				const participantIds = chat.participants.filter(e => `${e.user}` !== `${user._id}`).map(e => `${e.user}`)
				const payload = {
					chat: `${chat._id}`
				}
				for (const participantId of participantIds) {
					services.socketNotifyUser({
						userId: participantId,
						event: 'chat-created',
						data: payload
					})
				}
				// notify current user's other sockets(if socket available) with event "message-sent"
				services.socketNotifyUser({
					userId: `${user._id}`,
					event: 'chat-created',
					data: payload,
					currentSocket: socketId,
					socketType: 'others'
				})
				if (connection) {
					connection.chat = chat._id
					await connection.save()
				}
			} catch (error) {
				console.log(error)
			}
		})();

		// push notify
		(async () => {
			try {
				// notify other participants with event "chat-created"
				if(!isGroup) {
					return
				}
				const participantIds = chat.participants.filter(e => `${e.user}` !== `${user._id}`).map(e => `${e.user}`)
				const participants = await User.find({_id: {$in: participantIds}})
				for (const participant of participants) {
					services.notifyUser({
						title: 'You\'ve joined in a new community',
						description: `${user.username} added you to a new community \"${name}\"`,
						event: 'group-chat-created',
						user: participant as unknown as IUser,
						from: user._id,
						chat: chat._id
					})
				}
			} catch (error) {
				console.log(error)
			}
		})()

		return res.status(201).json({ chat })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function postChatParticipants(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const chatId = req.params.id
		let participantIds = req.body.participants as string[]

		const chat = await Chat.findById(chatId)
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		if (!chat.isGroup) {
			return res.status(403).json({ message: 'chat must be a group chat' })
		}
		let admin = chat.participants.find(
			(e) => `${e.user}` === `${user._id}`
		)
		if (!admin) {
			return res.status(403).json({ message: 'you must belong to this chat first' })
		}
		if (!admin.isAdmin) {
			return res.status(403).json({ message: 'you need admin permission first' })
		}

		participantIds = participantIds.filter(pid => chat.participants.every(e => pid !== `${e.user}`))
		participantIds = [...new Set(participantIds)]
		const remainings = await checkIfUsersInFriendCircle(participantIds, user)
		if (remainings.length) {
			return res.status(403).json({
				message:
					(remainings.length === 1 ? 'a user' : 'some users') +
					' does not belong to either followings list or connections',
				remainings,
			})
		}

		chat.participants.push(...participantIds.map(e => ({ user: e })))
		await chat.save()

		// socket notify all participants with event "chat-joined"
		for (const pid of participantIds) {
			services.socketNotifyUser({
				userId: pid,
				event: 'chat-joined',
				data: {
					chat: `${chat._id}`,
					admin: {
						_id: `${user._id}`,
						name: user.name,
						photo: user.photo,
						username: user.username,
					}
				}
			})
		}

		// fcm notify
		(async () => {
			try {
				const participants = await User.find({ _id: { $in: participantIds } })
				for (const participantId of participantIds) {
					const participant = participants.find(e => `${e._id}` === participantId)
					if (!participant) {
						continue
					}
					await services.notifyUser({
						title: 'You\'ve joined to a new chat',
						description: `You are now a member of ${chat.name || 'a group chat'}`,
						event: 'chat-joined',
						user: participant as IUser,
						from: user._id,
						chat: chat._id,
					})
				}
			} catch (error) {
				console.log(error)
			}
		})()

		const participants = chat.participants.filter(e => participantIds.includes(`${e.user}`))

		return res.status(201).json({ participants })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function putChat(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const id = req.params.id as string
		const photo = (req.files as IFiles)?.photo?.[0]
		const name = req.body.name as string | undefined
		const category = req.body.category as string | undefined
		let restricted = req.body.restricted as 'true' | 'false' | undefined
		const allowPublicPost = req.body.allowPublicPost as 'true' | 'false' | undefined

		if (category === 'Courses') {
			restricted = 'false'
		}

		const chat = await Chat.findById(id)
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		const userParticipant = chat.participants.find(e => `${e.user}` === `${user._id}`)
		if (!userParticipant) {
			return res
				.status(404)
				.json({ message: 'chat not found' })
		}
		if (!userParticipant.isAdmin) {
			return res
				.status(403)
				.json({ message: 'only group admin can change the details' })
		}
		if (!chat.isGroup) {
			return res
				.status(403)
				.json({ message: 'modifications is only allowed in group chat' })
		}

		if (!name && !photo && !category && !restricted && !allowPublicPost) {
			return res.json({ chat })
		}

		if (photo) {
			const path = `chats/${chat._id}/${photo.filename}`
			if (chat.photo) {
				await helpers.deleteR2File(chat.photo)
			}
			chat.photo = await helpers.uploadFile(photo, path)
			helpers.deleteFile(photo)
		}
		chat.name = name ?? chat.name
		if (restricted) {
			chat.restricted = restricted === 'true' || undefined
		}
		if (allowPublicPost) {
			chat.allowPublicPost = allowPublicPost === 'true' || undefined
		}
		chat.category = category ?? chat.category
		await chat.save()

		return res.json({ chat })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function postChatRead(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const id = req.params.id as string

		const chat = await Chat.findById(id).populate('lastMessage')
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		const participant = chat.participants.find(
			(e) => `${e.user}` === `${user._id}`
		)
		if (!participant) {
			return res.status(403).json({ message: 'permission denied' })
		}

		participant.lastReadAt = new Date()
		await chat.save({timestamps: false});

		(async () => {
			try {
				for (const userId of chat.participants.map(e => `${e.user}`)) {
					const payload = {
						user: {
							_id: `${user._id}`,
							name: user.name,
							username: user.username,
							photo: user.photo,
						},
						chat: `${chat._id}`,
						lastReadAt: participant.lastReadAt.toISOString(),
						lastMessage: chat.lastMessage ? JSON.parse(JSON.stringify((chat.lastMessage as IMessage).toObject())) : undefined
					}
					services.socketNotifyUser({
						userId,
						event: 'messages-read',
						data: payload
					})
				}
			} catch (error) {
				console.log(error)
			}
		})()

		return res.json({ message: 'messages marked as read' })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function postChatParticipant(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const id = req.params.id as string
		const socketId = req.body.socket as string | undefined

		const chat = await Chat.findById(id).populate('event')
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		if (!chat.isGroup) {
			return res.status(403).json({ message: 'chat must be a group chat' })
		}
		if (chat.restricted) {
			return res.status(403).json({ message: 'chat group must not be restricted' })
		}
		let participant = chat.participants.find(
			(e) => `${e.user}` === `${user._id}`
		)
		if (participant) {
			return res.status(409).json({ message: 'participant already exist' })
		}

		chat.participants.push({ user: user._id })
		await chat.save()

		// socket notify current user's other sockets(if socket available) with event "chat-joined"
		services.socketNotifyUser({
			userId: `${user._id}`,
			event: 'chat-joined',
			data: {
				chat: `${chat._id}`
			},
			currentSocket: socketId,
			socketType: 'others'
		});

		;(async () => {
			try {
				if (!chat.event) {
					return
				}
				const event = chat.event as unknown as IEvent
				const adminIds = chat.participants.filter(e => e.isAdmin).map(e => e.user)
				const admins = await User.find({ _id: { $in: adminIds } })
				for (const admin of admins) {
					await services.notifyUser({
						title: `A user joined to your event`,
						description: `${user.username} have participated in an event`,
						event: 'event-joined',
						user: admin as IUser,
						from: user._id,
						chat: chat._id,
						meetMeEvent: event._id
					})
				}
			} catch (error) {
				console.log(error)
			}
		})()

		;(async () => {
			try {
				const score = await services.getOrCreateTodayScore(user._id)
				if(!score) {
					return
				}
				const action = score.social.actions.find(e => e.type === 'join-group-chat')
				if(!action || action.completed) {
					return
				}
				action.completed = true
				action.value = 1
				await services.updateScore(score as unknown as IScore)
			} catch (error) {
				console.log(error)
			}
		})()

		participant = chat.participants.find((e) => `${e.user}` === `${user._id}`)

		return res.status(201).json({ participant })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function postChatParticipantStatus(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const { id, participantId } = req.params
		const status = req.body.status as 'accepted' | 'rejected'

		const chat = await Chat.findOne({ _id: id, 'participants.user': user._id }).populate('event')
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		if (!chat.isGroup) {
			return res.status(403).json({ message: 'only allowed in group chat' })
		}
		const userParticipant = chat.participants.find(
			(e) => `${e.user}` === `${user._id}`
		)!
		if (!userParticipant.isAdmin) {
			return res.status(403).json({ message: 'only admin can manage joining' })
		}
		const participant = chat.participants.find(
			(e) => `${e.user}` === participantId
		)
		if (!participant) {
			return res.status(404).json({ message: 'chat participant not found' })
		}
		const event = chat.event as unknown as IEvent
		if (!event) {
			return res.status(404).json({ message: 'event not found' })
		}
		if (event.endedAt < new Date()) {
			return res.status(403).json({ message: 'event must be active' })
		}

		// if (participant.status) {
		// 	return res.status(409).json({ message: `participant is already an ${participant.status}` })
		// }

		participant.status = status
		await chat.save();

		(async () => {
			try {
				// notify the user
				const participant = await User.findById(participantId)
				if (!participant) {
					return
				}
				await services.notifyUser({
					title: 'You got event access',
					description: `You are ${status} into ${chat.name}`,
					event: status === 'accepted' ? 'event-access-granted' : 'event-access-rejected',
					user: participant as IUser,
					from: user._id,
					chat: chat._id,
					meetMeEvent: event._id,
				})
			} catch (error) {
				console.log(error)
			}
		})()

		return res.json({ message: 'permission status updated successfully' })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function deleteChatParticipant(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const { id, participantId } = req.params
		const socketId = req.body.socket as string | undefined

		const chat = await Chat.findOne({ _id: id, 'participants.user': user._id })
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		if (!chat.isGroup) {
			return res.status(403).json({ message: 'only allowed in group chat' })
		}
		const userParticipant = chat.participants.find(
			(e) => `${e.user}` === `${user._id}`
		)!
		const participant = chat.participants.find(
			(e) => `${e.user}` === participantId
		)
		if (!participant) {
			return res.status(404).json({ message: 'chat participant not found' })
		}

		const removingItself = `${user._id}` === participantId
		if (!removingItself && !userParticipant.isAdmin) {
			return res.status(403).json({ message: 'you must be admin in order to remove other participant' })
		}

		const adminCount = chat.participants.reduce((acc, e) => e.isAdmin ? acc + 1 : acc, 0)
		if (adminCount === 1 && participant.isAdmin) {
			return res.status(403).json({ message: 'atleast one admin should be available even after removal' })
		}

		// @ts-ignore
		chat.participants = chat.participants.filter(e => `${e._id}` !== `${participant._id}`)
		await chat.save();

		(async () => {
			try {
				// notify the user
				if (removingItself) {
					return
				}
				const participant = await User.findById(participantId)
				if (!participant) {
					return
				}
				await services.notifyUser({
					title: 'You are now removed from a chat',
					description: `You are now removed from ${chat.name || 'a chat'}`,
					event: 'chat-access-removed',
					user: participant as IUser,
					from: user._id,
					chat: chat._id,
				})
			} catch (error) {
				console.log(error)
			}
		})();

		(async () => {
			try {
				// socket notify other participants user
				if (removingItself) {
					for (const p of chat.participants.map(e => `${e.user}`)) {
						services.socketNotifyUser({
							userId: p,
							event: 'chat-participant-removed',
							data: {
								chat: `${chat._id}`,
								user: `${user._id}`
							},
						})
					}
					return
				}
				// socket notify all other participants except user and that admin
				for (const p of chat.participants.map(e => `${e.user}`).filter(e => e !== `${user._id}`)) {
					services.socketNotifyUser({
						userId: p,
						event: 'chat-participant-removed',
						data: {
							chat: `${chat._id}`,
							user: participantId
						},
					})
				}
				// socket notify current admin but except his own socket (if socketId exist)
				services.socketNotifyUser({
					userId: `${user._id}`,
					event: 'chat-participant-removed',
					data: {
						chat: `${chat._id}`,
						user: participantId
					},
					currentSocket: socketId,
					socketType: 'others'
				})

				// notify current participant
				const participant = await User.findById(participantId)
				if (!participant) {
					return
				}
				await services.notifyUser({
					title: 'You are now removed from a chat',
					description: `You are now removed from ${chat.name || 'a chat'}`,
					event: 'chat-access-removed',
					user: participant as IUser,
					from: user._id,
					chat: chat._id,
				})
			} catch (error) {
				console.log(error)
			}
		})()

		return res.json({ message: `${removingItself ? 'you have been' : 'chat participant'} removed successfully` })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function postChatParticipantAdmin(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const { id, participantId } = req.params

		const chat = await Chat.findOne({ _id: id, 'participants.user': user._id })
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		if (!chat.isGroup) {
			return res.status(403).json({ message: 'only allowed in group chat' })
		}
		const userParticipant = chat.participants.find(
			(e) => `${e.user}` === `${user._id}`
		)!
		if (!userParticipant.isAdmin) {
			return res.status(403).json({ message: 'only admin can manage admins' })
		}
		const participant = chat.participants.find(
			(e) => `${e.user}` === participantId
		)
		if (!participant) {
			return res.status(404).json({ message: 'chat participant not found' })
		}

		if (participant.isAdmin) {
			return res.json({ message: 'participant is already an admin' })
		}

		participant.isAdmin = true
		await chat.save();

		(async () => {
			try {
				// notify the user
				const participant = await User.findById(participantId)
				if (!participant) {
					return
				}
				await services.notifyUser({
					title: 'You got admin access',
					description: `You are now the admin of ${chat.name || 'a chat'}`,
					event: 'chat-admin-access-granted',
					user: participant as IUser,
					from: user._id,
					chat: chat._id,
				})
			} catch (error) {
				console.log(error)
			}
		})()

		return res.json({ message: 'permission updated successfully' })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function deleteChatParticipantAdmin(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const { id, participantId } = req.params

		const chat = await Chat.findOne({ _id: id, 'participants.user': user._id })
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		if (!chat.isGroup) {
			return res.status(403).json({ message: 'only allowed in group chat' })
		}
		const userParticipant = chat.participants.find(
			(e) => `${e.user}` === `${user._id}`
		)!
		if (!userParticipant.isAdmin) {
			return res.status(403).json({ message: 'only admins can manage other admins' })
		}
		const participant = chat.participants.find(
			(e) => `${e.user}` === participantId
		)
		if (!participant) {
			return res.status(404).json({ message: 'chat participant not found' })
		}

		const removingItself = `${user._id}` === participantId
		if (removingItself && userParticipant.isAdmin) {
			return res.status(403).json({ message: 'You can\'t remove your own admin access' })
		}

		const adminCount = chat.participants.reduce((acc, e) => e.isAdmin ? acc + 1 : acc, 0)
		if (adminCount === 1 && participant.isAdmin) {
			return res.status(403).json({ message: 'atleast one admin should be available even after removal' })
		}

		participant.isAdmin = undefined
		await chat.save();

		(async () => {
			try {
				// notify the user
				const participant = await User.findById(participantId)
				if (!participant) {
					return
				}
				await services.notifyUser({
					title: 'Your admin access was revoked',
					description: `Your admin access are now removed from ${chat.name || 'a chat'}`,
					event: 'chat-admin-access-removed',
					user: participant as IUser,
					from: user._id,
					chat: chat._id,
				})
			} catch (error) {
				console.log(error)
			}
		})()

		return res.json({ message: 'admin access removed successfully' })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

async function checkIfUsersInFriendCircle(participants: string[], user: IUser) {
	const otherParticipants = participants.filter((e) => e !== `${user._id}`)
	const followings = (
		await Follower.find(
			{ follower: user._id, following: { $in: otherParticipants } },
			{ _id: 1, following: 1 }
		)
	).map((e) => `${e.following}`)
	let remainings = otherParticipants.filter((e) => !followings.includes(e))
	const connections = (
		await Connection.find(
			{
				$or: [
					{ from: user._id, to: { $in: remainings } },
					{ to: user._id, from: { $in: remainings } },
				],
			},
			{ from: 1, to: 1 }
		)
	).map((e) => ({ from: `${e.from}`, to: `${e.to}` }))
	remainings = remainings.filter((e) =>
		connections.every((c) => c.from !== e && c.to !== e)
	)
	return remainings
}

async function deleteChat(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const id = req.params.id
		const socketId = req.body.socket as string | undefined

		const chat = await Chat.findById(id)
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}

		const participant = chat.participants.find(e => `${e.user}` === `${user._id}` && e.isAdmin)
		if (!participant) {
			return res.status(403).json({ message: 'only admins of this chat can delete it' })
		}

		if (chat.event) {
			await Event.deleteOne({ _id: chat.event })
		}
		await Chat.deleteOne({ _id: chat._id });

		(async () => {
			try {
				if (chat.photo) {
					await helpers.deleteR2File(chat.photo)
				}
				await Message.deleteMany({ chat: chat._id })
				await helpers.deleteR2FilesWithPrefix(`chats/${chat._id}`)
				const payload = {
					user: `${user._id}`,
					chat: `${chat._id}`,
					event: chat.event?.toString()
				}
				services.socketNotifyUser({
					userId: `${user._id}`,
					event: 'chat-deleted',
					data: payload,
					currentSocket: socketId,
					socketType: 'others'
				})
				for (const participant of chat.participants.filter(e => `${e.user}` !== `${user._id}`)) {
					services.socketNotifyUser({
						userId: `${participant.user}`,
						event: 'chat-deleted',
						data: payload
					})
				}
			} catch (error) {
				console.log(error)
			}
		})()

		return res.json({ message: 'chat deleted successfully' })
	} catch (error) {
		console.log(error)
		return res.status(500).json({ message: 'server error' })
	}
}

const controllers = {
	getChats,
	getExploreChats,
	getChat,
	getChatFiles,
	postChat,
	putChat,
	postChatParticipants,
	postChatParticipantStatus,
	postChatRead,
	postChatParticipant,
	deleteChatParticipant,
	postChatParticipantAdmin,
	deleteChatParticipantAdmin,
	deleteChat
}

export default controllers
