"""Shared slot evaluation: time windows, student/TT clashes, and room choice."""

from datetime import timedelta

from api.services.exam_scheduler.catalog import _requirement_seat_usage, _requirement_student_ids
from api.services.exam_scheduler.occupancy import (
	_build_location_occupancy,
	_candidate_has_requirement_unavailability,
	_location_is_available,
	_locations_for_id,
	_same_location_sharing_for_slot,
	_student_conflict,
	_window_overlaps,
)
from api.services.exam_scheduler.slots import SLOT_MINUTES
from api.services.tt_availability import tt_location_conflict, tt_student_groups_conflict


def check_time_constraints(
	requirement,
	candidate_slot,
	candidate_dt,
	duration_slots,
	unavailability_map,
	*,
	legal_session_datetimes=None,
	required_slot=None,
	preceding_min_start=None,
	preceding_max_end=None,
	min_start_slot=None,
):
	"""Return True if the candidate start is a legal session time for this requirement."""
	if candidate_slot is None or candidate_dt is None:
		return False
	if required_slot is not None and candidate_slot != required_slot:
		return False
	if min_start_slot is not None and candidate_slot < min_start_slot:
		return False
	if preceding_min_start is not None and candidate_slot < preceding_min_start:
		return False
	if preceding_max_end is not None and candidate_slot + duration_slots > preceding_max_end:
		return False
	if legal_session_datetimes is not None and candidate_dt not in legal_session_datetimes:
		return False
	if requirement.earliest_start and candidate_dt < requirement.earliest_start:
		return False
	if requirement.latest_end and candidate_dt + timedelta(minutes=duration_slots * SLOT_MINUTES) > requirement.latest_end:
		return False
	if _candidate_has_requirement_unavailability(requirement.id, candidate_slot, duration_slots, unavailability_map):
		return False
	return True


def check_student_and_tt(
	student_ids,
	group_tt_ids,
	candidate_slot,
	duration_slots,
	activity_snapshots,
	exam_period,
	exclude_requirement_ids=None,
):
	"""Return True if students and timetable groups are free in this window."""
	if _student_conflict(
		student_ids,
		candidate_slot,
		duration_slots,
		activity_snapshots,
		exclude_requirement_ids=exclude_requirement_ids,
	):
		return False
	if group_tt_ids and tt_student_groups_conflict(group_tt_ids, candidate_slot, duration_slots, exam_period):
		return False
	return True


def choose_location(
	required_seats,
	available_locations,
	activity_snapshots,
	candidate_start_slot,
	duration_slots,
	candidate_exclusive_use=False,
	minimum_split_size=0,
	location_required=True,
	exclude_requirement_ids=None,
	exam_period=None,
):
	"""Pick the first catalog room that has capacity and no timetable clash."""
	if not location_required:
		return None

	occupied_seats = _build_location_occupancy(
		activity_snapshots,
		candidate_start_slot,
		duration_slots,
		exclude_requirement_ids=exclude_requirement_ids,
	)
	usable_locations = [
		location_candidate
		for location_candidate in available_locations
		if location_candidate["capacity"] >= minimum_split_size
	]

	for location_candidate in usable_locations:
		if not _location_is_available(location_candidate, occupied_seats, required_seats, candidate_exclusive_use, minimum_split_size):
			continue

		location = location_candidate["location"]
		if location.tt_id and tt_location_conflict(location, candidate_start_slot, duration_slots, exam_period):
			continue

		return location_candidate["location"]

	return None


def _choose_single_location(
	required_seats,
	available_locations,
	activity_snapshots,
	candidate_start_slot,
	duration_slots,
	candidate_exclusive_use=False,
	minimum_split_size=0,
	location_required=True,
	exclude_requirement_ids=None,
	exam_period=None,
):
	return choose_location(
		required_seats,
		available_locations,
		activity_snapshots,
		candidate_start_slot,
		duration_slots,
		candidate_exclusive_use=candidate_exclusive_use,
		minimum_split_size=minimum_split_size,
		location_required=location_required,
		exclude_requirement_ids=exclude_requirement_ids,
		exam_period=exam_period,
	)


def _choose_locations(
	requirement,
	activity_count,
	required_seats,
	available_locations,
	activity_snapshots,
	candidate_start_slot,
	duration_slots,
	exclude_requirement_ids=None,
	exam_period=None,
	location_required=None,
):
	if location_required is None:
		location_required = requirement.location_required
	if not location_required:
		return [None] * activity_count

	location = choose_location(
		required_seats,
		available_locations,
		activity_snapshots,
		candidate_start_slot,
		duration_slots,
		candidate_exclusive_use=bool(requirement.exclusive_use),
		minimum_split_size=requirement.minimum_split_size or 0,
		location_required=True,
		exclude_requirement_ids=exclude_requirement_ids,
		exam_period=exam_period,
	)
	if location is None:
		return None
	return [location] * activity_count


def evaluate_requirement_slot(
	requirement,
	candidate_slot,
	candidate_dt,
	duration_slots,
	exam_period,
	activity_snapshots,
	location_catalog,
	unavailability_map,
	student_ids,
	group_tt_ids,
	required_seats,
	*,
	activity_count=1,
	legal_session_datetimes=None,
	required_slot=None,
	preceding_min_start=None,
	preceding_max_end=None,
	min_start_slot=None,
	anchor_location_id=None,
	location_required=None,
	include_location_sharing=None,
	same_location_sibling_ids=None,
	pending_cohort_ids=None,
	requirement_by_id=None,
):
	"""Return assigned locations if this slot is legal, otherwise None.

	Used by auto-schedule and available-starts. Direct-schedule uses the
	time/student helpers, then packs a same-time location cohort itself.
	"""
	if location_required is None:
		location_required = bool(requirement.location_required)
	if include_location_sharing is None:
		include_location_sharing = location_required

	if not check_time_constraints(
		requirement,
		candidate_slot,
		candidate_dt,
		duration_slots,
		unavailability_map,
		legal_session_datetimes=legal_session_datetimes,
		required_slot=required_slot,
		preceding_min_start=preceding_min_start,
		preceding_max_end=preceding_max_end,
		min_start_slot=min_start_slot,
	):
		return None

	if not check_student_and_tt(
		student_ids,
		group_tt_ids,
		candidate_slot,
		duration_slots,
		activity_snapshots,
		exam_period,
		exclude_requirement_ids={requirement.id},
	):
		return None

	if not include_location_sharing:
		return [None] * activity_count

	location_catalog_for_job = _locations_for_id(location_catalog, anchor_location_id)
	if location_required and not location_catalog_for_job:
		return None

	sharing_conflict, sharing_exclude_ids, sharing_extra_seats, share_location_id = _same_location_sharing_for_slot(
		{requirement.id},
		activity_snapshots,
		candidate_slot,
		duration_slots,
		anchor_location_id,
		bool(requirement.exclusive_use),
	)
	if sharing_conflict:
		return None
	if anchor_location_id is None and share_location_id is not None:
		location_catalog_for_job = _locations_for_id(location_catalog, share_location_id)
		if location_required and not location_catalog_for_job:
			return None

	if not location_required:
		return [None] * activity_count

	location_exclude_requirement_ids = {requirement.id} | sharing_exclude_ids
	required_seats_for_job = required_seats + sharing_extra_seats
	counted_sharing_requirement_ids = {requirement.id} | sharing_exclude_ids
	same_location_sibling_ids = set(same_location_sibling_ids or [])

	for snapshot in activity_snapshots:
		snapshot_requirement_id = snapshot.get("requirement_id")
		if snapshot_requirement_id not in same_location_sibling_ids:
			continue
		if snapshot_requirement_id in counted_sharing_requirement_ids:
			continue
		if snapshot.get("location_id") is None:
			continue
		if not _window_overlaps(candidate_slot, duration_slots, snapshot["start_slot"], snapshot["duration_slots"]):
			continue
		counted_sharing_requirement_ids.add(snapshot_requirement_id)
		location_exclude_requirement_ids.add(snapshot_requirement_id)
		required_seats_for_job += snapshot.get("seat_usage", 0)

	pending_cohort_ids = list(pending_cohort_ids or [])
	requirement_by_id = requirement_by_id or {}
	if pending_cohort_ids and (
		bool(requirement.exclusive_use)
		or any(bool(requirement_by_id[peer_id].exclusive_use) for peer_id in pending_cohort_ids if peer_id in requirement_by_id)
	):
		return None
	for peer_id in pending_cohort_ids:
		if peer_id in counted_sharing_requirement_ids:
			continue
		peer_requirement = requirement_by_id.get(peer_id)
		if peer_requirement is None:
			continue
		counted_sharing_requirement_ids.add(peer_id)
		required_seats_for_job += _requirement_seat_usage(peer_requirement, len(_requirement_student_ids(peer_requirement)))

	return _choose_locations(
		requirement=requirement,
		activity_count=activity_count,
		required_seats=required_seats_for_job,
		available_locations=location_catalog_for_job,
		activity_snapshots=activity_snapshots,
		candidate_start_slot=candidate_slot,
		duration_slots=duration_slots,
		exclude_requirement_ids=location_exclude_requirement_ids,
		exam_period=exam_period,
		location_required=location_required,
	)
