import { IUser, User } from '../users/models'
import { Types } from 'mongoose'
import { ISmartMatch, SmartMatch } from './models'
import { Blocking } from '../blockings/models'
import { Community } from '../communities/models'
import helpers from '../../utils/helpers'
import { pushNotifyUser } from '../../utils/services'
import constants from '../../utils/constants'

const WEEKLY_WINDOW_DAYS = 7
const MAX_MATCHES_PER_WEEK = 12
const MIN_SCORE_THRESHOLD = 55
const MAX_PENDING_SUGGESTED = 3
const MAX_SUGGESTED_PER_DAY = 3
const MAX_SUGGESTED_PER_WEEK = 7
const EXPIRY_NOTIFY_WINDOW_MS = 24 * 60 * 60 * 1000
const RESET_SUGGESTED_AFTER_DAYS = 14

const ENERGY_SCORES: Record<string, number> = {
	'deep-deep': 30, 'deep-parallel': 18, 'deep-group': 0, 'deep-active': 8,
	'parallel-parallel': 30, 'parallel-group': 8, 'parallel-active': 8,
	'group-group': 30, 'group-active': 18,
	'active-active': 30,
}

const ENERGY_LABELS: Record<string, string> = {
	'deep-deep': 'Deep 1-on-1',
	'deep-parallel': 'Deep connection',
	'parallel-parallel': 'Parallel energy',
	'parallel-group': 'Group-friendly',
	'parallel-active': 'Active & social',
	'group-group': 'Group energy',
	'group-active': 'Group energy',
	'active-active': 'Activity-first',
	'deep-active': 'Casual exploration',
	'deep-group': 'Different energies',
}

const CONVERSATION_PROMPTS = [
	"What's something you're proud of that you don't get to talk about enough?",
	"What's the best advice someone gave you that actually stuck?",
	"What's something small that always makes your day better?",
	"What's something you changed your mind about recently?",
	"What's a topic you could talk about for hours without getting bored?",
	"What's something you wish more people knew about?",
	"What's the best meal you've had this month?",
	"If you had a completely free weekend with no obligations, what would you actually do?",
	"What's a skill you'd love to learn but haven't started yet?",
	"What are you working on right now that excites you?",
	"What's something you want to accomplish before the end of this year?",
	"What problem do you wish someone would just solve already?",
	"What's your go-to spot on campus when you need a reset?",
	"What's an event or experience at university that surprised you?",
	"What's the best way someone has introduced themselves to you?",
]

const ACTIVITY_ADJACENT: Record<string, string[]> = {
	'food': ['events'], 'study': ['active'],
	'active': ['study'], 'events': ['food'],
}

const INTENT_COMPATIBLE: Record<string, string[]> = {
	'expand': ['open'], 'open': ['expand'],
	'crew': ['accountability'], 'accountability': ['crew'],
}

interface SmartMatchScoreResult {
	total: number
	scores: { energy: number; values: number; activity: number; intent: number }
}

interface SmartMatchMetadata {
	clickReason: string
	sharedEnergy: string
	activityIdea: string
	conversationStarter: string
}

function generateMatchMetadata(userA: IUser, userB: IUser, scores: SmartMatchScoreResult['scores']): SmartMatchMetadata {
	const smA = userA.smartMatch!
	const smB = userB.smartMatch!

	// Determine shared energy label
	const eKey = [smA.energy, smB.energy].sort().join('-')
	const sharedEnergy = ENERGY_LABELS[eKey] ?? 'Compatible energy'

	// Map old activity values to statusCategories
	const activityMapping: Record<string, string> = {
		'food': 'food',
		'study': 'studying',
		'active': 'gym',
		'events': 'chat',
	}

	// Determine activity idea (prefer common activity or first user's activity, key not label)
	let activityIdea: string = 'open-to-anything'
	const userActivityKey = activityMapping[smA.activity] || 'open-to-anything'
	if (constants.statusCategories.includes(userActivityKey as any)) {
		activityIdea = userActivityKey
	}

	// Generate click reason based on scores
	const sharedValues = smA.values.filter(v => smB.values.includes(v))
	let clickReason = 'You both value the same things'
	if (sharedValues.length >= 2) {
		clickReason = `You both value ${sharedValues.join(' & ')}`
	} else if (sharedValues.length === 1) {
		clickReason = `You both value ${sharedValues[0]}`
	} else {
		// Fallback to energy match
		clickReason = `Same energy, both ${smA.energy} types`
	}

	// Select conversation prompt based on intent and values
	// Deep/parallel energy + high values overlap = reflective questions
	// Group/active energy = practical/experience questions
	let promptIndex = 0
	if (smA.intent === 'accountability' || smB.intent === 'accountability') {
		promptIndex = 9 // "What are you working on right now that excites you?"
	} else if (smA.intent === 'crew' || smB.intent === 'crew') {
		promptIndex = 13 // "What's an event or experience at university that surprised you?"
	} else if (smA.energy === 'deep' && smB.energy === 'deep') {
		promptIndex = 0 // "What's something you're proud of..."
	} else if (smA.energy === 'group' || smB.energy === 'group') {
		promptIndex = 12 // "What's your go-to spot on campus..."
	} else if (smA.activity === 'food' || smB.activity === 'food') {
		promptIndex = 6 // "What's the best meal you've had this month?"
	} else if (smA.activity === 'active' || smB.activity === 'active') {
		promptIndex = 8 // "What's a skill you'd love to learn..."
	} else if (sharedValues.length >= 2) {
		promptIndex = 4 // "What's a topic you could talk about for hours..."
	} else {
		promptIndex = 1 // "What's the best advice someone gave you..."
	}

	const conversationStarter = CONVERSATION_PROMPTS[promptIndex]

	return {
		clickReason,
		sharedEnergy,
		activityIdea,
		conversationStarter,
	}
}

/**
 * Generate a personalized introduction message for smart match based on common traits
 * Format: "Hey there! Please meet each other. You both value x & y."
 */
function generateSmartMatchIntroMessage(userA: IUser, userB: IUser): string {
	const smA = userA.smartMatch!
	const smB = userB.smartMatch!

	// Get shared values between both users
	const sharedValues = smA.values.filter(v => smB.values.includes(v))

	if (sharedValues.length >= 2) {
		// Multiple shared values: highlight them
		const traits = sharedValues.slice(0, 2).join(' & ')
		return `Hey there! 👋 Please meet each other. You both value ${traits}.`
	} else if (sharedValues.length === 1) {
		// Single shared value
		return `Hey there! 👋 Please meet each other. You both value ${sharedValues[0]}.`
	} else {
		// No shared values, use energy match
		const eKey = [smA.energy, smB.energy].sort().join('-')
		const sharedEnergy = ENERGY_LABELS[eKey] ?? 'compatible energy'
		return `Hey there! 👋 Please meet each other. You both have ${sharedEnergy}.`
	}
}

function getSmartMatchesScore(_smartMatch: ISmartMatch, userA: IUser, userB: IUser): SmartMatchScoreResult | null {
	try {
		const smA = userA.smartMatch!
		const smB = userB.smartMatch!

		// Radius filter: if either user picked 'field', they must share at least one course
		// if (smA.radius === 'field' || smB.radius === 'field') {
		// 	const coursesA = (userA.courses ?? []).map(c => `${c}`)
		// 	const coursesB = (userB.courses ?? []).map(c => `${c}`)
		// 	const sharedCourse = coursesA.some(c => coursesB.includes(c))
		// 	if (!sharedCourse) return null
		// }

		// Energy (max 30)
		const eKey = [smA.energy, smB.energy].sort().join('-')
		const energyPoints = ENERGY_SCORES[eKey] ?? 4

		// Values overlap (max 25)
		const sharedValues = smA.values.filter(v => smB.values.includes(v)).length
		const valuesPoints = sharedValues >= 2 ? 25 : sharedValues === 1 ? 15 : 5

		// Activity (max 15)
		let activityPoints = 3
		if (smA.activity === smB.activity) activityPoints = 15
		else if (ACTIVITY_ADJACENT[smA.activity]?.includes(smB.activity)) activityPoints = 9

		// Intent (max 15)
		let intentPoints = 3
		if (smA.intent === smB.intent) intentPoints = 15
		else if (INTENT_COMPATIBLE[smA.intent]?.includes(smB.intent)) intentPoints = 10

		// NOTE: for now radius point will always going to be 15 and no any calculation/computation should be done for it right now
		const radiusPoints = 15

		const w = (dim: keyof typeof smA.weights) =>
			(smA.weights?.[dim] ?? 1) * (smB.weights?.[dim] ?? 1)

		const weightedEnergy   = energyPoints   * w('energy')
		const weightedValues   = valuesPoints   * w('values')
		const weightedActivity = activityPoints * w('activity')
		const weightedIntent   = intentPoints   * w('intent')

		const total = Math.min(100, Math.max(0, Math.round(
			weightedEnergy + weightedValues + weightedActivity + weightedIntent + radiusPoints
		)))

		return {
			total,
			scores: {
				energy:   Math.round(weightedEnergy),
				values:   Math.round(weightedValues),
				activity: Math.round(weightedActivity),
				intent:   Math.round(weightedIntent),
			}
		}
	} catch (error) {
		helpers.saveErrorLogs((error as Error).stack ?? '')
		return null
	}
}

/**
 * Read public/vibeon.html to get more details, but not using firebase
 */
async function createSmartMatches() {
	await helpers.runInBackground(async () => {
		const startTime = Date.now()
		console.log(`[SmartMatch] Starting createSmartMatches job`)

		const communities = await Community.find()
		const users = await User.find(
			{ smartMatch: { $exists: true } },
			{ name: 1, username: 1, photo: 1, communities: 1, selectedCommunity: 1, smartMatch: 1, courses: 1, auth: 1 }
		)
		const blockings = await Blocking.find()
		console.log(`[SmartMatch] Loaded ${users.length} users, ${communities.length} communities, ${blockings.length} blockings`)

		// Build blocked pair set: "userId1:userId2" (sorted)
		const blockedPairs = new Set<string>()
		for (const b of blockings) {
			const key = [`${b.user}`, `${b.blockedBy}`].sort().join(':')
			blockedPairs.add(key)
		}

		// Load all existing non-expired matches to avoid re-creating a pair
		const existingMatches = await SmartMatch.find({ status: { $ne: 'expired' } }, { id: 1 })
		const existingPairs = new Set(existingMatches.map(m => m.id))

		// Load this week's match counts per user
		const weekStart = new Date()
		weekStart.setDate(weekStart.getDate() - WEEKLY_WINDOW_DAYS)
		const weeklyMatches = await SmartMatch.find({ createdAt: { $gte: weekStart } })
		const weeklyCount: Record<string, number> = {}
		for (const m of weeklyMatches) {
			const aId = `${m.userA.user}`
			const bId = `${m.userB.user}`
			weeklyCount[aId] = (weeklyCount[aId] ?? 0) + 1
			weeklyCount[bId] = (weeklyCount[bId] ?? 0) + 1
		}

		// Group users by community (use selectedCommunity)
		const usersByCommunity: Record<string, typeof users> = {}
		for (const user of users) {
			if (!user.selectedCommunity) continue
			const cId = `${user.selectedCommunity}`
			if (!usersByCommunity[cId]) usersByCommunity[cId] = []
			usersByCommunity[cId].push(user)
		}

		for (const community of communities) {
			const communityUsers = usersByCommunity[`${community._id}`] ?? []
			if (communityUsers.length < 2) continue

			const candidates: { a: IUser, b: IUser, score: number, scores: { energy: number, values: number, activity: number, intent: number } }[] = []

			for (let i = 0; i < communityUsers.length; i++) {
				for (let j = i + 1; j < communityUsers.length; j++) {
					const uA = communityUsers[i]
					const uB = communityUsers[j]

					const pairKey = [`${uA._id}`, `${uB._id}`].sort().join(':')

					// Create the match ID using sorted usernames to check against existing matches
					const usernameA = `${uA.username ?? uA._id}`
					const usernameB = `${uB.username ?? uB._id}`
					const [firstUsername, secondUsername] = [usernameA, usernameB].sort()
					const matchId = `${firstUsername}:${secondUsername}`

					if (blockedPairs.has(pairKey)) continue
					if (existingPairs.has(matchId)) continue
					if ((weeklyCount[`${uA._id}`] ?? 0) >= MAX_MATCHES_PER_WEEK) continue
					if ((weeklyCount[`${uB._id}`] ?? 0) >= MAX_MATCHES_PER_WEEK) continue

					const dummyMatch = { id: matchId } as ISmartMatch
					const result = getSmartMatchesScore(dummyMatch, uA, uB)
					if (result === null || result.total < MIN_SCORE_THRESHOLD) continue

					candidates.push({ a: uA, b: uB, score: result.total, scores: result.scores })
				}
			}

			// Sort by score descending, greedily assign top matches
			candidates.sort((x, y) => y.score - x.score)

			for (const { a, b, score, scores } of candidates) {
				if ((weeklyCount[`${a._id}`] ?? 0) >= MAX_MATCHES_PER_WEEK) continue
				if ((weeklyCount[`${b._id}`] ?? 0) >= MAX_MATCHES_PER_WEEK) continue

				const usernameA = `${a.username ?? a._id}`
				const usernameB = `${b.username ?? b._id}`
				const [firstUsername, secondUsername] = [usernameA, usernameB].sort()
				const matchId = `${firstUsername}:${secondUsername}`
				const [firstUser, secondUser] = firstUsername === usernameA ? [a, b] : [b, a]

				const metadata = generateMatchMetadata(firstUser, secondUser, scores)

				await SmartMatch.create({
					id: matchId,
					userA: { user: firstUser._id },
					userB: { user: secondUser._id },
					score,
					scores,
					community: community._id,
					clickReason: metadata.clickReason,
					sharedEnergy: metadata.sharedEnergy,
					activityIdea: metadata.activityIdea,
					conversationStarter: metadata.conversationStarter,
				})

				weeklyCount[`${a._id}`] = (weeklyCount[`${a._id}`] ?? 0) + 1
				weeklyCount[`${b._id}`] = (weeklyCount[`${b._id}`] ?? 0) + 1
			}
		}

		const elapsedMs = Date.now() - startTime
		console.log(`[SmartMatch] Finished createSmartMatches job in ${elapsedMs}ms`)
	})
}

async function notifyExpiringSmartMatches() {
	await helpers.runInBackground(async () => {
		console.log(`[SmartMatch] Starting notifyExpiringSmartMatches job`)
		const now = new Date()
		const oneDayFromNow = new Date(now.getTime() + EXPIRY_NOTIFY_WINDOW_MS)

		const expiringMatches = await SmartMatch.find({
			status: 'pending',
			suggested: true,
			expiredAt: { $gte: now, $lte: oneDayFromNow },
		})

		// Collect user IDs that haven't responded yet (status is pending so response can only be 'yes' or absent)
		const userMatchMap = new Map<string, { matchId: Types.ObjectId }>()
		for (const match of expiringMatches) {
			if (!match.userA.response) userMatchMap.set(`${match.userA.user}`, { matchId: match._id })
			if (!match.userB.response) userMatchMap.set(`${match.userB.user}`, { matchId: match._id })
		}

		const userIds = Array.from(userMatchMap.keys())
		console.log(`[SmartMatch] Found ${expiringMatches.length} expiring matches, notifying ${userIds.length} users`)
		if (!userIds.length) return

		const users = await User.find(
			{ _id: { $in: userIds } },
			{ name: 1, username: 1, auth: 1 }
		)

		let notifyCount = 0
		for (const user of users) {
			if (!user.auth?.loggedinDevices?.length) continue
			const { matchId } = userMatchMap.get(`${user._id}`)!
			await pushNotifyUser({
				user,
				title: 'Your Vibe Match is expiring soon! ✦',
				body: 'Your match expires in less than 24 hours. Don\'t miss out!',
				event: 'smart-match-expiring-soon',
				data: { smartMatch: matchId },
			})
			notifyCount++
		}
		console.log(`[SmartMatch] Finished notifyExpiringSmartMatches job - sent ${notifyCount} notifications`)
	})
}

async function resetSuggestedSmartMatches() {
	await helpers.runInBackground(async () => {
		console.log(`[SmartMatch] Starting resetSuggestedSmartMatches job`)
		const fourteenDaysAgo = new Date()
		fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - RESET_SUGGESTED_AFTER_DAYS)

		const result = await SmartMatch.updateMany(
			{ suggested: true, suggestedAt: { $lte: fourteenDaysAgo } },
			{
				$set: { status: 'pending' },
				$unset: {
					suggested: '', suggestedAt: '',
					'userA.response': '', 'userA.feedback': '',
					'userA.responseAt': '', 'userA.feedbackAt': '',
					'userB.responseAt': '', 'userB.feedbackAt': '',
					'userB.response': '', 'userB.feedback': '',
					acceptedAt: '', rejectedAt: '',
				}
			}
		)
		console.log(`[SmartMatch] Finished resetSuggestedSmartMatches job - reset ${result.modifiedCount} matches`)
	})
}

async function expireSmartMatches() {
	await helpers.runInBackground(async () => {
		console.log(`[SmartMatch] Starting expireSmartMatches job`)
		const result = await SmartMatch.updateMany(
			{ status: 'pending', expiredAt: { $lte: new Date() } },
			{ $set: { status: 'expired' } }
		)
		console.log(`[SmartMatch] Finished expireSmartMatches job - expired ${result.modifiedCount} matches`)
	})
}

async function suggestSmartMatches() {
	await helpers.runInBackground(async () => {
		console.log(`[SmartMatch] Starting suggestSmartMatches job`)
		const now = new Date()
		const weekStart = new Date(now)
		weekStart.setDate(weekStart.getDate() - WEEKLY_WINDOW_DAYS)
		const dayStart = new Date(now)
		dayStart.setHours(0, 0, 0, 0)

		// Count how many suggested matches each user already has this week
		const weeklySuggested = await SmartMatch.find({ suggested: true, suggestedAt: { $gte: weekStart } })
		const weeklyCount: Record<string, number> = {}
		for (const m of weeklySuggested) {
			const aId = `${m.userA.user}`
			const bId = `${m.userB.user}`
			weeklyCount[aId] = (weeklyCount[aId] ?? 0) + 1
			weeklyCount[bId] = (weeklyCount[bId] ?? 0) + 1
		}

		// Count how many suggested matches each user already has today
		const dailySuggested = await SmartMatch.find({ suggested: true, suggestedAt: { $gte: dayStart } })
		const dailyCount: Record<string, number> = {}
		for (const m of dailySuggested) {
			const aId = `${m.userA.user}`
			const bId = `${m.userB.user}`
			dailyCount[aId] = (dailyCount[aId] ?? 0) + 1
			dailyCount[bId] = (dailyCount[bId] ?? 0) + 1
		}

		// Count how many pending suggested matches each user currently has
		const pendingSuggested = await SmartMatch.find({ suggested: true, status: 'pending' })
		const pendingCount: Record<string, number> = {}
		for (const m of pendingSuggested) {
			const aId = `${m.userA.user}`
			const bId = `${m.userB.user}`
			pendingCount[aId] = (pendingCount[aId] ?? 0) + 1
			pendingCount[bId] = (pendingCount[bId] ?? 0) + 1
		}

		// Find all unprocessed pending matches, sorted by score descending
		const candidates = await SmartMatch.find({ suggested: { $ne: true }, status: 'pending' }).sort({ score: -1 })
		console.log(`[SmartMatch] Found ${candidates.length} candidate matches to suggest`)

		for (const match of candidates) {
			const aId = `${match.userA.user}`
			const bId = `${match.userB.user}`

			if ((pendingCount[aId] ?? 0) >= MAX_PENDING_SUGGESTED) continue
			if ((pendingCount[bId] ?? 0) >= MAX_PENDING_SUGGESTED) continue
			if ((dailyCount[aId] ?? 0) >= MAX_SUGGESTED_PER_DAY) continue
			if ((dailyCount[bId] ?? 0) >= MAX_SUGGESTED_PER_DAY) continue
			if ((weeklyCount[aId] ?? 0) >= MAX_SUGGESTED_PER_WEEK) continue
			if ((weeklyCount[bId] ?? 0) >= MAX_SUGGESTED_PER_WEEK) continue

			match.suggested = true
			match.suggestedAt = new Date()
			await match.save()

			// Notify both users
			const matchUsers = await User.find(
				{ _id: { $in: [match.userA.user, match.userB.user] } },
				{ name: 1, username: 1, auth: 1 }
			)
			for (const user of matchUsers) {
				if (!user.auth?.loggedinDevices?.length) continue
				await pushNotifyUser({
					user,
					title: 'You have a new Vibe Match! ✦',
					body: 'Someone on campus matches your vibe. Check it out!',
					event: 'smart-match-created',
					data: { smartMatch: match._id },
				})
			}

			pendingCount[aId] = (pendingCount[aId] ?? 0) + 1
			pendingCount[bId] = (pendingCount[bId] ?? 0) + 1
			dailyCount[aId] = (dailyCount[aId] ?? 0) + 1
			dailyCount[bId] = (dailyCount[bId] ?? 0) + 1
			weeklyCount[aId] = (weeklyCount[aId] ?? 0) + 1
			weeklyCount[bId] = (weeklyCount[bId] ?? 0) + 1
		}

		let suggestedCount = 0
		for (const [userId, count] of Object.entries(pendingCount)) {
			if (count > 0) suggestedCount += count
		}
		console.log(`[SmartMatch] Finished suggestSmartMatches job - suggested ${suggestedCount} matches`)
	})
}

const smartMatchServices = {
	getSmartMatchesScore,
	createSmartMatches,
	notifyExpiringSmartMatches,
	resetSuggestedSmartMatches,
	expireSmartMatches,
	suggestSmartMatches,
	generateSmartMatchIntroMessage,
}

export default smartMatchServices
