"""Student clashes, room occupancy, and same-location sharing for a candidate slot."""

from collections import defaultdict

from django.db.models import Q

from api.models import EsExamRequirementSameLocation


def _window_overlaps(start_a, duration_a, start_b, duration_b):
	end_a = start_a + duration_a
	end_b = start_b + duration_b
	return start_a < end_b and start_b < end_a


def _candidate_has_requirement_unavailability(requirement_id, candidate_start_slot, duration_slots, unavailability_map):
	blocked_slots = unavailability_map.get(requirement_id, set())
	for slot in range(candidate_start_slot, candidate_start_slot + duration_slots):
		if slot in blocked_slots:
			return True
	return False


def _student_conflict(requirement_student_ids, candidate_start_slot, duration_slots, activity_snapshots, exclude_requirement_ids=None):
	exclude_requirement_ids = set(exclude_requirement_ids or [])
	for snapshot in activity_snapshots:
		if snapshot["requirement_id"] in exclude_requirement_ids:
			continue
		if not _window_overlaps(candidate_start_slot, duration_slots, snapshot["start_slot"], snapshot["duration_slots"]):
			continue
		if requirement_student_ids.intersection(snapshot["student_ids"]):
			return True
	return False


def _build_location_occupancy(activity_snapshots, candidate_start_slot, duration_slots, exclude_requirement_ids=None):
	exclude_requirement_ids = set(exclude_requirement_ids or [])
	occupancy = defaultdict(lambda: {"seats": 0, "exclusive": False})
	seen_requirement_ids = set()
	for snapshot in activity_snapshots:
		location_id = snapshot.get("location_id")
		if location_id is None:
			continue
		if snapshot.get("requirement_id") in exclude_requirement_ids:
			continue
		if not _window_overlaps(candidate_start_slot, duration_slots, snapshot["start_slot"], snapshot["duration_slots"]):
			continue
		requirement_id = snapshot.get("requirement_id")
		if requirement_id in seen_requirement_ids:
			continue
		seen_requirement_ids.add(requirement_id)
		occupancy[location_id]["seats"] += snapshot.get("seat_usage", 0)
		if snapshot.get("exclusive_use"):
			occupancy[location_id]["exclusive"] = True
	return occupancy


def _location_is_available(location_candidate, occupied_seats, required_seats, candidate_exclusive_use, minimum_split_size):
	location = location_candidate["location"]
	occupancy = occupied_seats.get(location.id) or {"seats": 0, "exclusive": False}
	occupied = occupancy["seats"]
	remaining = location_candidate["capacity"] - occupied
	if remaining < max(required_seats, minimum_split_size):
		return False
	if occupancy["exclusive"]:
		return False
	if candidate_exclusive_use and occupied > 0:
		return False
	if not location_candidate["is_partition"] and occupied > 0:
		return False
	return True


def _same_location_peer_requirement_ids(requirement_ids):
	requirement_ids = set(requirement_ids or [])
	if not requirement_ids:
		return set()

	group_ids = set(
		EsExamRequirementSameLocation.objects.filter(
			Q(exam_requirement_id__in=requirement_ids) | Q(exam_activity__exam_requirement_id__in=requirement_ids)
		).values_list("group_id", flat=True)
	)
	if not group_ids:
		return set()

	peer_ids = set()
	for source_requirement_id, target_requirement_id in EsExamRequirementSameLocation.objects.filter(
		group_id__in=group_ids
	).values_list("exam_requirement_id", "exam_activity__exam_requirement_id"):
		if source_requirement_id is not None:
			peer_ids.add(source_requirement_id)
		if target_requirement_id is not None:
			peer_ids.add(target_requirement_id)
	return peer_ids - requirement_ids


def _same_location_sharing_for_slot(requirement_ids, activity_snapshots, candidate_start_slot, duration_slots, anchor_location_id=None, candidate_exclusive_use=False):
	requirement_ids = set(requirement_ids or [])
	peer_ids = _same_location_peer_requirement_ids(requirement_ids)
	exclude_ids = set()
	extra_seats = 0
	counted_requirement_ids = set()
	occupied_locations = set()
	peer_exclusive_use = False

	for snapshot in activity_snapshots:
		snapshot_requirement_id = snapshot.get("requirement_id")
		if snapshot_requirement_id not in peer_ids or snapshot_requirement_id in requirement_ids:
			continue
		if snapshot_requirement_id in counted_requirement_ids:
			continue
		if snapshot.get("location_id") is None:
			continue
		if not _window_overlaps(candidate_start_slot, duration_slots, snapshot["start_slot"], snapshot["duration_slots"]):
			continue
		counted_requirement_ids.add(snapshot_requirement_id)
		occupied_locations.add(snapshot["location_id"])
		exclude_ids.add(snapshot_requirement_id)
		extra_seats += snapshot.get("seat_usage", 0)
		if snapshot.get("exclusive_use"):
			peer_exclusive_use = True

	if len(occupied_locations) > 1:
		return True, set(), 0, None
	if occupied_locations and anchor_location_id is not None and next(iter(occupied_locations)) != anchor_location_id:
		return True, set(), 0, None
	if exclude_ids and (candidate_exclusive_use or peer_exclusive_use):
		return True, set(), 0, None
	share_location_id = next(iter(occupied_locations), None)
	return False, exclude_ids, extra_seats, share_location_id


def _requirement_overlaps_slot(requirement_id, activity_snapshots, candidate_start_slot, duration_slots):
	for snapshot in activity_snapshots:
		if snapshot.get("requirement_id") != requirement_id:
			continue
		if _window_overlaps(candidate_start_slot, duration_slots, snapshot["start_slot"], snapshot["duration_slots"]):
			return True
	return False


def _locations_for_id(location_catalog, location_id):
	if location_id is None:
		return location_catalog
	return [
		location_candidate
		for location_candidate in location_catalog
		if location_candidate["location"].id == location_id
	]
