"""List legal session starts for one unscheduled exam activity."""

from datetime import timedelta

from api.models import (
	EsExamPeriodUnavailability,
	EsExamRequirementPrecedingExamination,
	EsExamRequirementSameLocation,
	EsExamRequirementSameTime,
	EsSessionStart,
)
from api.services.exam_scheduler.catalog import (
	_build_location_catalog,
	_build_requirement_unavailability_map,
	_iter_candidate_session_starts,
	_load_external_snapshots,
	_load_requirements,
	_requirement_real_size,
	_requirement_seat_usage,
	_requirement_student_group_tt_ids,
	_requirement_student_ids,
)
from api.services.exam_scheduler.constraints import evaluate_requirement_slot
from api.services.exam_scheduler.relations import (
	_already_scheduled_preceding_bounds,
	_relation_group_ids_for_requirement,
	_seed_same_location_group_anchor_map,
	_seed_same_time_group_anchor_map,
)
from api.services.exam_scheduler.slots import SLOT_MINUTES, _datetime_to_slot, _duration_slots


def available_session_starts_for_activity(activity):
	requirement_id = activity.exam_requirement_id
	requirements = _load_requirements([requirement_id])
	if not requirements:
		return []

	requirement = requirements[0]
	exam_period = requirement.exam_period
	if exam_period is None:
		return []

	period_id = exam_period.id
	session_starts = list(
		EsSessionStart.objects
		.filter(exam_period_id=period_id, status=EsSessionStart.STATUS_TO_CODE["active"])
		.prefetch_related("essessionstartday_set")
		.order_by("start_time", "id")
	)
	if not session_starts:
		return []

	unavailable_dates = set(
		EsExamPeriodUnavailability.objects
		.filter(exam_period_id=period_id)
		.values_list("unavailable_date", flat=True)
	)
	candidates = _iter_candidate_session_starts(requirement, exam_period, session_starts, unavailable_dates)
	if not candidates:
		return []

	sibling_time_slots = {
		sibling.time_slot
		for sibling in requirement.esexamactivity_set.all()
		if sibling.id != activity.id and sibling.is_scheduled and sibling.time_slot is not None
	}
	if len(sibling_time_slots) > 1:
		return []

	sibling_location_ids = {
		sibling.location_id
		for sibling in requirement.esexamactivity_set.all()
		if sibling.id != activity.id and sibling.is_scheduled and sibling.location_id is not None
	}
	if len(sibling_location_ids) > 1:
		return []

	fixed_slot = next(iter(sibling_time_slots), None)
	fixed_location = next(iter(sibling_location_ids), None)

	same_time_group_anchor_by_group_id, same_time_group_conflict_ids = _seed_same_time_group_anchor_map(
		[requirement_id],
		exclude_activity_ids={activity.id},
	)
	same_location_group_anchor_by_group_id, same_location_group_conflict_ids = _seed_same_location_group_anchor_map(
		[requirement_id],
		exclude_activity_ids={activity.id},
	)
	requirement_same_time_group_ids = _relation_group_ids_for_requirement(EsExamRequirementSameTime, requirement_id)
	requirement_same_location_group_ids = _relation_group_ids_for_requirement(EsExamRequirementSameLocation, requirement_id)
	if requirement_same_time_group_ids & same_time_group_conflict_ids:
		return []
	if requirement_same_location_group_ids & same_location_group_conflict_ids:
		return []

	group_time_anchor_values = {
		same_time_group_anchor_by_group_id[group_id]
		for group_id in requirement_same_time_group_ids
		if same_time_group_anchor_by_group_id.get(group_id) is not None
	}
	if len(group_time_anchor_values) > 1:
		return []
	group_time_anchor = next(iter(group_time_anchor_values), None)

	group_location_anchor_values = {
		same_location_group_anchor_by_group_id[group_id]
		for group_id in requirement_same_location_group_ids
		if same_location_group_anchor_by_group_id.get(group_id) is not None
	}
	if len(group_location_anchor_values) > 1:
		return []
	group_location_anchor = next(iter(group_location_anchor_values), None)
	if group_location_anchor is not None and fixed_location is not None and group_location_anchor != fixed_location:
		return []

	if EsExamRequirementPrecedingExamination.objects.filter(
		exam_requirement_id=requirement_id,
		exam_activity__exam_requirement_id=requirement_id,
	).exists():
		return []

	preceding_min_start_by_requirement, preceding_max_end_by_requirement = _already_scheduled_preceding_bounds([requirement_id])
	preceding_min_start = preceding_min_start_by_requirement.get(requirement_id)
	preceding_max_end = preceding_max_end_by_requirement.get(requirement_id)

	duration_slots = _duration_slots(requirement)
	student_ids = _requirement_student_ids(requirement)
	required_seats = _requirement_seat_usage(requirement, _requirement_real_size(requirement))
	location_catalog = _build_location_catalog()
	requirement_unavailability_map = _build_requirement_unavailability_map([requirement_id])
	external = _load_external_snapshots(period_id, exclude_activity_ids={activity.id})
	activity_snapshots = list(external["activity_snapshots"])
	group_tt_ids = _requirement_student_group_tt_ids(requirement)
	anchor_location_id = group_location_anchor or fixed_location
	available_locations = location_catalog
	if anchor_location_id is not None:
		available_locations = [
			location_candidate
			for location_candidate in location_catalog
			if location_candidate["location"].id == anchor_location_id
		]
		if requirement.location_required and not available_locations:
			return []

	required_slot = None
	if fixed_slot is not None and group_time_anchor is not None and fixed_slot != group_time_anchor:
		return []
	if fixed_slot is not None:
		required_slot = fixed_slot
	elif group_time_anchor is not None:
		required_slot = group_time_anchor

	available = []
	for candidate_dt, session_start in candidates:
		time_slot = _datetime_to_slot(exam_period, candidate_dt)
		assigned_locations = evaluate_requirement_slot(
			requirement,
			time_slot,
			candidate_dt,
			duration_slots,
			exam_period,
			activity_snapshots,
			location_catalog,
			requirement_unavailability_map,
			student_ids,
			group_tt_ids,
			required_seats,
			activity_count=1,
			required_slot=required_slot,
			preceding_min_start=preceding_min_start,
			preceding_max_end=preceding_max_end,
			anchor_location_id=anchor_location_id,
			location_required=bool(requirement.location_required),
			include_location_sharing=True,
		)
		if assigned_locations is None:
			continue

		available.append({
			"id": session_start.id,
			"code": session_start.code,
			"start_time": session_start.start_time,
			"time_slot": time_slot,
			"scheduled_start_at": candidate_dt,
			"scheduled_end_at": candidate_dt + timedelta(minutes=duration_slots * SLOT_MINUTES),
		})

	return available
