import { api } from '../api.js';
import { bindDateInputMask, readDateInputIso, setDateInputIso } from '../date-input.js';
import { escapeHtml, formatLocalDate, showAlert, hideAlert } from '../utils.js';
import { academicCalendarWeekNumber } from './academic-calendar-grid.js';
import { buildAcademicCalendarTitle } from './academic-calendar-title.js';

const DAY_OPTIONS = [
    { value: 1, label: 'Понедельник', shortLabel: 'Пн' },
    { value: 2, label: 'Вторник', shortLabel: 'Вт' },
    { value: 3, label: 'Среда', shortLabel: 'Ср' },
    { value: 4, label: 'Четверг', shortLabel: 'Чт' },
    { value: 5, label: 'Пятница', shortLabel: 'Пт' },
    { value: 6, label: 'Суббота', shortLabel: 'Сб' },
    { value: 7, label: 'Воскресенье', shortLabel: 'Вс' }
];

const SEMESTER_LABELS = {
    autumn: 'осенний',
    spring: 'весенний'
};

const CALENDAR_TOOLTIP_OFFSET = 14;
const CALENDAR_TOOLTIP_VIEWPORT_PADDING = 12;

export function calculateCalendarTooltipPosition({
    pointerX,
    pointerY,
    tooltipWidth,
    tooltipHeight,
    viewportWidth,
    viewportHeight
}) {
    const maxLeft = Math.max(
        CALENDAR_TOOLTIP_VIEWPORT_PADDING,
        viewportWidth - tooltipWidth - CALENDAR_TOOLTIP_VIEWPORT_PADDING
    );
    const maxTop = Math.max(
        CALENDAR_TOOLTIP_VIEWPORT_PADDING,
        viewportHeight - tooltipHeight - CALENDAR_TOOLTIP_VIEWPORT_PADDING
    );
    let left = pointerX + CALENDAR_TOOLTIP_OFFSET;
    let top = pointerY + CALENDAR_TOOLTIP_OFFSET;

    if (left > maxLeft) {
        left = pointerX - tooltipWidth - CALENDAR_TOOLTIP_OFFSET;
    }
    if (top > maxTop) {
        top = pointerY - tooltipHeight - CALENDAR_TOOLTIP_OFFSET;
    }

    return {
        left: Math.round(Math.max(CALENDAR_TOOLTIP_VIEWPORT_PADDING, Math.min(left, maxLeft))),
        top: Math.round(Math.max(CALENDAR_TOOLTIP_VIEWPORT_PADDING, Math.min(top, maxTop)))
    };
}

export async function initAcademicCalendar() {
    const academicYearForm = document.getElementById('academic-year-form');
    const academicYearFormTitle = document.getElementById('academic-year-form-title');
    const academicYearIdInput = document.getElementById('academic-year-id');
    const academicYearTitleInput = document.getElementById('academic-year-title');
    const academicYearStartInput = document.getElementById('academic-year-start');
    const academicYearEndInput = document.getElementById('academic-year-end');
    const academicYearResetButton = document.getElementById('academic-year-reset');
    const academicYearsTbody = document.getElementById('academic-years-tbody');
    const academicYearsRefreshButton = document.getElementById('academic-years-refresh');

    const semesterForm = document.getElementById('semester-form');
    const semesterFormTitle = document.getElementById('semester-form-title');
    const semesterIdInput = document.getElementById('semester-id');
    const semesterYearSelect = document.getElementById('semester-year');
    const semesterTypeSelect = document.getElementById('semester-type');
    const semesterStartInput = document.getElementById('semester-start');
    const semesterEndInput = document.getElementById('semester-end');
    const semesterResetButton = document.getElementById('semester-reset');

    const calendarForm = document.getElementById('academic-calendar-form');
    const calendarFormTitle = document.getElementById('academic-calendar-form-title');
    const calendarIdInput = document.getElementById('academic-calendar-id');
    const calendarYearSelect = document.getElementById('academic-calendar-year');
    const calendarSpecialtySelect = document.getElementById('academic-calendar-specialty');
    const calendarProfileSelect = document.getElementById('academic-calendar-profile');
    const calendarStudyFormSelect = document.getElementById('academic-calendar-study-form');
    const calendarCourseCountInput = document.getElementById('academic-calendar-course-count');
    const calendarResetButton = document.getElementById('academic-calendar-reset');
    const calendarsRefreshButton = document.getElementById('academic-calendars-refresh');
    const calendarsTbody = document.getElementById('academic-calendars-tbody');

    const editorCalendarSelect = document.getElementById('calendar-editor-calendar');
    const editorLoadButton = document.getElementById('calendar-editor-load');
    const editorSaveButton = document.getElementById('calendar-editor-save');
    const fillCourseSelect = document.getElementById('calendar-fill-course');
    const fillStartInput = document.getElementById('calendar-fill-start');
    const fillEndInput = document.getElementById('calendar-fill-end');
    const fillActivitySelect = document.getElementById('calendar-fill-activity');
    const fillApplyButton = document.getElementById('calendar-fill-apply');
    const calendarGrid = document.getElementById('calendar-grid');
    const calendarTotals = document.getElementById('calendar-totals');
    const calendarDayDialog = document.getElementById('calendar-day-dialog');
    const calendarDayDialogClose = document.getElementById('calendar-day-dialog-close');
    const calendarDayDialogDate = document.getElementById('calendar-day-dialog-date');
    const calendarDayDialogCourse = document.getElementById('calendar-day-dialog-course');
    const calendarDayDialogWeek = document.getElementById('calendar-day-dialog-week');
    const calendarDayDialogDay = document.getElementById('calendar-day-dialog-day');
    const calendarDayDialogActivitySummary = document.getElementById('calendar-day-dialog-activity-summary');
    const calendarDayDialogActivitySelect = document.getElementById('calendar-day-dialog-activity');
    const calendarDayDialogTimeScopeSelect = document.getElementById('calendar-day-dialog-time-scope');
    const calendarDayDialogTimeSaveButton = document.getElementById('calendar-day-dialog-time-save');
    const calendarDayDialogTimeClearButton = document.getElementById('calendar-day-dialog-time-clear');
    const calendarTabButtons = Array.from(document.querySelectorAll('[data-calendar-tab]'));
    const calendarTabPanels = Array.from(document.querySelectorAll('[data-calendar-tab-panel]'));
    const subjectCalendarSelect = document.getElementById('calendar-subject-calendar');
    const subjectSemesterSelect = document.getElementById('calendar-subject-semester');
    const subjectSelect = document.getElementById('calendar-subject-id');
    const subjectAddButton = document.getElementById('calendar-subject-add');
    const subjectSaveButton = document.getElementById('calendar-subject-save');
    const subjectRefreshButton = document.getElementById('calendar-subject-refresh');
    const subjectSummary = document.getElementById('calendar-subject-summary');
    const subjectCount = document.getElementById('calendar-subject-count');

    let academicYears = [];
    let semesters = [];
    let specialties = [];
    let profilesBySpecialty = new Map();
    let studyForms = [];
    let activityTypes = [];
    let calendars = [];
    let subjects = [];
    let calendarSubjectRows = [];
    let timeSlotScopes = [];
    let timeSlotDateAssignments = [];
    let activeCalendarDay = null;
    let selectedCalendarDayKey = null;
    let selectedCalendarDays = [];
    let isCalendarDragSelecting = false;
    let didCalendarDragSelect = false;
    let suppressCalendarDayClick = false;
    let isSyncingCalendarDayDialog = false;
    let calendarDayTooltip = null;
    let calendarDayTooltipPositionAnimation = null;

    bindEvents();

    try {
        await Promise.all([
            loadBaseLists(),
            loadYears(),
            loadSubjects(),
            loadStudyForms(),
            loadActivityTypes(),
            loadTimeSlotScopes(),
            loadTimeSlotDateAssignments()
        ]);
        await loadCalendars();
    } catch (error) {
        showAlert('academic-calendar-alert', error.message || 'Ошибка загрузки календарных графиков', 'error');
    }

    function bindEvents() {
        [
            academicYearStartInput,
            academicYearEndInput,
            semesterStartInput,
            semesterEndInput,
            fillStartInput,
            fillEndInput
        ].forEach(bindDateInputMask);

        calendarTabButtons.forEach(button => {
            button.addEventListener('click', () => activateCalendarTab(button.dataset.calendarTab));
        });
        academicYearsRefreshButton?.addEventListener('click', loadYears);
        calendarsRefreshButton?.addEventListener('click', loadCalendars);
        academicYearResetButton?.addEventListener('click', resetAcademicYearForm);
        semesterResetButton?.addEventListener('click', resetSemesterForm);
        calendarResetButton?.addEventListener('click', resetCalendarForm);
        calendarSpecialtySelect?.addEventListener('change', () => populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value));
        editorLoadButton?.addEventListener('click', loadCalendarGrid);
        editorSaveButton?.addEventListener('click', saveCalendarGrid);
        fillApplyButton?.addEventListener('click', applyCalendarFill);
        subjectCalendarSelect?.addEventListener('change', loadCalendarSubjects);
        subjectAddButton?.addEventListener('click', addCalendarSubject);
        subjectSaveButton?.addEventListener('click', saveCalendarSubjects);
        subjectRefreshButton?.addEventListener('click', loadCalendarSubjects);
        subjectSummary?.addEventListener('click', handleSubjectSummaryClick);

        calendarGrid?.addEventListener('change', (event) => {
            if (event.target.matches('.calendar-day-activity')) {
                updateCalendarDayVisual(event.target.closest('.calendar-day'));
                renderCalendarTotals();
            }
        });
        calendarGrid?.addEventListener('mousedown', (event) => {
            const day = event.target.closest('.calendar-day');
            if (day) startCalendarDayDragSelection(event, day);
        });
        calendarGrid?.addEventListener('mouseover', (event) => {
            const day = event.target.closest('.calendar-day');
            if (day) addCalendarDayToDragSelection(day);
        });
        calendarGrid?.addEventListener('mousemove', (event) => {
            const day = event.target.closest('.calendar-day');
            if (day) showCalendarDayTooltip(day, event);
        });
        calendarGrid?.addEventListener('mouseleave', hideCalendarDayTooltip);
        calendarGrid?.addEventListener('focusin', (event) => {
            const day = event.target.closest('.calendar-day');
            if (day) showCalendarDayTooltip(day);
        });
        calendarGrid?.addEventListener('focusout', hideCalendarDayTooltip);
        calendarGrid?.addEventListener('click', (event) => {
            if (suppressCalendarDayClick) {
                suppressCalendarDayClick = false;
                event.preventDefault();
                return;
            }
            const day = event.target.closest('.calendar-day');
            if (day) openCalendarDayDialog(day);
        });
        document.addEventListener('mouseup', finishCalendarDayDragSelection);
        calendarGrid?.addEventListener('keydown', (event) => {
            if (event.key !== 'Enter' && event.key !== ' ') return;
            const day = event.target.closest('.calendar-day');
            if (!day) return;
            event.preventDefault();
            openCalendarDayDialog(day);
        });
        calendarDayDialogClose?.addEventListener('click', closeCalendarDayDialog);
        calendarDayDialog?.addEventListener('click', (event) => {
            if (event.target === calendarDayDialog) closeCalendarDayDialog();
        });
        calendarDayDialogActivitySelect?.addEventListener('change', () => {
            if (isSyncingCalendarDayDialog || !calendarDayDialogActivitySelect.value) return;
            const days = selectedCalendarDays.length ? selectedCalendarDays : (activeCalendarDay ? [activeCalendarDay] : []);
            days.forEach(day => {
                const select = day.querySelector('.calendar-day-activity');
                select.value = calendarDayDialogActivitySelect.value;
                updateCalendarDayVisual(day);
            });
            updateCalendarDayDialogSummaryForSelection(days);
            renderCalendarTotals();
        });
        calendarDayDialogTimeSaveButton?.addEventListener('click', closeCalendarDayDialog);
        calendarDayDialogTimeClearButton?.addEventListener('click', clearTimeSlotDateAssignment);

        academicYearForm?.addEventListener('submit', saveAcademicYear);
        semesterForm?.addEventListener('submit', saveSemester);
        calendarForm?.addEventListener('submit', saveCalendar);

        academicYearsTbody?.addEventListener('click', handleAcademicYearTableClick);
        calendarsTbody?.addEventListener('click', handleCalendarTableClick);
    }

    async function loadBaseLists() {
        specialties = await api.get('/api/specialties');
        await loadProfiles();
        populateSpecialtySelects();
    }

    async function loadProfiles() {
        const pairs = await Promise.all(specialties.map(async (speciality) => {
            const profiles = await api.get(`/api/specialties/${speciality.id}/profiles`);
            return [String(speciality.id), profiles];
        }));
        profilesBySpecialty = new Map(pairs);
    }

    async function loadYears() {
        academicYearsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Загрузка...</td></tr>';
        try {
            academicYears = await api.get('/api/admin/calendar/years');
            semesters = academicYears.flatMap(year => year.semesters || []);
            renderAcademicYears();
            populateYearSelects();
        } catch (error) {
            academicYearsTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
            throw error;
        }
    }

    function renderAcademicYears() {
        if (!academicYears.length) {
            academicYearsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Учебные годы не созданы</td></tr>';
            return;
        }
        academicYearsTbody.innerHTML = academicYears.map(year => `
            <tr>
                <td class="academic-year-center-cell academic-year-title-cell">${escapeHtml(year.title)}</td>
                <td class="academic-year-center-cell academic-year-period-cell">${escapeHtml(formatDisplayDateRange(year.startDate, year.endDate))}</td>
                <td class="academic-year-semesters-cell">${renderSemesterList(year.semesters || [])}</td>
                <td class="academic-year-actions-cell">
                    <div class="academic-year-actions">
                        <button class="btn btn-sm btn-secondary btn-edit-year" data-id="${year.id}">Изменить</button>
                        <button class="btn btn-sm btn-danger btn-delete-year" data-id="${year.id}">Удалить</button>
                    </div>
                </td>
            </tr>
        `).join('');
    }

    function renderSemesterList(items) {
        if (!items.length) return '<span class="muted">Нет семестров</span>';
        return `<div class="academic-year-semesters">${items.map(semester => {
            const title = semesterTypeTitle(semester);
            const period = formatDisplayDateRange(semester.startDate, semester.endDate);
            return `
                <div class="academic-year-semester-item">
                    <div class="academic-year-semester-meta">
                        <span class="academic-year-semester-type">${escapeHtml(title)}</span>
                        <span class="academic-year-semester-period">${escapeHtml(period)}</span>
                    </div>
                    <button class="btn btn-sm btn-secondary btn-edit-semester" data-id="${semester.id}" aria-label="Изменить ${escapeHtml(title)} семестр ${escapeHtml(period)}">Изменить</button>
                </div>
            `;
        }).join('')}</div>`;
    }

    async function saveAcademicYear(event) {
        event.preventDefault();
        hideAlert('academic-year-alert');
        try {
            const payload = {
                title: academicYearTitleInput.value.trim(),
                startDate: readDateInputIso(academicYearStartInput, 'Начало учебного года'),
                endDate: readDateInputIso(academicYearEndInput, 'Окончание учебного года')
            };
            if (!payload.title || !payload.startDate || !payload.endDate) throw new Error('Заполните название и даты учебного года');
            const id = academicYearIdInput.value;
            await (id
                ? api.put('/api/admin/calendar/years/' + id, payload)
                : api.post('/api/admin/calendar/years', payload));
            showAlert('academic-year-alert', 'Учебный год сохранён', 'success');
            resetAcademicYearForm();
            await loadYears();
            await loadCalendars();
        } catch (error) {
            showAlert('academic-year-alert', error.message || 'Ошибка сохранения учебного года', 'error');
        }
    }

    async function handleAcademicYearTableClick(event) {
        const editYearButton = event.target.closest('.btn-edit-year');
        const deleteYearButton = event.target.closest('.btn-delete-year');
        const editSemesterButton = event.target.closest('.btn-edit-semester');

        if (editYearButton) {
            const year = academicYears.find(item => item.id == editYearButton.dataset.id);
            if (!year) return;
            academicYearFormTitle.textContent = 'Редактирование учебного года';
            academicYearIdInput.value = year.id;
            academicYearTitleInput.value = year.title || '';
            setDateInputIso(academicYearStartInput, year.startDate);
            setDateInputIso(academicYearEndInput, year.endDate);
            hideAlert('academic-year-alert');
            academicYearForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
            return;
        }

        if (editSemesterButton) {
            const semester = semesters.find(item => item.id == editSemesterButton.dataset.id);
            if (!semester) return;
            semesterFormTitle.textContent = `Редактирование семестра: ${semesterTypeTitle(semester)}`;
            semesterIdInput.value = semester.id;
            semesterYearSelect.value = semester.academicYearId || '';
            semesterTypeSelect.value = normalizeSemesterType(semester.semesterType);
            setDateInputIso(semesterStartInput, semester.startDate);
            setDateInputIso(semesterEndInput, semester.endDate);
            syncSelects(semesterYearSelect, semesterTypeSelect);
            hideAlert('semester-alert');
            semesterForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
            return;
        }

        if (deleteYearButton) {
            if (!confirm('Удалить учебный год вместе с семестрами и календарными графиками?')) return;
            try {
                await api.delete('/api/admin/calendar/years/' + deleteYearButton.dataset.id);
                showAlert('academic-year-alert', 'Учебный год удалён', 'success');
                await loadYears();
                await loadCalendars();
            } catch (error) {
                showAlert('academic-year-alert', error.message || 'Ошибка удаления учебного года', 'error');
            }
        }
    }

    function resetAcademicYearForm() {
        academicYearForm.reset();
        academicYearFormTitle.textContent = 'Учебный год';
        academicYearIdInput.value = '';
        hideAlert('academic-year-alert');
    }

    async function saveSemester(event) {
        event.preventDefault();
        hideAlert('semester-alert');
        const yearId = semesterYearSelect.value;
        try {
            const payload = {
                semesterType: semesterTypeSelect.value,
                startDate: readDateInputIso(semesterStartInput, 'Начало семестра'),
                endDate: readDateInputIso(semesterEndInput, 'Окончание семестра')
            };
            if (!yearId || !payload.semesterType || !payload.startDate || !payload.endDate) {
                throw new Error('Заполните учебный год, тип и даты семестра');
            }
            const id = semesterIdInput.value;
            await (id
                ? api.put('/api/admin/calendar/semesters/' + id, payload)
                : api.post(`/api/admin/calendar/years/${yearId}/semesters`, payload));
            showAlert('semester-alert', 'Семестр сохранён', 'success');
            resetSemesterForm();
            await loadYears();
        } catch (error) {
            showAlert('semester-alert', error.message || 'Ошибка сохранения семестра', 'error');
        }
    }

    function resetSemesterForm() {
        semesterForm.reset();
        semesterFormTitle.textContent = 'Семестр';
        semesterIdInput.value = '';
        syncSelects(semesterYearSelect, semesterTypeSelect);
        hideAlert('semester-alert');
    }

    async function loadStudyForms() {
        studyForms = await api.get('/api/education-forms');
        populateStudyFormSelect();
    }

    async function loadSubjects() {
        subjects = await api.get('/api/subjects');
        populateSubjectSelect();
    }

    async function loadActivityTypes() {
        activityTypes = await api.get('/api/admin/calendar/activity-types');
        populateActivitySelects();
    }

    async function loadTimeSlotScopes() {
        timeSlotScopes = await api.get('/api/admin/time-slots/scopes');
    }

    async function loadTimeSlotDateAssignments() {
        timeSlotDateAssignments = await api.get('/api/admin/time-slots/date-assignments');
    }

    async function loadCalendars() {
        calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Загрузка...</td></tr>';
        try {
            calendars = await api.get('/api/admin/academic-calendars');
            renderCalendars();
            populateEditorCalendarSelect();
            populateSubjectCalendarSelect();
        } catch (error) {
            calendarsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
            throw error;
        }
    }

    function renderCalendars() {
        if (!calendars.length) {
            calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Календарные графики не созданы</td></tr>';
            return;
        }
        calendarsTbody.innerHTML = calendars.map(calendar => `
            <tr>
                <td>${escapeHtml(calendar.academicYearTitle || '-')}</td>
                <td>${escapeHtml(`${calendar.specialtyCode || ''} ${calendar.specialtyName || ''}`.trim() || '-')}</td>
                <td>${escapeHtml(calendar.specialtyProfileName || '-')}</td>
                <td>${escapeHtml(calendar.studyFormName || '-')}</td>
                <td>${escapeHtml(String(calendar.courseCount || '-'))}</td>
                <td>
                    <button class="btn btn-sm btn-secondary btn-edit-calendar" data-id="${calendar.id}">Изменить</button>
                    <button class="btn btn-sm btn-secondary btn-open-calendar-grid" data-id="${calendar.id}">Сетка</button>
                    <button class="btn btn-sm btn-secondary btn-open-calendar-subjects" data-id="${calendar.id}">Дисциплины</button>
                    <button class="btn btn-sm btn-danger btn-delete-calendar" data-id="${calendar.id}">Удалить</button>
                </td>
            </tr>
        `).join('');
    }

    async function saveCalendar(event) {
        event.preventDefault();
        hideAlert('academic-calendar-alert');
        const payload = {
            id: calendarIdInput.value ? Number(calendarIdInput.value) : null,
            academicYearId: Number(calendarYearSelect.value),
            specialtyId: Number(calendarSpecialtySelect.value),
            specialtyProfileId: Number(calendarProfileSelect.value),
            studyFormId: Number(calendarStudyFormSelect.value),
            courseCount: Number(calendarCourseCountInput.value)
        };
        try {
            if (!payload.academicYearId || !payload.specialtyId
                || !payload.specialtyProfileId || !payload.studyFormId || !payload.courseCount) {
                throw new Error('Заполните все поля календарного графика');
            }
            const id = calendarIdInput.value;
            const saved = id
                ? await api.put('/api/admin/academic-calendars/' + id, payload)
                : await api.post('/api/admin/academic-calendars', payload);
            const savedTitle = buildAcademicCalendarTitle(saved);
            showAlert('academic-calendar-alert', `График "${escapeHtml(savedTitle)}" сохранён`, 'success');
            resetCalendarForm();
            await loadCalendars();
        } catch (error) {
            showAlert('academic-calendar-alert', error.message || 'Ошибка сохранения календарного графика', 'error');
        }
    }

    async function handleCalendarTableClick(event) {
        const editButton = event.target.closest('.btn-edit-calendar');
        const gridButton = event.target.closest('.btn-open-calendar-grid');
        const subjectsButton = event.target.closest('.btn-open-calendar-subjects');
        const deleteButton = event.target.closest('.btn-delete-calendar');

        if (editButton) {
            activateCalendarTab('manage');
            const calendar = calendars.find(item => item.id == editButton.dataset.id);
            if (!calendar) return;
            calendarFormTitle.textContent = 'Редактирование календарного графика';
            calendarIdInput.value = calendar.id;
            calendarYearSelect.value = calendar.academicYearId || '';
            calendarSpecialtySelect.value = calendar.specialtyId || '';
            populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value, calendar.specialtyProfileId);
            calendarStudyFormSelect.value = calendar.studyFormId || '';
            calendarCourseCountInput.value = calendar.courseCount || 4;
            syncSelects(calendarYearSelect, calendarSpecialtySelect, calendarProfileSelect, calendarStudyFormSelect);
            hideAlert('academic-calendar-alert');
            calendarForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
            return;
        }

        if (gridButton) {
            activateCalendarTab('grid');
            editorCalendarSelect.value = gridButton.dataset.id;
            refreshSelectVisuals(editorCalendarSelect);
            await loadCalendarGrid();
            calendarGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
            return;
        }

        if (subjectsButton) {
            activateCalendarTab('subjects');
            subjectCalendarSelect.value = subjectsButton.dataset.id;
            refreshSelectVisuals(subjectCalendarSelect);
            await loadCalendarSubjects();
            subjectSummary.scrollIntoView({ behavior: 'smooth', block: 'start' });
            return;
        }

        if (deleteButton) {
            if (!confirm('Удалить календарный график?')) return;
            try {
                await api.delete('/api/admin/academic-calendars/' + deleteButton.dataset.id);
                showAlert('academic-calendar-alert', 'Календарный график удалён', 'success');
                await loadCalendars();
            } catch (error) {
                showAlert('academic-calendar-alert', error.message || 'Ошибка удаления календарного графика', 'error');
            }
        }
    }

    function resetCalendarForm() {
        calendarForm.reset();
        calendarFormTitle.textContent = 'Календарный учебный график';
        calendarIdInput.value = '';
        calendarCourseCountInput.value = 4;
        populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value);
        syncSelects(calendarYearSelect, calendarSpecialtySelect, calendarProfileSelect, calendarStudyFormSelect);
        hideAlert('academic-calendar-alert');
    }

    function activateCalendarTab(tabName) {
        const target = tabName || 'manage';
        calendarTabButtons.forEach(button => {
            const active = button.dataset.calendarTab === target;
            button.classList.toggle('active', active);
            button.setAttribute('aria-selected', active ? 'true' : 'false');
        });
        calendarTabPanels.forEach(panel => {
            const active = panel.dataset.calendarTabPanel === target;
            panel.classList.toggle('active', active);
            panel.hidden = !active;
        });
    }

    async function loadCalendarSubjects() {
        hideAlert('calendar-subject-alert');
        const calendar = selectedSubjectCalendar();
        populateSubjectSemesterSelect();
        calendarSubjectRows = [];
        if (!calendar) {
            renderCalendarSubjects();
            return;
        }
        subjectSummary.innerHTML = '<div class="loading-row">Загрузка...</div>';
        try {
            const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/subjects`);
            calendarSubjectRows = sortCalendarSubjectRows(rows || []);
            renderCalendarSubjects();
        } catch (error) {
            subjectSummary.innerHTML = `<div class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</div>`;
            showAlert('calendar-subject-alert', error.message || 'Ошибка загрузки дисциплин графика', 'error');
        }
    }

    function addCalendarSubject() {
        hideAlert('calendar-subject-alert');
        const calendar = selectedSubjectCalendar();
        const semesterNumber = Number(subjectSemesterSelect.value);
        const subjectId = Number(subjectSelect.value);
        if (!calendar || !semesterNumber || !subjectId) {
            showAlert('calendar-subject-alert', 'Выберите график, семестр и дисциплину', 'error');
            return;
        }
        if (calendarSubjectRows.some(row =>
            Number(row.semesterNumber) === semesterNumber && Number(row.subjectId) === subjectId
        )) {
            showAlert('calendar-subject-alert', 'Дисциплина уже добавлена в этот семестр', 'error');
            return;
        }
        const subject = subjects.find(item => Number(item.id) === subjectId);
        if (!subject) {
            showAlert('calendar-subject-alert', 'Дисциплина не найдена в справочнике', 'error');
            return;
        }
        calendarSubjectRows = sortCalendarSubjectRows([
            ...calendarSubjectRows,
            {
                id: null,
                calendarId: calendar.id,
                semesterNumber,
                subjectId,
                subjectName: subject.name,
                subjectCode: subject.code,
                departmentId: subject.departmentId
            }
        ]);
        renderCalendarSubjects();
        showAlert('calendar-subject-alert', 'Дисциплина добавлена. Сохраните привязки.', 'success');
    }

    async function saveCalendarSubjects() {
        hideAlert('calendar-subject-alert');
        const calendar = selectedSubjectCalendar();
        if (!calendar) {
            showAlert('calendar-subject-alert', 'Выберите календарный график', 'error');
            return;
        }
        const payload = calendarSubjectRows.map(row => ({
            semesterNumber: Number(row.semesterNumber),
            subjectId: Number(row.subjectId)
        }));
        try {
            const saved = await api.put(`/api/admin/academic-calendars/${calendar.id}/subjects`, payload);
            calendarSubjectRows = sortCalendarSubjectRows(saved || []);
            renderCalendarSubjects();
            showAlert('calendar-subject-alert', 'Привязки дисциплин сохранены', 'success');
        } catch (error) {
            showAlert('calendar-subject-alert', error.message || 'Ошибка сохранения дисциплин графика', 'error');
        }
    }

    function handleSubjectSummaryClick(event) {
        const removeButton = event.target.closest('.calendar-subject-remove');
        if (!removeButton) return;
        const semesterNumber = Number(removeButton.dataset.semester);
        const subjectId = Number(removeButton.dataset.subject);
        calendarSubjectRows = calendarSubjectRows.filter(row =>
            Number(row.semesterNumber) !== semesterNumber || Number(row.subjectId) !== subjectId
        );
        renderCalendarSubjects();
        showAlert('calendar-subject-alert', 'Дисциплина убрана. Сохраните привязки.', 'success');
    }

    function renderCalendarSubjects() {
        const calendar = selectedSubjectCalendar();
        if (!calendar) {
            subjectCount.textContent = '';
            subjectSummary.innerHTML = '<div class="loading-row">Выберите календарный график</div>';
            return;
        }

        const semesterCount = calendarSemesterCount(calendar);
        subjectCount.textContent = `${calendarSubjectRows.length} дисциплин`;
        subjectSummary.innerHTML = Array.from({ length: semesterCount }, (_, index) => {
            const semesterNumber = index + 1;
            const semesterRows = calendarSubjectRows.filter(row => Number(row.semesterNumber) === semesterNumber);
            return `
                <section class="calendar-subject-semester">
                    <div class="calendar-subject-semester-head">
                        <strong>${escapeHtml(semesterOptionLabel(semesterNumber))}</strong>
                        <span>${semesterRows.length} дисциплин</span>
                    </div>
                    ${semesterRows.length
                        ? `<div class="calendar-subject-chips">${semesterRows.map(renderSubjectChip).join('')}</div>`
                        : '<div class="calendar-subject-empty">Нет дисциплин</div>'}
                </section>
            `;
        }).join('');
    }

    function renderSubjectChip(row) {
        return `
            <span class="calendar-subject-chip">
                <span>${escapeHtml(subjectRowLabel(row))}</span>
                <button type="button"
                        class="calendar-subject-remove"
                        data-semester="${escapeHtml(String(row.semesterNumber))}"
                        data-subject="${escapeHtml(String(row.subjectId))}"
                        aria-label="Убрать ${escapeHtml(subjectRowLabel(row))}">×</button>
            </span>
        `;
    }

    async function loadCalendarGrid() {
        hideAlert('calendar-editor-alert');
        const calendar = selectedEditorCalendar();
        if (!calendar) {
            showAlert('calendar-editor-alert', 'Выберите календарный график', 'error');
            return;
        }
        try {
            await loadTimeSlotDateAssignments();
            const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/grid`);
            const gridRows = rows.length ? rows : buildDefaultGrid(calendar);
            renderCalendarGrid(gridRows);
            showAlert('calendar-editor-alert', rows.length ? 'Сетка загружена' : 'Подготовлена новая сетка по датам учебного года', 'success');
        } catch (error) {
            showAlert('calendar-editor-alert', error.message || 'Ошибка загрузки сетки', 'error');
        }
    }

    async function saveCalendarGrid() {
        hideAlert('calendar-editor-alert');
        const calendar = selectedEditorCalendar();
        if (!calendar) {
            showAlert('calendar-editor-alert', 'Выберите календарный график', 'error');
            return;
        }
        const rows = Array.from(calendarGrid.querySelectorAll('.calendar-day')).map(day => ({
            calendarId: calendar.id,
            courseNumber: Number(day.dataset.course),
            date: day.dataset.date,
            weekNumber: Number(day.dataset.week),
            dayOfWeek: Number(day.dataset.day),
            activityTypeId: Number(day.querySelector('.calendar-day-activity').value)
        }));
        if (!rows.length) {
            showAlert('calendar-editor-alert', 'Сначала загрузите сетку графика', 'error');
            return;
        }
        try {
            await api.put(`/api/admin/academic-calendars/${calendar.id}/grid`, rows);
            showAlert('calendar-editor-alert', 'Календарный учебный график сохранён', 'success');
            await loadCalendarGrid();
        } catch (error) {
            showAlert('calendar-editor-alert', error.message || 'Ошибка сохранения сетки', 'error');
        }
    }

    function applyCalendarFill() {
        hideAlert('calendar-editor-alert');
        const activityId = fillActivitySelect.value;
        if (!activityId || !fillStartInput.value || !fillEndInput.value) {
            showAlert('calendar-editor-alert', 'Выберите даты и код активности для заполнения', 'error');
            return;
        }
        let start;
        let end;
        try {
            start = readDateInputIso(fillStartInput, 'Дата начала диапазона');
            end = readDateInputIso(fillEndInput, 'Дата окончания диапазона');
        } catch (error) {
            showAlert('calendar-editor-alert', error.message, 'error');
            return;
        }
        if (end < start) {
            showAlert('calendar-editor-alert', 'Дата окончания не может быть раньше даты начала', 'error');
            return;
        }
        const course = fillCourseSelect.value;
        let changed = 0;
        calendarGrid.querySelectorAll('.calendar-day').forEach(day => {
            const sameCourse = !course || day.dataset.course === course;
            const inRange = day.dataset.date >= start && day.dataset.date <= end;
            if (sameCourse && inRange) {
                day.querySelector('.calendar-day-activity').value = activityId;
                updateCalendarDayVisual(day);
                changed += 1;
            }
        });
        renderCalendarTotals();
        showAlert('calendar-editor-alert', `Обновлено ячеек: ${changed}`, 'success');
    }

    function renderCalendarGrid(rows) {
        activeCalendarDay = null;
        selectedCalendarDayKey = null;
        selectedCalendarDays = [];
        const grouped = groupBy(rows, row => row.courseNumber);
        calendarGrid.innerHTML = Array.from(grouped.entries()).map(([course, courseRows]) => `
            <details class="calendar-course-block">
                <summary class="calendar-course-header">
                    <span class="calendar-course-title">
                        <span class="calendar-course-chevron" aria-hidden="true"></span>
                        <span>${course} курс</span>
                    </span>
                    <span class="calendar-course-meta">${courseRows.length} дней</span>
                </summary>
                <div class="calendar-semesters-grid">
                    ${renderCourseSemesters(Number(course), courseRows)}
                </div>
            </details>
        `).join('');
        populateFillCourseSelect();
        renderCalendarTotals();
    }

    function renderCourseSemesters(course, courseRows) {
        const calendar = selectedEditorCalendar();
        const semestersForCourse = semesterBlocksForCourse(calendar, course, courseRows);
        const maxWeekColumns = Math.max(
            ...semestersForCourse.map(block => uniqueWeekNumbers(block.rows).length),
            1
        );
        return semestersForCourse.map(block => `
            <section class="calendar-semester-block">
                <div class="calendar-semester-header">
                    <h4>${block.semesterNumber} семестр</h4>
                    <span>${escapeHtml(block.caption)}</span>
                </div>
                ${renderSemesterTable(block.rows, maxWeekColumns)}
            </section>
        `).join('');
    }

    function renderSemesterTable(rows, targetWeekColumns = 0) {
        if (!rows.length) {
            return '<div class="calendar-empty-semester">Нет дат в этом семестре</div>';
        }

        const rowsByWeek = groupBy(rows, row => row.weekNumber);
        const weeks = Array.from(rowsByWeek.keys()).sort((a, b) => Number(a) - Number(b));
        const weekSlots = [...weeks];
        while (weekSlots.length < targetWeekColumns) {
            weekSlots.push(null);
        }
        const cellByWeekAndDay = new Map(rows.map(row => [`${row.weekNumber}:${row.dayOfWeek}`, row]));
        return `
            <div class="calendar-semester-table-wrap">
                <table class="calendar-semester-table">
                    <colgroup>
                        <col class="calendar-day-head-col">
                        ${weekSlots.map(() => '<col class="calendar-week-col">').join('')}
                    </colgroup>
                    <thead>
                        <tr>
                            <th class="calendar-day-head">День</th>
                            ${weekSlots.map(week => week === null
                                ? '<th class="calendar-week-placeholder" aria-hidden="true"></th>'
                                : `<th title="Неделя ${week} учебного года">${week}</th>`).join('')}
                        </tr>
                    </thead>
                    <tbody>
                        ${DAY_OPTIONS.map(day => `
                            <tr>
                                <th class="calendar-day-head">${escapeHtml(day.shortLabel)}</th>
                                ${weekSlots.map(week => {
                                    if (week === null) {
                                        return '<td class="calendar-empty-day calendar-week-placeholder" aria-hidden="true"></td>';
                                    }
                                    const row = cellByWeekAndDay.get(`${week}:${day.value}`);
                                    return row ? renderCalendarDay(row) : '<td class="calendar-empty-day"></td>';
                                }).join('')}
                            </tr>
                        `).join('')}
                    </tbody>
                </table>
            </div>
        `;
    }

    function uniqueWeekNumbers(rows) {
        return Array.from(new Set(rows.map(row => row.weekNumber)));
    }

    function renderCalendarDay(row) {
        const selected = row.activityTypeId || activityIdByCode(row.activityCode) || theoryActivityId();
        const dayName = shortDayLabel(row.dayOfWeek);
        const activity = activityById(selected) || activityTypes[0] || {};
        const code = activity.code || row.activityCode || 'Т';
        return `
            <td class="calendar-day"
                 data-course="${row.courseNumber}"
                 data-date="${row.date}"
                 data-week="${row.weekNumber}"
                 data-day="${row.dayOfWeek}"
                 data-time-assignment-id="${timeAssignmentForDate(row.date)?.id || ''}"
                 data-time-scope-id="${timeAssignmentForDate(row.date)?.scopeId || ''}"
                 role="button"
                 tabindex="0"
                 aria-label="${escapeHtml(`${formatShortDate(row.date)} ${dayName}, ${row.weekNumber} неделя, код ${code}`)}">
                <strong class="calendar-day-code">${escapeHtml(code)}</strong>
                ${timeAssignmentForDate(row.date) ? '<span class="calendar-day-time-marker">вр.</span>' : ''}
                <select class="calendar-day-activity custom-select-initialized" aria-hidden="true" tabindex="-1">${options(activityTypes.map(toActivityOption), selected)}</select>
            </td>
        `;
    }

    function openCalendarDayDialog(day) {
        openCalendarDayDialogForSelection([day]);
    }

    function openCalendarDayDialogForSelection(days) {
        const selectedDays = uniqueCalendarDays(days);
        if (!selectedDays.length) return;

        activeCalendarDay = selectedDays[0];
        selectCalendarDays(selectedDays);

        const dates = selectedDays.map(day => day.dataset.date).sort();
        const courses = new Set(selectedDays.map(day => day.dataset.course));
        const weeks = new Set(selectedDays.map(day => day.dataset.week));
        const weekDays = new Set(selectedDays.map(day => day.dataset.day));
        const isSingle = selectedDays.length === 1;

        calendarDayDialogDate.textContent = isSingle
            ? `${formatLongDate(activeCalendarDay.dataset.date)} (${shortDayLabel(activeCalendarDay.dataset.day)})`
            : `${formatLongDate(dates[0])} - ${formatLongDate(dates[dates.length - 1])}`;
        calendarDayDialogCourse.textContent = courses.size === 1
            ? `${activeCalendarDay.dataset.course} курс`
            : `${courses.size} курса`;
        calendarDayDialogWeek.textContent = weeks.size === 1
            ? `${activeCalendarDay.dataset.week} неделя`
            : `${weeks.size} недель`;
        calendarDayDialogDay.textContent = weekDays.size === 1
            ? dayLabel(activeCalendarDay.dataset.day)
            : `${selectedDays.length} ячеек`;
        calendarDayDialogActivitySelect.innerHTML = options(
            activityTypes.map(toActivityOption),
            commonCalendarDayActivityValue(selectedDays)
        );
        if (isSingle) {
            populateTimeScopeDialogSelect(activeCalendarDay.dataset.date);
        } else {
            populateMultiSelectionTimeScopeDialogSelect();
        }
        isSyncingCalendarDayDialog = true;
        syncSelects(calendarDayDialogActivitySelect);
        syncSelects(calendarDayDialogTimeScopeSelect);
        isSyncingCalendarDayDialog = false;
        updateCalendarDayDialogSummaryForSelection(selectedDays);
        renderCalendarTotals();
        calendarDayDialog.classList.add('open');
    }

    function closeCalendarDayDialog() {
        activeCalendarDay = null;
        calendarDayDialog.classList.remove('open');
    }

    function updateCalendarDayDialogSummary(day) {
        const activity = activityById(day.querySelector('.calendar-day-activity').value);
        if (!activity) {
            calendarDayDialogActivitySummary.innerHTML = '';
            return;
        }
        calendarDayDialogActivitySummary.innerHTML = `
            <span class="calendar-day-dialog-code">${escapeHtml(activity.code)}</span>
            <div>
                <strong>${escapeHtml(activity.name)}</strong>
                <p>${escapeHtml(activity.description || (activity.allowSchedule ? 'Обычные пары разрешены' : 'Обычные пары не генерируются'))}</p>
            </div>
        `;
    }

    function updateCalendarDayDialogSummaryForSelection(days) {
        if (days.length <= 1) {
            updateCalendarDayDialogSummary(days[0]);
            return;
        }

        const activity = activityById(commonCalendarDayActivityValue(days));
        if (!activity) {
            calendarDayDialogActivitySummary.innerHTML = `
                <span class="calendar-day-dialog-code">...</span>
                <div>
                    <strong>Выбрано ячеек: ${days.length}</strong>
                    <p>Выберите код активности, чтобы применить его ко всему выделению</p>
                </div>
            `;
            return;
        }

        calendarDayDialogActivitySummary.innerHTML = `
            <span class="calendar-day-dialog-code">${escapeHtml(activity.code)}</span>
            <div>
                <strong>${escapeHtml(activity.name)} · ${days.length} ячеек</strong>
                <p>${escapeHtml(activity.description || (activity.allowSchedule ? 'Обычные пары разрешены' : 'Обычные пары не генерируются'))}</p>
            </div>
        `;
    }

    async function saveTimeSlotDateAssignment() {
        if (!activeCalendarDay) return;
        hideAlert('calendar-day-dialog-time-alert');
        const scopeId = calendarDayDialogTimeScopeSelect.value;
        if (!scopeId) {
            await clearTimeSlotDateAssignment();
            return;
        }
        try {
            await api.post('/api/admin/time-slots/date-assignments', {
                date: activeCalendarDay.dataset.date,
                scopeId: Number(scopeId)
            });
            await loadTimeSlotDateAssignments();
            updateTimeAssignmentVisuals(activeCalendarDay.dataset.date);
            populateTimeScopeDialogSelect(activeCalendarDay.dataset.date);
            syncSelects(calendarDayDialogTimeScopeSelect);
            showAlert('calendar-day-dialog-time-alert', 'Ручная сетка применена к дате', 'success');
        } catch (error) {
            showAlert('calendar-day-dialog-time-alert', error.message || 'Ошибка применения временной сетки', 'error');
        }
    }

    async function clearTimeSlotDateAssignment() {
        if (!activeCalendarDay) return;
        hideAlert('calendar-day-dialog-time-alert');
        const assignment = timeAssignmentForDate(activeCalendarDay.dataset.date);
        if (!assignment) {
            calendarDayDialogTimeScopeSelect.value = '';
            syncSelects(calendarDayDialogTimeScopeSelect);
            showAlert('calendar-day-dialog-time-alert', 'Для даты нет ручной сетки', 'success');
            return;
        }
        try {
            await api.delete('/api/admin/time-slots/date-assignments/' + assignment.id);
            await loadTimeSlotDateAssignments();
            updateTimeAssignmentVisuals(activeCalendarDay.dataset.date);
            populateTimeScopeDialogSelect(activeCalendarDay.dataset.date);
            syncSelects(calendarDayDialogTimeScopeSelect);
            showAlert('calendar-day-dialog-time-alert', 'Ручная сетка убрана', 'success');
        } catch (error) {
            showAlert('calendar-day-dialog-time-alert', error.message || 'Ошибка удаления ручной сетки', 'error');
        }
    }

    function populateTimeScopeDialogSelect(date) {
        const assignment = timeAssignmentForDate(date);
        const manualScopes = timeSlotScopes.filter(scope => scope.applyMode === 'MANUAL');
        const automaticLabel = automaticTimeScopeLabel(date);
        calendarDayDialogTimeScopeSelect.innerHTML = `<option value="">${escapeHtml(automaticLabel)}</option>` +
            manualScopes.map(scope => `<option value="${scope.id}">${escapeHtml(scope.name)}</option>`).join('');
        calendarDayDialogTimeScopeSelect.value = assignment?.scopeId || '';
        calendarDayDialogTimeScopeSelect.disabled = false;
        calendarDayDialogTimeSaveButton.disabled = false;
        calendarDayDialogTimeClearButton.disabled = !assignment;
    }

    function populateMultiSelectionTimeScopeDialogSelect() {
        calendarDayDialogTimeScopeSelect.innerHTML = '<option value="">Для выделения доступен только код дня</option>';
        calendarDayDialogTimeScopeSelect.value = '';
        calendarDayDialogTimeScopeSelect.disabled = true;
        calendarDayDialogTimeSaveButton.disabled = false;
        calendarDayDialogTimeClearButton.disabled = true;
    }

    function updateTimeAssignmentVisuals(date) {
        const assignment = timeAssignmentForDate(date);
        calendarGrid.querySelectorAll(`.calendar-day[data-date="${date}"]`).forEach(day => {
            day.dataset.timeAssignmentId = assignment?.id || '';
            day.dataset.timeScopeId = assignment?.scopeId || '';
            const marker = day.querySelector('.calendar-day-time-marker');
            if (assignment && !marker) {
                day.insertAdjacentHTML('beforeend', '<span class="calendar-day-time-marker">вр.</span>');
            }
            if (!assignment && marker) {
                marker.remove();
            }
        });
    }

    function updateCalendarDayVisual(day) {
        if (!day) return;
        const select = day.querySelector('.calendar-day-activity');
        const activity = activityById(select.value);
        const code = activity?.code || '-';
        day.querySelector('.calendar-day-code').textContent = code;
        day.setAttribute('aria-label', `${formatShortDate(day.dataset.date)} ${shortDayLabel(day.dataset.day)}, ${day.dataset.week} неделя, код ${code}`);
    }

    function showCalendarDayTooltip(day, event = null) {
        if (isCalendarDragSelecting) {
            hideCalendarDayTooltip();
            return;
        }
        const tooltip = ensureCalendarDayTooltip();
        tooltip.innerHTML = renderCalendarDayTooltip(day);
        tooltip.classList.add('visible');
        positionCalendarDayTooltip(tooltip, day, event);
    }

    function hideCalendarDayTooltip() {
        calendarDayTooltip?.classList.remove('visible');
    }

    function positionCalendarDayTooltip(tooltip, day, event) {
        const dayRect = day.getBoundingClientRect();
        const pointerX = Number.isFinite(event?.clientX) ? event.clientX : dayRect.right;
        const pointerY = Number.isFinite(event?.clientY) ? event.clientY : dayRect.bottom;
        const tooltipRect = tooltip.getBoundingClientRect();
        const { left, top } = calculateCalendarTooltipPosition({
            pointerX,
            pointerY,
            tooltipWidth: tooltipRect.width,
            tooltipHeight: tooltipRect.height,
            viewportWidth: window.innerWidth,
            viewportHeight: window.innerHeight
        });
        calendarDayTooltipPositionAnimation?.cancel();
        const positionedTransform = `translate3d(${left}px, ${top}px, 0) scale(1)`;
        calendarDayTooltipPositionAnimation = tooltip.animate(
            [
                { transform: positionedTransform },
                { transform: positionedTransform }
            ],
            { duration: 1, fill: 'forwards' }
        );
    }

    function ensureCalendarDayTooltip() {
        if (calendarDayTooltip) return calendarDayTooltip;
        calendarDayTooltip = document.createElement('div');
        calendarDayTooltip.className = 'calendar-day-tooltip';
        calendarDayTooltip.setAttribute('role', 'tooltip');
        document.body.appendChild(calendarDayTooltip);
        return calendarDayTooltip;
    }

    function renderCalendarDayTooltip(day) {
        const activity = activityById(day.querySelector('.calendar-day-activity')?.value);
        const code = activity?.code || day.querySelector('.calendar-day-code')?.textContent || '-';
        const timeAssignment = timeAssignmentForDate(day.dataset.date);
        const timeLabel = timeAssignment
            ? (timeSlotScopes.find(scope => String(scope.id) === String(timeAssignment.scopeId))?.name || 'ручная сетка')
            : automaticTimeScopeLabel(day.dataset.date).replace('По правилу даты ', '');
        return `
            <div class="calendar-day-tooltip-head">
                <span class="calendar-day-tooltip-code"><span>${escapeHtml(code)}</span></span>
                <div>
                    <strong>${escapeHtml(formatLongDate(day.dataset.date))}</strong>
                    <span>${escapeHtml(shortDayLabel(day.dataset.day))}, ${escapeHtml(day.dataset.week)} неделя</span>
                </div>
            </div>
            <div class="calendar-day-tooltip-body">
                <div>
                    <span>Курс</span>
                    <strong>${escapeHtml(day.dataset.course)}</strong>
                </div>
                <div>
                    <span>Активность</span>
                    <strong>${escapeHtml(activity?.name || 'Не задана')}</strong>
                </div>
                <div>
                    <span>Время</span>
                    <strong>${escapeHtml(timeLabel)}</strong>
                </div>
            </div>
        `;
    }

    function startCalendarDayDragSelection(event, day) {
        if (event.button !== 0) return;
        hideCalendarDayTooltip();
        isCalendarDragSelecting = true;
        didCalendarDragSelect = false;
        suppressCalendarDayClick = false;
        selectCalendarDays([day]);
        event.preventDefault();
    }

    function addCalendarDayToDragSelection(day) {
        if (!isCalendarDragSelecting || selectedCalendarDays.includes(day)) return;
        didCalendarDragSelect = true;
        selectedCalendarDays.push(day);
        day.classList.add('is-selected');
        renderCalendarTotals();
    }

    function finishCalendarDayDragSelection() {
        if (!isCalendarDragSelecting) return;
        isCalendarDragSelecting = false;
        if (didCalendarDragSelect && selectedCalendarDays.length > 1) {
            suppressCalendarDayClick = true;
            openCalendarDayDialogForSelection(selectedCalendarDays);
        }
    }

    function selectCalendarDay(day) {
        selectCalendarDays(day ? [day] : []);
    }

    function selectCalendarDays(days) {
        const uniqueDays = uniqueCalendarDays(days);
        selectedCalendarDays = uniqueDays;
        selectedCalendarDayKey = uniqueDays[0] ? calendarDayKey(uniqueDays[0]) : null;
        calendarGrid.querySelectorAll('.calendar-day.is-selected').forEach(item => item.classList.remove('is-selected'));
        uniqueDays.forEach(item => item.classList.add('is-selected'));
        renderCalendarTotals();
    }

    function uniqueCalendarDays(days) {
        return Array.from(new Set(days.filter(Boolean)));
    }

    function commonCalendarDayActivityValue(days) {
        const values = uniqueCalendarDays(days).map(day => day.querySelector('.calendar-day-activity')?.value || '');
        const uniqueValues = Array.from(new Set(values));
        return uniqueValues.length === 1 ? uniqueValues[0] : '';
    }

    function calendarDayKey(day) {
        return `${day.dataset.course}:${day.dataset.date}`;
    }

    function buildDefaultGrid(calendar) {
        const rows = [];
        const dates = enumerateDates(calendar.academicYearStartDate, calendar.academicYearEndDate);
        for (let course = 1; course <= calendar.courseCount; course += 1) {
            dates.forEach(date => {
                rows.push({
                    id: null,
                    calendarId: calendar.id,
                    courseNumber: course,
                    date: date.iso,
                    weekNumber: academicCalendarWeekNumber(calendar.academicYearStartDate, date.iso),
                    dayOfWeek: date.dayOfWeek,
                    activityTypeId: theoryActivityId(),
                    activityCode: 'Т'
                });
            });
        }
        return rows;
    }

    function semesterBlocksForCourse(calendar, course, courseRows) {
        const sortedRows = [...courseRows].sort((a, b) => String(a.date).localeCompare(String(b.date)));
        const year = academicYears.find(item => String(item.id) === String(calendar?.academicYearId));
        const yearSemesters = [...(year?.semesters || [])]
            .sort((a, b) => String(a.startDate).localeCompare(String(b.startDate)));

        if (!yearSemesters.length) {
            return [{
                semesterNumber: `${course * 2 - 1}-${course * 2}`,
                caption: dateRangeCaption(sortedRows),
                rows: sortedRows
            }];
        }

        return yearSemesters.map((semester, index) => {
            const nextSemester = yearSemesters[index + 1];
            const startDate = index === 0
                ? minIso(calendar.academicYearStartDate, semester.startDate)
                : semester.startDate;
            const endDate = nextSemester
                ? previousIsoDate(nextSemester.startDate)
                : maxIso(calendar.academicYearEndDate, semester.endDate);
            const rows = sortedRows.filter(row => row.date >= startDate && row.date <= endDate);
            return {
                semesterNumber: course * 2 - 1 + index,
                caption: dateRangeCaption(rows) || `${formatLongDate(startDate)} - ${formatLongDate(endDate)}`,
                rows
            };
        });
    }

    function renderCalendarTotals() {
        const counts = new Map();
        calendarGrid.querySelectorAll('.calendar-day-activity').forEach(select => {
            const activity = activityById(select.value);
            const key = activity ? activity.code : '-';
            counts.set(key, (counts.get(key) || 0) + 1);
        });
        const selectedDays = selectedCalendarDays.length
            ? selectedCalendarDays
            : (selectedCalendarDayKey
                ? [Array.from(calendarGrid.querySelectorAll('.calendar-day')).find(day => calendarDayKey(day) === selectedCalendarDayKey)].filter(Boolean)
                : []);
        const selectedActivity = selectedDays.length ? activityById(commonCalendarDayActivityValue(selectedDays)) : null;
        const selectedCode = selectedActivity?.code || null;
        calendarTotals.innerHTML = Array.from(counts.entries())
            .sort(([a], [b]) => a.localeCompare(b, 'ru'))
            .map(([code, count]) => {
                const selectedClass = selectedCode === code ? ' is-selected' : '';
                const suffix = selectedCode === code ? '<span class="calendar-total-selected">выбрано</span>' : '';
                return `<span class="calendar-total-chip${selectedClass}">${escapeHtml(code)}: ${count}${suffix}</span>`;
            }).join('');
    }

    function populateYearSelects() {
        const yearOptions = '<option value="">Выберите учебный год</option>' +
            academicYears.map(year => `<option value="${year.id}">${escapeHtml(year.title)}</option>`).join('');
        semesterYearSelect.innerHTML = yearOptions;
        calendarYearSelect.innerHTML = yearOptions;
        syncSelects(semesterYearSelect, calendarYearSelect);
    }

    function populateSpecialtySelects() {
        calendarSpecialtySelect.innerHTML = '<option value="">Выберите специальность</option>' +
            specialties.map(speciality => `<option value="${speciality.id}">${escapeHtml(specialityLabel(speciality))}</option>`).join('');
        populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value);
        syncSelects(calendarSpecialtySelect);
    }

    function populateProfileSelect(select, specialtyId, selectedProfileId = null) {
        const profiles = profilesBySpecialty.get(String(specialtyId)) || [];
        select.innerHTML = profiles.length
            ? '<option value="">Выберите профиль</option>' + profiles.map(profile =>
                `<option value="${profile.id}">${escapeHtml(profile.name)}</option>`
            ).join('')
            : '<option value="">Нет профилей</option>';
        if (selectedProfileId && profiles.some(profile => String(profile.id) === String(selectedProfileId))) {
            select.value = selectedProfileId;
        }
        syncSelects(select);
    }

    function populateStudyFormSelect() {
        calendarStudyFormSelect.innerHTML = '<option value="">Выберите форму</option>' +
            studyForms.map(form => `<option value="${form.id}">${escapeHtml(form.name)}</option>`).join('');
        syncSelects(calendarStudyFormSelect);
    }

    function populateSubjectSelect() {
        if (!subjectSelect) return;
        const current = subjectSelect.value;
        const sortedSubjects = [...subjects].sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'ru'));
        subjectSelect.innerHTML = sortedSubjects.length
            ? '<option value="">Выберите дисциплину</option>' + sortedSubjects.map(subject =>
                `<option value="${subject.id}">${escapeHtml(subjectLabel(subject))}</option>`
            ).join('')
            : '<option value="">Нет дисциплин</option>';
        if (current && sortedSubjects.some(subject => String(subject.id) === String(current))) {
            subjectSelect.value = current;
        }
        syncSelects(subjectSelect);
    }

    function populateActivitySelects() {
        const activityOptions = '<option value="">Выберите код</option>' +
            activityTypes.map(type => `<option value="${type.id}">${escapeHtml(type.code)} - ${escapeHtml(type.name)}</option>`).join('');
        fillActivitySelect.innerHTML = activityOptions;
        syncSelects(fillActivitySelect);
    }

    function populateEditorCalendarSelect() {
        const current = editorCalendarSelect.value;
        editorCalendarSelect.innerHTML = '<option value="">Выберите график</option>' +
            calendars.map(calendar =>
                `<option value="${calendar.id}">${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</option>`
            ).join('');
        if (current && calendars.some(calendar => String(calendar.id) === String(current))) {
            editorCalendarSelect.value = current;
        }
        syncSelects(editorCalendarSelect);
    }

    function populateSubjectCalendarSelect() {
        if (!subjectCalendarSelect) return;
        const current = subjectCalendarSelect.value;
        subjectCalendarSelect.innerHTML = calendars.length
            ? '<option value="">Выберите график</option>' + calendars.map(calendar =>
                `<option value="${calendar.id}">${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</option>`
            ).join('')
            : '<option value="">Нет графиков</option>';
        if (current && calendars.some(calendar => String(calendar.id) === String(current))) {
            subjectCalendarSelect.value = current;
        }
        populateSubjectSemesterSelect();
        syncSelects(subjectCalendarSelect);
    }

    function populateSubjectSemesterSelect() {
        if (!subjectSemesterSelect) return;
        const calendar = selectedSubjectCalendar();
        const current = subjectSemesterSelect.value;
        if (!calendar) {
            subjectSemesterSelect.innerHTML = '<option value="">Выберите график</option>';
            syncSelects(subjectSemesterSelect);
            return;
        }
        const semesterCount = calendarSemesterCount(calendar);
        subjectSemesterSelect.innerHTML = Array.from({ length: semesterCount }, (_, index) => {
            const semesterNumber = index + 1;
            return `<option value="${semesterNumber}">${escapeHtml(semesterOptionLabel(semesterNumber))}</option>`;
        }).join('');
        if (current && Number(current) >= 1 && Number(current) <= semesterCount) {
            subjectSemesterSelect.value = current;
        }
        syncSelects(subjectSemesterSelect);
    }

    function populateFillCourseSelect() {
        const courses = new Set(Array.from(calendarGrid.querySelectorAll('.calendar-day')).map(day => day.dataset.course));
        fillCourseSelect.innerHTML = '<option value="">Все курсы</option>' +
            Array.from(courses).sort((a, b) => Number(a) - Number(b))
                .map(course => `<option value="${course}">${course} курс</option>`).join('');
        syncSelects(fillCourseSelect);
    }

    function options(items, selectedValue) {
        const selected = selectedValue == null ? '' : String(selectedValue);
        return '<option value="">Выберите</option>' + items.map(item => {
            const value = String(item.value);
            return `<option value="${escapeHtml(value)}" ${value === selected ? 'selected' : ''}>${escapeHtml(item.label)}</option>`;
        }).join('');
    }

    function toActivityOption(type) {
        return { value: type.id, label: `${type.code} - ${type.name}` };
    }

    function selectedEditorCalendar() {
        return calendars.find(calendar => String(calendar.id) === String(editorCalendarSelect.value));
    }

    function selectedSubjectCalendar() {
        return calendars.find(calendar => String(calendar.id) === String(subjectCalendarSelect?.value));
    }

    function calendarSemesterCount(calendar) {
        return Math.max(1, Number(calendar?.courseCount || 0) * 2);
    }

    function semesterOptionLabel(semesterNumber) {
        const course = Math.ceil(Number(semesterNumber) / 2);
        const type = Number(semesterNumber) % 2 === 1 ? 'осенний' : 'весенний';
        return `${semesterNumber} семестр · ${course} курс, ${type}`;
    }

    function sortCalendarSubjectRows(rows) {
        return [...rows].sort((a, b) => {
            const semesterDiff = Number(a.semesterNumber) - Number(b.semesterNumber);
            if (semesterDiff !== 0) return semesterDiff;
            return subjectRowLabel(a).localeCompare(subjectRowLabel(b), 'ru');
        });
    }

    function subjectLabel(subject) {
        const code = subject.code ? `${subject.code} · ` : '';
        return `${code}${subject.name || 'Без названия'}`;
    }

    function subjectRowLabel(row) {
        const code = row.subjectCode ? `${row.subjectCode} · ` : '';
        return `${code}${row.subjectName || row.subjectId || 'Без названия'}`;
    }

    function theoryActivityId() {
        return activityTypes.find(type => type.code === 'Т')?.id || activityTypes[0]?.id || '';
    }

    function activityIdByCode(code) {
        return activityTypes.find(type => type.code === code)?.id || null;
    }

    function activityById(id) {
        return activityTypes.find(type => String(type.id) === String(id));
    }

    function timeAssignmentForDate(date) {
        return timeSlotDateAssignments.find(item => item.date === date);
    }

    function automaticTimeScopeLabel(date) {
        const jsDate = new Date(`${date}T00:00:00`);
        const jsDay = jsDate.getDay();
        const isoDay = jsDay === 0 ? 7 : jsDay;
        const weekdayScope = timeSlotScopes.find(scope => scope.applyMode === 'WEEKDAY' && Number(scope.dayOfWeek) === isoDay);
        const defaultScope = timeSlotScopes.find(scope => scope.applyMode === 'DEFAULT');
        const scopeName = weekdayScope?.name || defaultScope?.name || 'базовая сетка';
        return `По правилу даты (${scopeName})`;
    }

    function semesterLabel(semester) {
        const type = normalizeSemesterType(semester.semesterType);
        return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${formatDisplayDateRange(semester.startDate, semester.endDate)})`;
    }

    function semesterTypeTitle(semester) {
        const type = normalizeSemesterType(semester.semesterType);
        const label = SEMESTER_LABELS[type] || type || 'семестр';
        return label.charAt(0).toUpperCase() + label.slice(1);
    }

    function normalizeSemesterType(type) {
        return String(type || '').toLowerCase();
    }

    function specialityLabel(speciality) {
        const code = specialityCode(speciality) || speciality.id;
        const name = speciality.specialityName || speciality.name || '';
        return name ? `${code} - ${name}` : String(code);
    }

    function specialityCode(speciality) {
        return speciality?.specialityCode
            || speciality?.specialtyCode
            || speciality?.speciality_code
            || speciality?.specialty_code
            || '';
    }

    function dayLabel(value) {
        return DAY_OPTIONS.find(day => String(day.value) === String(value))?.label || '-';
    }

    function shortDayLabel(value) {
        return DAY_OPTIONS.find(day => String(day.value) === String(value))?.shortLabel || '-';
    }

    function formatShortDate(value) {
        const [year, month, day] = String(value).split('-');
        return `${day}.${month}`;
    }

    function formatLongDate(value) {
        const [year, month, day] = String(value).split('-');
        return `${day}.${month}.${year}`;
    }

    function formatDisplayDate(value) {
        const [year, month, day] = String(value || '').split('-');
        if (!year || !month || !day) return String(value || '');
        return `${day}-${month}-${year}`;
    }

    function formatDisplayDateRange(startDate, endDate) {
        return `${formatDisplayDate(startDate)} — ${formatDisplayDate(endDate)}`;
    }

    function dateRangeCaption(rows) {
        if (!rows.length) return '';
        const dates = rows.map(row => row.date).sort();
        return `${formatLongDate(dates[0])} - ${formatLongDate(dates[dates.length - 1])}`;
    }

    function minIso(a, b) {
        return String(a) <= String(b) ? a : b;
    }

    function maxIso(a, b) {
        return String(a) >= String(b) ? a : b;
    }

    function previousIsoDate(value) {
        const date = parseLocalIsoDate(value);
        date.setDate(date.getDate() - 1);
        return formatLocalDate(date);
    }

    function enumerateDates(startIso, endIso) {
        const result = [];
        const current = parseLocalIsoDate(startIso);
        const end = parseLocalIsoDate(endIso);
        while (current <= end) {
            const iso = formatLocalDate(current);
            const jsDay = current.getDay();
            result.push({ iso, dayOfWeek: jsDay === 0 ? 7 : jsDay });
            current.setDate(current.getDate() + 1);
        }
        return result;
    }

    function parseLocalIsoDate(value) {
        const [year, month, day] = String(value).split('-').map(Number);
        const date = new Date(year, month - 1, day, 12);
        if (!year || !month || !day || Number.isNaN(date.getTime())
            || date.getFullYear() !== year
            || date.getMonth() !== month - 1
            || date.getDate() !== day) {
            throw new TypeError('Ожидалась дата в формате ГГГГ-ММ-ДД');
        }
        return date;
    }

    function groupBy(items, keyFn) {
        const grouped = new Map();
        items.forEach(item => {
            const key = keyFn(item);
            if (!grouped.has(key)) grouped.set(key, []);
            grouped.get(key).push(item);
        });
        return grouped;
    }

    function syncSelects(...selects) {
        selects.filter(Boolean).forEach(select => {
            refreshSelectVisuals(select);
            select.dispatchEvent(new Event('change', { bubbles: true }));
        });
    }

    function refreshSelectVisuals(...selects) {
        selects.filter(Boolean).forEach(select => {
            if (select.customSelectInstance) {
                select.customSelectInstance.rebuildMenu();
            }
        });
    }
}
