import { Request, Response } from 'express'
import { IUser, User } from '../users/models'
import { Message } from './models'
import helpers from '../../utils/helpers'
import { Types } from 'mongoose'
import { Chat, IChat } from '../chats/models'
import services from '../../utils/services'
import reelController from '../reels/controllers'
import { IScore } from '../scores/models'
import { Reel } from '../reels/models'
import { Share } from '../shares/models'

async function getMessages(req: Request, res: Response) {
	interface Payload {
		limit?: string | number
		page?: string | number
	}

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

		const chat = await Chat.findById(chatId)
		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: 'user must belong to this chat' })
		}

		const query: any = { chat: chatId }
		const messages = await Message.find(query)
			.sort({ at: -1 })
			.skip(skip)
			.limit(limit)
			.populate({
				path: 'user',
				select: { name: 1, username: 1, bio: 1, photo: 1 },
			})
			.populate({
				path: 'reel',
				select: { ...reelController.reelFieldsToProject, user: 1 },
			})
		const count = await Message.countDocuments(query)

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

async function postMessage(req: Request, res: Response) {
	interface Payload {
		chat: string
		content?: string
		files?: { url: string, cover?: string, meta?: any }[]
		socket?: string
	}
	try {
		const user = res.locals.user as IUser
		const { chat: chatId, content, files, socket: socketId } = req.body as Payload

		const chat = await Chat.findById(chatId)
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		if (chat.blocked) {
			return res.status(403).json({ message: "Can't post new messages once its blocked" })
		}
		const participant = chat.participants.find(e => `${e.user}` === `${user._id}`)
		if (!participant) {
			return res.status(403).json({ message: 'user must belong to this chat' })
		}

		const message = await Message.create({
			user: user._id,
			chat: chat._id,
			content,
			files
		})

		participant.lastReadAt = new Date()
		// @ts-ignore
		chat.lastMessage = message._id
		await chat.save();

		(async () => {
			try {
				// notify other participants with push only notification
				const otherParticipants = chat.participants.filter(e => `${e.user}` !== `${user._id}`)
				const participants: IUser[] = await User.find({ _id: { $in: otherParticipants.map(e => e.user) } })
				const title = `New message from ${user.name ?? user.username ?? 'a user'}`
				const description = message.content ?? `${message.files?.length ?? 0} file${message.files?.length ?? 0 > 1 ? 's' : ''}`
				for (const participant of participants) {
					await services.pushNotifyUser({
						title,
						body: description,
						event: 'message-received',
						user: participant,
						data: {
							message: {
								_id: message._id,
								user: {
									_id: user._id,
									username: user.username,
									name: user.name,
									photo: user.photo,
								},
								isGroup: chat.isGroup,
								chat: message.chat,
								content: message.content,
								files: message.files,
								at: message.at,
							},
						}
					})
				}
			} catch (error) {
				console.log(error)
			}
		})();

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

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

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

async function postMessageShareReels(req: Request, res: Response) {
	interface Payload {
		chats: string[]
		reel: string
		socket?: string
	}
	interface MessageData {
		_id: Types.ObjectId
		user: Types.ObjectId
		chat: Types.ObjectId
		reel: Types.ObjectId
		at: number
	}
	interface ChatUpdateData {
		updateOne: {
			filter: { _id: Types.ObjectId }
			update: {
				$set: {
					lastMessage: Types.ObjectId
					'participants.$[p].lastReadAt': number
				}
			}
			arrayFilters: { 'p.user': Types.ObjectId }[]
		}
	}
	try {
		const user = res.locals.user as IUser
		const { chats: chatIds, reel: reelId, socket: socketId } = req.body as Payload

		const reel = await Reel.findById(reelId).populate('user')
		if (!reel) {
			return res.status(404).json({ message: 'vibe not found' })
		}

		const chats = await Chat.find({
			_id: { $in: chatIds },
			'participants.user': user._id
		})

		const messages: MessageData[] = []
		const chatUpdates: ChatUpdateData[] = []
		for (const chat of chats) {
			if (chat.blocked) {
				continue
			}
			const message: MessageData = {
				_id: new Types.ObjectId(),
				user: user._id,
				chat: chat._id,
				reel: reel._id,
				at: Date.now()
			}
			const chatUpdate = {
				updateOne: {
					filter: { _id: chat._id },
					update: {
						$set: {
							lastMessage: message._id,
							'participants.$[p].lastReadAt': message.at
						}
					},
					arrayFilters: [
						{
							'p.user': user._id
						}
					]
				}
			}
			messages.push(message)
			chatUpdates.push(chatUpdate)
		}

		await Promise.all([
			Message.insertMany(messages),
			Chat.bulkWrite(chatUpdates)
		]);

		// fcm notify
		(async () => {
			try {
				// notify other participants with push only notification
				let userIds: string[] = []
				for (const chat of chats) {
					const otherParticipants = chat.participants.filter(e => `${e.user}` !== `${user._id}`)
					userIds.push(...otherParticipants.map(e => `${e.user}`))
				}
				userIds = [...new Set(userIds)]
				const users = await User.find({ _id: { $in: userIds } })
				for (const chat of chats) {
					const messageData = messages.find(e => `${e.chat}` === `${chat._id}`)
					if (!messageData) {
						continue
					}
					const otherParticipants = chat.participants.filter(e => `${e.user}` !== `${user._id}`)
					const participants = users.filter(e => otherParticipants.some(o => `${o.user}` === `${e._id}`)) as IUser[]
					const title = `New message from ${user.name ?? user.username ?? 'a user'}`
					const description = `A vibe has been shared with you`
					for (const participant of participants) {
						services.pushNotifyUser({
							title,
							body: description,
							event: 'message-received',
							user: participant,
							data: {
								message: {
									_id: messageData._id,
									user: {
										_id: user._id,
										username: user.username,
										name: user.name,
										photo: user.photo,
									},
									isGroup: chat.isGroup,
									chat: messageData.chat,
									content: '',
									at: messageData.at,
								},
							}
						})
					}
				}
			} catch (error) {
				console.log(error)
			}
		})();

		// socket notify
		(async () => {
			try {
				// notify other participants with event "message-received"
				for (const chat of chats) {
					const messageData = messages.find(e => `${e.chat}` === `${chat._id}`)
					if (!messageData) {
						continue
					}
					const participantIds = chat.participants.filter(e => `${e.user}` !== `${user._id}`).map(e => `${e.user}`)
					const payload = {
						message: {
							_id: `${messageData._id}`,
							user: {
								_id: `${user._id}`,
								name: user.name,
								photo: user.photo,
								username: user.username
							},
							chat: `${chat._id}`,
							content: '',
							at: new Date(messageData.at).toISOString()
						}
					}
					for (const participantId of participantIds) {
						services.socketNotifyUser({
							userId: participantId,
							event: 'messages-received',
							data: payload
						})
					}
					// notify current user's other sockets(if socket available) with event "message-sent"
					services.socketNotifyUser({
						userId: `${user._id}`,
						event: 'messages-sent',
						data: payload,
						currentSocket: socketId,
						socketType: 'others'
					})
				}
			} catch (error) {
				console.log(error)
			}
		})();

		// update reels share count and create entries
		(async () => {
			try {
				const reelUser = reel.user as unknown as IUser

				await Share.create({
					user: user._id,
					reel: reel._id,
					chats: chats,
					count: chats.length
				})
				const count = await Share.countDocuments({ reel: reel._id })
				reel.shares = count
				await reel.save()

				if (`${reelUser._id}` !== `${user._id}`) {
					await services.notifyUser({
						title: 'Your vibe got shared',
						description: `${user.username} shared your vibe`,
						event: 'reel-shared',
						user: reelUser,
						from: user._id,
						reel: reel._id
					})
				}
			} catch (error) {
				console.log(error)
			}
		})()

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

async function patchMessage(req: Request, res: Response) {
	interface Payload {
		tag: string
	}
	try {
		const user = res.locals.user as IUser
		const id = req.params.id
		const { tag } = req.body as Payload

		const message = await Message.findById(id).populate('chat')
		if (!message) {
			return res.status(404).json({ message: 'message not found' })
		}
		const chat = message.chat as unknown as IChat
		if (!chat) {
			return res.status(404).json({ chat: 'chat not found' })
		}
		if (chat.blocked) {
			return res.status(403).json({ message: "Can't update the messages once its blocked" })
		}

		const participant = chat.participants.find(e => `${e.user}` === `${user._id}`)
		if (!participant) {
			return res.status(403).json({ message: 'permission denied' })
		}
		if (`${message.user}` !== `${user._id}` && !participant.isAdmin) {
			return res.status(403).json({ message: 'you must be chat admin or message creator' })
		}

		if (!message.files) {
			return res.status(404).json({ message: 'files not found' })
		}

		if (tag && chat.tags.every(e => e.tag !== tag)) {
			return res.status(404).json({ message: 'tag not found' })
		}

		message.tag = tag ?? undefined
		await message.save()

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

async function postPresignUrl(req: Request, res: Response) {
	try {
		const user = res.locals.user as IUser
		const fileNames = req.body.files as string[]
		const chatId = new Types.ObjectId(req.body.chat as string)

		const chat = await Chat.findById(chatId)
		if (!chat) {
			return res.status(404).json({ message: 'chat not found' })
		}
		if (chat.blocked) {
			return res.status(403).json({ message: "Can't post new messages once its blocked" })
		}
		const participant = chat.participants.find(e => `${e.user}` === `${user._id}`)
		if (!participant) {
			return res.status(403).json({ message: 'user must belong to this chat' })
		}

		const files = fileNames.map(e => {
			const extension = e.split('.').at(-1)
			e = e.split('.').slice(0, -1).join('.')
			let name = e
				.replace(/ /g, '-')
				.replace(/#/g, '-')
				.replace(/,/g, '-')
				.split('')
				.filter(c => /[A-Za-z0-9_\-.]/.test(c))
				.join('')
			while (name.includes('--')) {
				name = name.replace(/\-\-/g, '-')
			}
			name = name.length >= 10 ? name.slice(0, 10) : name
			if (name.endsWith('-')) {
				name = name.slice(0, -1)
			}
			name += '.' + extension
			const hash = Date.now().toString() + '-' + Math.random().toString().split('.')[1]
			const path = `chats/${chat._id}/${hash}-${name}`
			return { path, name: e + '.' + extension }
		})

		const urls: any[] = []
		for (const file of files) {
			const url = await helpers.generateSignedUrl(file.path)
			if (!url) {
				return res.status(500).json({ message: 'server error' })
			}
			urls.push({
				file: file.name,
				upload: url.upload,
				download: url.download,
			})
		}

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

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

		const message = await Message.findById(id).populate('chat')
		if (!message) {
			return res.status(404).json({ message: 'message not found' })
		}
		const chat = message.chat as unknown as IChat
		if (!chat) {
			return res.status(404).json({ chat: 'chat not found' })
		}
		if (chat.blocked) {
			return res.status(403).json({ message: "Can't delete older messages once its blocked" })
		}

		const participant = chat.participants.find(e => `${e.user}` === `${user._id}`)
		if (!participant) {
			return res.status(403).json({ message: 'permission denied' })
		}
		if (`${message.user}` !== `${user._id}` && !participant.isAdmin) {
			return res.status(403).json({ message: 'you must be chat admin or message creator' })
		}

		await message.deleteOne();

		(async () => {
			try {
				if (message.files) {
					for (const f of message.files) {
						await helpers.deleteR2File(f.url)
					}
				}
				if (`${chat.lastMessage}` === id) {
					const lastMessages = await Message.find({ chat: chat._id })
						.sort({ at: -1 })
						.limit(1)
					chat.lastMessage = lastMessages[0]?._id
					await chat.save()
				}
			} catch (error) {
				console.log(error)
			}
		})()

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

const controllers = {
	getMessages,
	postMessage,
	postMessageShareReels,
	patchMessage,
	postPresignUrl,
	deleteMessage
}

export default controllers
