import { api, getSession } from '../api.js';
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
import { canEditScheduleOverrides, createScheduleOverridePanel } from './schedule-override-panel.js';

const ROLE_DEPARTMENT = 'DEPARTMENT';
const TARGET_GROUPS = 'groups';
const TARGET_TEACHERS = 'teachers';
const TARGET_CLASSROOMS = 'classrooms';
const TARGET_DEPARTMENTS = 'departments';
const PARITY_LABELS = {
    BOTH: 'каждая',
    ODD: 'нечётная',
    EVEN: 'чётная'
};
const DAY_SHORT_LABELS = {
    1: 'Пн',
    2: 'Вт',
    3: 'Ср',
    4: 'Чт',
    5: 'Пт',
    6: 'Сб',
    7: 'Вс'
};
const DAY_FULL_LABELS = {
    1: 'Понедельник',
    2: 'Вторник',
    3: 'Среда',
    4: 'Четверг',
    5: 'Пятница',
    6: 'Суббота',
    7: 'Воскресенье'
};
const DAY_ORDER = [1, 2, 3, 4, 5, 6, 7];
const CELL_PARITY_LAYOUT = [
    { parity: 'ODD', label: 'Нечётная' },
    { parity: 'EVEN', label: 'Чётная' }
];
const MOBILE_QUERY = '(max-width: 760px)';
const DEFAULT_SEMESTER_WEEKS = 22;
const SCHEDULE_REFRESH_PROMPT = 'Нажмите «Показать», чтобы обновить расписание';

export async function initScheduleView() {
    const session = getSession();
    const role = session?.role;
    const userDepartmentId = normalizeId(session?.departmentId);
    const dateInput = document.getElementById('schedule-view-date');
    const targetSelect = document.getElementById('schedule-view-target');
    const loadButton = document.getElementById('schedule-view-load');
    const previousPeriodButton = document.getElementById('schedule-view-prev-period');
    const todayButton = document.getElementById('schedule-view-today');
    const nextPeriodButton = document.getElementById('schedule-view-next-period');
    const resultPreviousButton = document.getElementById('schedule-view-result-prev');
    const resultNextButton = document.getElementById('schedule-view-result-next');
    const resultTabs = document.getElementById('schedule-view-result-tabs');
    const activeLabel = document.getElementById('schedule-view-active-label');
    const countLabel = document.getElementById('schedule-view-count');
    const periodLabel = document.getElementById('schedule-view-period-label');
    const tableTitle = document.getElementById('schedule-view-table-title');
    const tableCaption = document.getElementById('schedule-view-table-caption');
    const dayTabs = document.getElementById('schedule-view-day-tabs');
    const tables = document.getElementById('schedule-view-tables');
    const entityFields = Array.from(document.querySelectorAll('[data-schedule-view-target-field]'));
    const departmentSelect = document.getElementById('schedule-view-department');
    const semesterSelect = document.getElementById('schedule-view-semester');

    const state = {
        dictionaries: {
            groups: [],
            teachers: [],
            classrooms: [],
            departments: []
        },
        sections: [],
        activeSectionIndex: 0,
        mobileDay: 1,
        range: null,
        totalLessons: 0,
        lessons: [],
        overrides: []
    };
    let overridePanel = createEmptyOverridePanel();
    const mobileMedia = window.matchMedia(MOBILE_QUERY);

    setDefaultDate(dateInput);
    configureDepartmentTarget();
    updatePeriodLabel();
    updateEntityFieldVisibility();
    renderResultNavigation();

    targetSelect?.addEventListener('change', () => {
        clearInactiveEntityValues();
        updateEntityFieldVisibility();
        resetResults(SCHEDULE_REFRESH_PROMPT);
    });
    loadButton?.addEventListener('click', loadData);
    previousPeriodButton?.addEventListener('click', () => movePeriod(-1));
    todayButton?.addEventListener('click', () => {
        dateInput.value = toIsoDate(new Date());
        loadData();
    });
    nextPeriodButton?.addEventListener('click', () => movePeriod(1));
    dateInput?.addEventListener('change', () => {
        updatePeriodLabel();
        resetResults(SCHEDULE_REFRESH_PROMPT);
    });
    semesterSelect?.addEventListener('change', () => {
        synchronizeDateWithSemester();
        updatePeriodLabel();
        resetResults(SCHEDULE_REFRESH_PROMPT);
    });
    resultPreviousButton?.addEventListener('click', () => moveResult(-1));
    resultNextButton?.addEventListener('click', () => moveResult(1));
    tables?.addEventListener('click', event => {
        const reloadButton = event.target.closest('[data-schedule-view-reload]');
        if (reloadButton) {
            loadData();
            return;
        }
        const button = event.target.closest('[data-schedule-override-edit]');
        if (!button) return;
        const lesson = state.lessons.find(item =>
            String(item.scheduleRuleSlotId) === button.dataset.baseRuleSlotId
            && (item.originalLessonDate || item.date) === button.dataset.lessonDate
        );
        if (lesson) overridePanel.openLesson(lesson);
    });
    if (typeof mobileMedia.addEventListener === 'function') {
        mobileMedia.addEventListener('change', renderActiveSection);
    } else if (typeof mobileMedia.addListener === 'function') {
        mobileMedia.addListener(renderActiveSection);
    }

    try {
        state.dictionaries = await loadDictionaries(role, userDepartmentId);
        selectCurrentSemester();
        overridePanel = createScheduleOverridePanel({
            role,
            dictionaries: state.dictionaries,
            getRange: () => state.range,
            onChanged: loadData
        });
        configureDepartmentAccess();
        updateEntityFieldVisibility();
        await loadData();
    } catch (error) {
        const message = error.message || 'Ошибка загрузки справочников';
        showAlert('schedule-view-alert', message, 'error');
        resetResults(message);
    }

    function configureDepartmentTarget() {
        if (role !== ROLE_DEPARTMENT) return;
        targetSelect.value = TARGET_DEPARTMENTS;
        targetSelect.disabled = true;
    }

    function configureDepartmentAccess() {
        if (role !== ROLE_DEPARTMENT) return;
        targetSelect.value = TARGET_DEPARTMENTS;
        targetSelect.disabled = true;
        if (departmentSelect) {
            departmentSelect.value = userDepartmentId || '';
            departmentSelect.disabled = true;
            if (userDepartmentId && departmentSelect.value !== userDepartmentId) {
                departmentSelect.insertAdjacentHTML('afterbegin',
                    `<option value="${escapeHtml(userDepartmentId)}">Своя кафедра</option>`);
                departmentSelect.value = userDepartmentId;
            }
        }
    }

    function updateEntityFieldVisibility() {
        const target = activeTarget();
        entityFields.forEach(field => {
            field.hidden = field.dataset.scheduleViewTargetField !== target;
        });
    }

    function clearInactiveEntityValues() {
        const target = activeTarget();
        [
            [TARGET_GROUPS, 'schedule-view-group'],
            [TARGET_TEACHERS, 'schedule-view-teacher'],
            [TARGET_CLASSROOMS, 'schedule-view-classroom'],
            [TARGET_DEPARTMENTS, 'schedule-view-department']
        ].forEach(([fieldTarget, id]) => {
            if (fieldTarget === target) return;
            if (role === ROLE_DEPARTMENT && id === 'schedule-view-department') return;
            const select = document.getElementById(id);
            if (select) select.value = '';
        });
    }

    function activeTarget() {
        if (role === ROLE_DEPARTMENT) return TARGET_DEPARTMENTS;
        return targetSelect?.value || TARGET_GROUPS;
    }

    async function movePeriod(direction) {
        if (!dateInput.value) {
            dateInput.value = toIsoDate(new Date());
        }
        const range = twoWeekRange(dateInput.value);
        const nextDate = addDays(parseIsoDate(range.startDate), direction * 14);
        dateInput.value = toIsoDate(nextDate);
        await loadData();
    }

    async function loadData() {
        hideAlert('schedule-view-alert');

        if (!dateInput.value) {
            const message = 'Выберите дату в периоде';
            showAlert('schedule-view-alert', message, 'error');
            resetResults(message);
            return;
        }

        if (role === ROLE_DEPARTMENT && !userDepartmentId) {
            const message = 'Не удалось определить кафедру пользователя';
            showAlert('schedule-view-alert', message, 'error');
            resetResults(message);
            return;
        }

        const target = activeTarget();
        const range = scheduleRangeForSemester(dateInput.value, selectedSemester());
        const previousKey = state.sections[state.activeSectionIndex]?.key;
        state.range = range;
        updatePeriodLabel(range);
        setTableState('Загрузка...');
        setActiveSummary('Загрузка расписаний...');
        countLabel.textContent = 'Загрузка...';
        resultTabs.innerHTML = '';
        dayTabs.hidden = true;
        dayTabs.innerHTML = '';

        const params = new URLSearchParams({
            startDate: range.startDate,
            endDate: range.endDate
        });
        applyTargetFilter(params, target);
        addOptional(params, 'subjectId', valueOf('schedule-view-subject'));
        addOptional(params, 'lessonTypeId', valueOf('schedule-view-lesson-type'));
        addOptional(params, 'parity', valueOf('schedule-view-parity'));

        try {
            const [lessons, overrides] = await Promise.all([
                api.get(`/api/schedule/search?${params}`),
                canEditScheduleOverrides(role)
                    ? api.get(`/api/edu-office/schedule/overrides?startDate=${range.startDate}&endDate=${range.endDate}`)
                    : Promise.resolve([])
            ]);
            state.overrides = overrides;
            overridePanel.setRegistry(overrides, range);
            renderLessons(lessons, range, target, previousKey);
        } catch (error) {
            const message = error.message || 'Ошибка загрузки расписания';
            showAlert('schedule-view-alert', message, 'error');
            resetResults(message);
        }
    }

    function applyTargetFilter(params, target) {
        if (role === ROLE_DEPARTMENT) {
            params.set('departmentId', userDepartmentId);
            return;
        }
        if (target === TARGET_GROUPS) {
            addOptional(params, 'groupId', valueOf('schedule-view-group'));
            return;
        }
        if (target === TARGET_TEACHERS) {
            addOptional(params, 'teacherId', valueOf('schedule-view-teacher'));
            return;
        }
        if (target === TARGET_CLASSROOMS) {
            addOptional(params, 'classroomId', valueOf('schedule-view-classroom'));
            return;
        }
        if (target === TARGET_DEPARTMENTS) {
            addOptional(params, 'departmentId', valueOf('schedule-view-department'));
        }
    }

    function renderLessons(lessons, range, target, previousKey) {
        state.lessons = lessons;
        state.totalLessons = lessons.length;
        if (!lessons.length) {
            state.sections = [];
            state.activeSectionIndex = 0;
            renderResultNavigation();
            tableTitle.textContent = 'Расписание';
            tableCaption.textContent = periodCaption(range);
            setTableState('Занятия не найдены');
            countLabel.textContent = '0 занятий';
            return;
        }

        const sections = buildScheduleSections(lessons, splitTypeForTarget(target), buildSectionContext(target));
        if (!sections.length) {
            state.sections = [];
            state.activeSectionIndex = 0;
            renderResultNavigation();
            tableTitle.textContent = 'Расписание';
            tableCaption.textContent = periodCaption(range);
            setTableState('Нет данных для выбранного режима просмотра');
            countLabel.textContent = lessonCountLabel(lessons.length);
            return;
        }

        state.sections = sections;
        state.activeSectionIndex = Math.max(0, sections.findIndex(section => section.key === previousKey));
        if (state.activeSectionIndex < 0) {
            state.activeSectionIndex = 0;
        }
        state.mobileDay = preferredMobileDay(range);
        renderResultNavigation();
        renderActiveSection();
    }

    function buildSectionContext(target) {
        const selectedGroupId = target === TARGET_GROUPS ? normalizeId(valueOf('schedule-view-group')) : '';
        const selectedDepartmentId = role === ROLE_DEPARTMENT
            ? userDepartmentId
            : (target === TARGET_DEPARTMENTS ? normalizeId(valueOf('schedule-view-department')) : '');
        const allowedGroupIds = new Set();
        if (selectedGroupId) {
            allowedGroupIds.add(selectedGroupId);
        } else if (selectedDepartmentId) {
            state.dictionaries.groups
                .filter(group => String(group.departmentId) === String(selectedDepartmentId))
                .forEach(group => allowedGroupIds.add(String(group.id)));
        }
        return {
            selectedGroupId,
            selectedGroupName: selectedGroupId ? groupNameById(selectedGroupId) : '',
            allowedGroupIds,
            groupNamesById: new Map(state.dictionaries.groups.map(group => [String(group.id), group.name]))
        };
    }

    function splitTypeForTarget(target) {
        if (target === TARGET_TEACHERS) return TARGET_TEACHERS;
        if (target === TARGET_CLASSROOMS) return TARGET_CLASSROOMS;
        return TARGET_GROUPS;
    }

    function renderResultNavigation() {
        const section = activeSection();
        const totalSections = state.sections.length;
        countLabel.textContent = state.totalLessons
            ? `${lessonCountLabel(state.totalLessons)}, ${sectionCountLabel(totalSections)}`
            : '0 занятий';

        resultPreviousButton.disabled = totalSections < 2;
        resultNextButton.disabled = totalSections < 2;

        if (!section) {
            setActiveSummary('Нет выбранного расписания');
            resultTabs.innerHTML = '<span class="schedule-view-empty-result">Ничего не найдено</span>';
            return;
        }

        setActiveSummary(`${section.title} · ${lessonCountLabel(section.lessons.length)} · ${state.activeSectionIndex + 1} из ${totalSections}`);
        resultTabs.innerHTML = state.sections.map((item, index) => `
            <button type="button"
                    class="schedule-view-result-tab${index === state.activeSectionIndex ? ' active' : ''}"
                    data-schedule-view-section="${index}">
                <span>${escapeHtml(item.title)}</span>
                <small>${escapeHtml(lessonCountLabel(item.lessons.length))}</small>
            </button>
        `).join('');
        resultTabs.querySelectorAll('[data-schedule-view-section]').forEach(button => {
            button.addEventListener('click', () => {
                state.activeSectionIndex = Number(button.dataset.scheduleViewSection);
                state.mobileDay = preferredMobileDay(state.range);
                renderResultNavigation();
                renderActiveSection();
            });
        });
    }

    function setActiveSummary(text) {
        activeLabel.textContent = text;
    }

    function moveResult(direction) {
        if (state.sections.length < 2) return;
        state.activeSectionIndex = (state.activeSectionIndex + direction + state.sections.length) % state.sections.length;
        state.mobileDay = preferredMobileDay(state.range);
        renderResultNavigation();
        renderActiveSection();
    }

    function activeSection() {
        return state.sections[state.activeSectionIndex] || null;
    }

    function renderActiveSection() {
        const section = activeSection();
        if (!section || !state.range) {
            dayTabs.hidden = true;
            dayTabs.innerHTML = '';
            return;
        }

        tableTitle.textContent = section.title;
        tableCaption.textContent = `${lessonCountLabel(section.lessons.length)} · ${periodCaption(state.range)}`;

        if (mobileMedia.matches) {
            renderMobileDayTabs();
            renderMobileSchedule(section, state.range);
            return;
        }

        dayTabs.hidden = true;
        dayTabs.innerHTML = '';
        renderDesktopSchedule(section, state.range);
    }

    function renderDesktopSchedule(section, range) {
        const slots = collectSlots(section.lessons);
        const lessonsByDateSlot = groupLessonsByDateSlot(section.lessons);
        const blocks = buildCombinedWeekBlocks(range.startDate, range.endDate, section.lessons);
        if (!blocks.length || !slots.length) {
            setTableState('Занятия не найдены');
            return;
        }
        tables.innerHTML = blocks
            .map(block => renderCombinedWeekTable(block, slots, lessonsByDateSlot, section))
            .join('');
    }

    function renderMobileDayTabs() {
        dayTabs.hidden = false;
        dayTabs.innerHTML = DAY_ORDER.map(day => `
            <button type="button"
                    class="schedule-view-day-tab${day === state.mobileDay ? ' active' : ''}"
                    data-schedule-view-day="${day}">
                ${escapeHtml(dayShort(day))}
            </button>
        `).join('');
        dayTabs.querySelectorAll('[data-schedule-view-day]').forEach(button => {
            button.addEventListener('click', () => {
                state.mobileDay = Number(button.dataset.scheduleViewDay);
                renderActiveSection();
            });
        });
    }

    function renderMobileSchedule(section, range) {
        const slots = collectSlots(section.lessons);
        const lessonsByDateSlot = groupLessonsByDateSlot(section.lessons);
        const blocks = buildCombinedWeekBlocks(range.startDate, range.endDate, section.lessons);
        if (!blocks.length || !slots.length) {
            setTableState('Занятия не найдены');
            return;
        }
        tables.innerHTML = blocks.map(block => renderMobileDayBlock(block, slots, lessonsByDateSlot)).join('');
    }

    function renderMobileDayBlock(block, slots, lessonsByDateSlot) {
        const dayDates = block.datesByDay[state.mobileDay] || {};
        const rows = slots.map(slot => {
            const states = slotStatesForDates(dayDates, slot, lessonsByDateSlot);
            return `
                <article class="schedule-view-mobile-slot">
                    <div class="schedule-view-mobile-slot-time">
                        <strong>${escapeHtml(slot.orderLabel)}</strong>
                        <span>${escapeHtml(slot.timeLabel)}</span>
                    </div>
                    <div class="schedule-view-mobile-slot-content">
                        ${renderSlotContent(states)}
                    </div>
                </article>
            `;
        }).join('');

        return `
            <section class="schedule-view-mobile-day">
                <div class="schedule-view-mobile-day-heading">
                    <strong>${escapeHtml(DAY_FULL_LABELS[state.mobileDay] || dayShort(state.mobileDay))}</strong>
                    <span>${escapeHtml(mobileDayCaption(dayDates))}</span>
                </div>
                ${rows}
            </section>
        `;
    }

    function renderCombinedWeekTable(block, slots, lessonsByDateSlot, section) {
        const dayHeaders = DAY_ORDER.map(day => `
            <th class="room-day-cell">
                <strong>${escapeHtml(dayShort(day))}</strong>
                <span>${escapeHtml(DAY_FULL_LABELS[day] || '')}</span>
            </th>
        `).join('');
        const rows = slots.map(slot => `
            <tr>
                <td class="room-time-cell">
                    <strong>${escapeHtml(slot.orderLabel)}</strong>
                    <span>${escapeHtml(slot.timeLabel)}</span>
                    ${slot.id ? `<small>ID ${escapeHtml(slot.id)}</small>` : ''}
                </td>
                ${DAY_ORDER.map(day => renderCombinedSlotCell(block.datesByDay[day] || {}, slot, lessonsByDateSlot)).join('')}
            </tr>
        `).join('');

        return `
            <section class="schedule-view-week-panel">
                <div class="room-week-heading">
                    <h3>${escapeHtml(section.title)}</h3>
                    <span>${escapeHtml(combinedWeekCaption(block))}</span>
                </div>
                <div class="workload-grid-container schedule-view-grid-container">
                    <table class="workload-table room-week-table schedule-view-matrix-table">
                        <thead>
                            <tr>
                                <th class="room-time-heading">Время</th>
                                ${dayHeaders}
                            </tr>
                        </thead>
                        <tbody>${rows}</tbody>
                    </table>
                </div>
            </section>
        `;
    }

    function renderDayParityDate(date, label) {
        return `<span>${escapeHtml(label)}: ${date ? escapeHtml(formatDateShort(date)) : '-'}</span>`;
    }

    function renderCombinedSlotCell(datesByParity, slot, lessonsByDateSlot) {
        const states = slotStatesForDates(datesByParity, slot, lessonsByDateSlot);
        const singleCellClass = slotStatesEqual(states[0].state, states[1].state) ? ' room-single-cell' : '';
        return `<td class="room-combined-cell${singleCellClass}">${renderSlotContent(states)}</td>`;
    }

    function slotStatesForDates(datesByParity, slot, lessonsByDateSlot) {
        return CELL_PARITY_LAYOUT.map(({ parity, label }) => ({
            parity,
            label,
            state: scheduleSlotState(datesByParity[parity], slot, lessonsByDateSlot)
        }));
    }

    function renderSlotContent(states) {
        if (slotStatesEqual(states[0].state, states[1].state)) {
            return renderUnifiedSlot(states[0].state);
        }
        return states.map(({ parity, label, state }) =>
            renderSlotHalf(state, parity, label)
        ).join('');
    }

    function scheduleSlotState(date, slot, lessonsByDateSlot) {
        if (!date) {
            return { kind: 'empty', lessons: [] };
        }
        const lessons = lessonsByDateSlot.get(dateSlotKey(date, slot.key)) || [];
        if (lessons.length) {
            return { kind: 'busy', lessons };
        }
        return { kind: 'free', lessons: [] };
    }

    function renderUnifiedSlot(state) {
        if (state.kind === 'busy') {
            return `<div class="room-week-unified room-week-unified-busy">${state.lessons.map(renderLessonCard).join('')}</div>`;
        }
        if (state.kind === 'empty') {
            return '<div class="room-week-unified room-week-unified-empty"><span class="slot-unavailable">Нет даты</span></div>';
        }
        return '<div class="room-week-unified room-week-unified-free"><span class="free-slot">Нет занятий</span></div>';
    }

    function renderSlotHalf(state, parity, label) {
        if (state.kind === 'busy') {
            return `
                <div class="room-week-half room-week-half-${parity.toLowerCase()} room-week-half-busy">
                    <span class="room-half-marker">${escapeHtml(label)}</span>
                    ${state.lessons.map(renderLessonCard).join('')}
                </div>
            `;
        }
        if (state.kind === 'empty') {
            return `
                <div class="room-week-half room-week-half-${parity.toLowerCase()} room-week-half-empty">
                    <span class="room-half-marker">${escapeHtml(label)}</span>
                    <span class="slot-unavailable">Нет даты</span>
                </div>
            `;
        }
        return `
            <div class="room-week-half room-week-half-${parity.toLowerCase()} room-week-half-free">
                <span class="room-half-marker">${escapeHtml(label)}</span>
                <span class="free-slot">Нет занятий</span>
            </div>
        `;
    }

    function slotStatesEqual(first, second) {
        if (first.kind !== second.kind) return false;
        if (first.kind !== 'busy') return true;
        return lessonSetSignature(first.lessons) === lessonSetSignature(second.lessons);
    }

    function lessonSetSignature(lessons) {
        return lessons
            .map(lessonContentSignature)
            .sort((a, b) => naturalCompare(a, b))
            .join('|');
    }

    function lessonContentSignature(lesson) {
        return [
            lesson.scheduleRuleSlotId || lesson.scheduleRuleId || '',
            lesson.subjectId || lesson.subjectName || '',
            lesson.teacherId || lesson.teacherName || '',
            lesson.classroomId || lesson.classroomName || '',
            lesson.lessonTypeId || lesson.lessonTypeName || '',
            lesson.lessonFormat || '',
            lesson.scheduleOverrideId || '',
            lesson.overrideAction || '',
            lesson.originalLessonDate || '',
            valueListSignature(lesson.groupIds, lesson.groupNames),
            valueListSignature(lesson.subgroupIds, lesson.subgroupNames)
        ].join(':');
    }

    function valueListSignature(primary = [], fallback = []) {
        const primaryList = Array.isArray(primary) ? primary : [];
        const fallbackList = Array.isArray(fallback) ? fallback : [];
        const source = primaryList.length ? primaryList : fallbackList;
        return [...source]
            .map(value => String(value))
            .sort((a, b) => naturalCompare(a, b))
            .join(',');
    }

    function getLessonWeeksText(lesson) {
        if (!lesson || lesson.weekNumber == null || lesson.lessonTypeAcademicHours == null) {
            return '';
        }
        const totalHours = lesson.lessonTypeAcademicHours;
        const consumedBefore = lesson.consumedLessonTypeAcademicHoursBeforeLesson || 0;
        
        // Каждое занятие - это 2 часа
        const totalLessons = Math.ceil(totalHours / 2);
        if (totalLessons <= 0) return '';
        
        const parity = lesson.ruleParity || lesson.parity;
        let startWeek, endWeek;
        
        if (parity === 'EVEN' || parity === 'ODD') {
            // Раз в две недели
            startWeek = lesson.weekNumber - consumedBefore;
            endWeek = startWeek + (totalLessons - 1) * 2;
        } else {
            // Каждую неделю
            const steps = Math.floor(consumedBefore / 2);
            startWeek = lesson.weekNumber - steps;
            endWeek = startWeek + totalLessons - 1;
        }
        
        if (startWeek <= 0) startWeek = 1;
        if (endWeek < startWeek) endWeek = startWeek;

        return formatLessonWeeksText(startWeek, endWeek, parity, semesterWeeksForLesson(lesson));
    }

    function formatLessonWeeksText(startWeek, endWeek, parity, semesterWeeks) {
        const semesterEndWeek = lastApplicableSemesterWeek(semesterWeeks, parity);
        const semesterStartWeek = firstApplicableSemesterWeek(parity);
        if (semesterEndWeek && startWeek <= semesterStartWeek && endWeek >= semesterEndWeek) return '';
        if (semesterEndWeek && endWeek >= semesterEndWeek) return `(с ${startWeek} нед.)`;
        return startWeek === endWeek
            ? `(${startWeek} нед.)`
            : `(с ${startWeek} по ${endWeek} нед.)`;
    }

    function semesterWeeksForLesson(lesson) {
        const semester = (state.dictionaries.semesters || []).find(item => dateInRange(lesson.date, item.startDate, item.endDate));
        if (!semester?.startDate || !semester?.endDate) return DEFAULT_SEMESTER_WEEKS;
        const start = parseIsoDate(semester.startDate);
        const end = parseIsoDate(semester.endDate);
        const days = Math.max(1, Math.round((end - start) / 86400000) + 1);
        return Math.max(1, Math.ceil(days / 7));
    }

    function dateInRange(date, startDate, endDate) {
        return Boolean(date && startDate && endDate && date >= startDate && date <= endDate);
    }

    function firstApplicableSemesterWeek(parity) {
        return parity === 'EVEN' ? 2 : 1;
    }

    function lastApplicableSemesterWeek(maxWeeks, parity) {
        const totalWeeks = Number(maxWeeks || 0);
        if (!totalWeeks) return 0;
        if (parity === 'ODD' && totalWeeks % 2 === 0) return totalWeeks - 1;
        if (parity === 'EVEN' && totalWeeks % 2 !== 0) return totalWeeks - 1;
        return totalWeeks;
    }

    function renderLessonCard(lesson) {
        const subgroupText = (lesson.subgroupNames || []).join(', ');
        const meta = [
            lesson.lessonTypeName,
            lesson.lessonFormat,
            PARITY_LABELS[lesson.parity] || lesson.parity
        ].filter(Boolean).join(' · ');

        const weeksText = getLessonWeeksText(lesson);
        const weeksBadge = weeksText ? `<span class="lesson-weeks-badge">${escapeHtml(weeksText)}</span>` : '';
        const sourceDate = lesson.originalLessonDate || lesson.date;
        const overrideBadge = lesson.scheduleOverrideId
            ? '<span class="schedule-override-badge">Разовая правка</span>'
            : '';
        const editButton = canEditScheduleOverrides(role) && lesson.scheduleRuleSlotId && sourceDate
            ? `
                <button type="button"
                        class="schedule-override-edit"
                        data-schedule-override-edit
                        data-base-rule-slot-id="${escapeHtml(lesson.scheduleRuleSlotId)}"
                        data-lesson-date="${escapeHtml(sourceDate)}">
                    Изменить
                </button>
            `
            : '';

        return `
            <div class="lesson-card schedule-view-lesson-card">
                <div class="lesson-subject schedule-view-lesson-title" style="display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem;">
                    <span>${escapeHtml(lesson.subjectName || 'Без дисциплины')}</span>
                    ${weeksBadge}
                </div>
                ${overrideBadge}
                <div class="lesson-group">${escapeHtml((lesson.groupNames || []).join(', ') || 'Группа не указана')}</div>
                ${subgroupText ? `<div class="lesson-subgroup">${escapeHtml(subgroupText)}</div>` : ''}
                <div class="lesson-teacher">${escapeHtml(lesson.teacherName || 'Преподаватель не указан')}</div>
                <div class="lesson-meta">${escapeHtml(lesson.classroomName || 'Аудитория не указана')}</div>
                ${meta ? `<div class="lesson-meta">${escapeHtml(meta)}</div>` : ''}
                ${lesson.scheduleRuleSlotId ? `<div class="lesson-meta">ID слота: ${escapeHtml(lesson.scheduleRuleSlotId)}</div>` : ''}
                ${editButton}
            </div>
        `;
    }

    function setTableState(message, showReloadButton = false) {
        if (!tables) return;
        tables.innerHTML = `
            <div class="loading-row schedule-view-state${showReloadButton ? ' schedule-view-state-actionable' : ''}">
                <span>${escapeHtml(message)}</span>
                ${showReloadButton ? `
                    <button type="button"
                            class="btn btn-md btn-primary schedule-view-state-reload"
                            data-schedule-view-reload>
                        Показать расписание
                    </button>
                ` : ''}
            </div>
        `;
    }

    function resetResults(message) {
        state.sections = [];
        state.activeSectionIndex = 0;
        state.mobileDay = 1;
        state.totalLessons = 0;
        state.lessons = [];
        renderResultNavigation();
        tableTitle.textContent = 'Расписание';
        tableCaption.textContent = message;
        setTableState(message, message === SCHEDULE_REFRESH_PROMPT);
        countLabel.textContent = '0 занятий';
        dayTabs.hidden = true;
        dayTabs.innerHTML = '';
    }

    function updatePeriodLabel(range) {
        if (!periodLabel) return;
        if (!dateInput.value) {
            periodLabel.textContent = 'Период не выбран';
            return;
        }
        periodLabel.textContent = periodCaption(
            range || scheduleRangeForSemester(dateInput.value, selectedSemester()),
            selectedSemester()
        );
    }

    function selectedSemester() {
        const semesterId = semesterSelect?.value;
        if (!semesterId) return null;
        return (state.dictionaries.semesters || [])
            .find(semester => String(semester.id) === String(semesterId)) || null;
    }

    function selectCurrentSemester() {
        if (!semesterSelect) return;
        const currentSemester = findSemesterForDate(state.dictionaries.semesters, dateInput.value);
        semesterSelect.value = currentSemester ? String(currentSemester.id) : '';
        synchronizeDateWithSemester();
        updatePeriodLabel();
    }

    function synchronizeDateWithSemester() {
        const semester = selectedSemester();
        if (!semester) {
            dateInput.value = toIsoDate(new Date());
            return;
        }
        if (!dateWithinSemester(dateInput.value, semester)) {
            dateInput.value = semester.startDate;
        }
    }

    function groupNameById(id) {
        return state.dictionaries.groups.find(group => String(group.id) === String(id))?.name || `Группа ${id}`;
    }

    function preferredMobileDay(range) {
        if (!range) return 1;
        return isoDay(dateInput.value || range.startDate);
    }
}

function createEmptyOverridePanel() {
    return {
        setRegistry() {},
        openLesson() {},
        openRegistry() {}
    };
}

async function loadDictionaries(role, userDepartmentId) {
    const [groups, teachers, classrooms, departments, subjects, lessonTypes, semesters] = await Promise.all([
        api.get('/api/groups'),
        api.get('/api/users/teachers'),
        api.get('/api/classrooms'),
        api.get('/api/departments'),
        api.get('/api/subjects'),
        api.get('/api/lesson-types'),
        api.get('/api/schedule/semesters')
    ]);

    const visibleGroups = role === ROLE_DEPARTMENT && userDepartmentId
        ? groups.filter(group => String(group.departmentId) === String(userDepartmentId))
        : groups;

    fillSelect('schedule-view-group', visibleGroups, item => item.id, item => item.name, 'Все группы');
    fillSelect('schedule-view-teacher', teachers, item => item.id, item => item.fullName || item.username, 'Все преподаватели');
    fillSelect('schedule-view-classroom', classrooms, item => item.id, item => roomLabel(item), 'Все аудитории');
    fillSelect('schedule-view-department', departments, item => item.id, item => item.departmentName, 'Все кафедры');
    fillSelect('schedule-view-subject', subjects, item => item.id, item => item.name, 'Все дисциплины');
    fillSelect('schedule-view-lesson-type', lessonTypes, item => item.id, item => item.name, 'Все типы');
    fillSelect('schedule-view-semester', semesters, item => item.id, semesterOptionLabel, 'По текущей дате');

    return {
        groups,
        teachers,
        classrooms,
        departments,
        semesters: semesters || []
    };
}

function setDefaultDate(dateInput) {
    const today = new Date();
    dateInput.value = toIsoDate(today);
}

function fillSelect(id, items, valueFn, labelFn, emptyLabel) {
    const select = document.getElementById(id);
    if (!select) return;
    select.innerHTML = `<option value="">${escapeHtml(emptyLabel)}</option>` +
        (items || []).map(item => `<option value="${escapeHtml(valueFn(item))}">${escapeHtml(labelFn(item))}</option>`).join('');
}

function addOptional(params, key, value) {
    if (value) params.set(key, value);
}

function valueOf(id) {
    return document.getElementById(id)?.value || '';
}

function normalizeId(value) {
    if (!value || value === 'null' || value === 'undefined') return '';
    return String(value);
}

function roomLabel(room) {
    return [room.name, room.building, room.floor ? `${room.floor} этаж` : null].filter(Boolean).join(' · ');
}

function collectSlots(lessons) {
    const slots = new Map();
    lessons.forEach(lesson => {
        const key = lessonSlotKey(lesson);
        const existing = slots.get(key);
        const next = {
            key,
            id: lesson.timeSlotId,
            orderNumber: lesson.timeSlotOrder,
            startTime: lesson.startTime,
            endTime: lesson.endTime,
            orderLabel: lesson.timeSlotOrder ? `${lesson.timeSlotOrder} пара` : 'Пара',
            timeLabel: lesson.startTime || lesson.endTime
                ? `${trimTime(lesson.startTime)}-${trimTime(lesson.endTime)}`
                : 'Время не указано'
        };
        if (!existing || existing.timeLabel === 'Время не указано') {
            slots.set(key, next);
        }
    });
    return Array.from(slots.values()).sort(compareSlots);
}

function groupLessonsByDateSlot(lessons) {
    const grouped = new Map();
    lessons.forEach(lesson => {
        if (!lesson.date) return;
        const key = dateSlotKey(lesson.date, lessonSlotKey(lesson));
        if (!grouped.has(key)) grouped.set(key, []);
        grouped.get(key).push(lesson);
    });
    grouped.forEach(items => {
        items.sort((a, b) => naturalCompare(a.subjectName, b.subjectName)
            || naturalCompare(a.teacherName, b.teacherName)
            || naturalCompare((a.groupNames || []).join(','), (b.groupNames || []).join(',')));
    });
    return grouped;
}

function buildScheduleSections(lessons, splitType, context) {
    const sections = new Map();

    lessons.forEach(lesson => {
        splitEntitiesForLesson(lesson, splitType, context).forEach(entity => {
            if (!sections.has(entity.key)) {
                sections.set(entity.key, {
                    key: entity.key,
                    title: entity.title,
                    lessons: []
                });
            }
            sections.get(entity.key).lessons.push(lesson);
        });
    });

    return Array.from(sections.values())
        .map(section => ({
            ...section,
            lessons: sortLessons(section.lessons)
        }))
        .sort((a, b) => naturalCompare(a.title, b.title));
}

function splitEntitiesForLesson(lesson, splitType, context) {
    if (splitType === TARGET_TEACHERS) {
        return [singleEntity('teacher', lesson.teacherId, lesson.teacherName, 'Преподаватель не указан')];
    }
    if (splitType === TARGET_CLASSROOMS) {
        return [singleEntity('classroom', lesson.classroomId, lesson.classroomName, 'Аудитория не указана')];
    }
    if (context.selectedGroupId) {
        return [singleEntity('group', context.selectedGroupId, context.selectedGroupName, 'Группа не указана')];
    }
    return listEntities('group', lesson.groupIds, lesson.groupNames, 'Группа не указана', context.allowedGroupIds, context.groupNamesById);
}

function singleEntity(prefix, id, name, fallback) {
    const title = name || fallback;
    return {
        id,
        key: `${prefix}:${id ?? title}`,
        title
    };
}

function listEntities(prefix, ids = [], names = [], fallback, allowedIds = new Set(), namesById = new Map()) {
    const safeIds = Array.isArray(ids) ? ids : [];
    const safeNames = Array.isArray(names) ? names : [];
    if (!safeIds.length && !safeNames.length) {
        return allowedIds.size ? [] : [singleEntity(prefix, null, fallback, fallback)];
    }
    const maxLength = Math.max(safeIds.length, safeNames.length);
    const entities = [];
    for (let index = 0; index < maxLength; index += 1) {
        const id = safeIds[index] ?? null;
        if (allowedIds.size && (id == null || !allowedIds.has(String(id)))) {
            continue;
        }
        const name = namesById.get(String(id)) || safeNames[index] || (id == null ? fallback : `Группа ${id}`);
        entities.push(singleEntity(prefix, id, name, fallback));
    }
    return entities;
}

function sortLessons(lessons) {
    return [...lessons].sort((a, b) =>
        naturalCompare(a.date, b.date)
        || compareSlots(lessonSlotDescriptor(a), lessonSlotDescriptor(b))
        || naturalCompare(a.subjectName, b.subjectName)
        || naturalCompare(a.teacherName, b.teacherName)
        || naturalCompare(a.classroomName, b.classroomName)
    );
}

function lessonSlotDescriptor(lesson) {
    return {
        key: lessonSlotKey(lesson),
        orderNumber: lesson.timeSlotOrder,
        startTime: lesson.startTime
    };
}

function buildCombinedWeekBlocks(startDate, endDate, lessons) {
    const dates = datesBetween(startDate, endDate);
    const weekGroups = new Map();
    dates.forEach(date => {
        const key = weekStart(date);
        if (!weekGroups.has(key)) weekGroups.set(key, []);
        weekGroups.get(key).push(date);
    });

    const weekStarts = Array.from(weekGroups.keys()).sort((a, b) => naturalCompare(a, b));
    const blocks = [];
    for (let index = 0; index < weekStarts.length; index += 2) {
        const firstWeek = weekGroups.get(weekStarts[index]) || [];
        const secondWeek = weekGroups.get(weekStarts[index + 1]) || [];
        const blockDates = [...firstWeek, ...secondWeek];
        const firstParity = weekParity(firstWeek, lessons) || 'ODD';
        const secondParity = weekParity(secondWeek, lessons) || (firstParity === 'ODD' ? 'EVEN' : 'ODD');
        const datesWithFallback = [
            ...firstWeek.map(date => ({ date, fallbackParity: firstParity })),
            ...secondWeek.map(date => ({ date, fallbackParity: secondParity }))
        ];
        blocks.push({
            startDate: blockDates[0],
            endDate: blockDates[blockDates.length - 1],
            datesByDay: buildDatesByDay(datesWithFallback, lessons),
            label: `Период ${formatDate(blockDates[0])} - ${formatDate(blockDates[blockDates.length - 1])}`
        });
    }
    return blocks;
}

function weekParity(dates, lessons) {
    for (const date of dates) {
        const parity = parityForDate(date, lessons);
        if (parity) return parity;
    }
    return null;
}

function buildDatesByDay(datesWithFallback, lessons) {
    const datesByDay = {};
    datesWithFallback.forEach(({ date, fallbackParity }) => {
        const day = isoDay(date);
        const parity = parityForDate(date, lessons) || fallbackParity;
        if (!datesByDay[day]) datesByDay[day] = {};
        datesByDay[day][parity] = date;
    });
    return datesByDay;
}

function parityForDate(date, lessons) {
    const lesson = lessons.find(item => item.date === date && ['ODD', 'EVEN'].includes(item.parity));
    if (lesson?.parity) return lesson.parity;
    const withWeek = lessons.find(item => item.date === date && item.weekNumber != null);
    if (withWeek?.weekNumber != null) return Number(withWeek.weekNumber) % 2 === 0 ? 'EVEN' : 'ODD';
    return null;
}

function combinedWeekCaption(block) {
    return 'Нечётная и чётная недели';
}

function mobileDayCaption(datesByParity) {
    return 'Нечётная / Чётная';
}

function periodCaption(range, semester = null) {
    if (!range) return 'Учебное расписание';
    const rangeLabel = `${formatDate(range.startDate)} — ${formatDate(range.endDate)}`;
    return semester ? `${semesterOptionLabel(semester)} · ${rangeLabel}` : rangeLabel;
}

export function semesterOptionLabel(semester) {
    const type = String(semester?.semesterType || '').toLowerCase();
    const typeLabel = type === 'autumn'
        ? 'Осенний семестр'
        : (type === 'spring' ? 'Весенний семестр' : 'Семестр');
    const academicYear = semester?.academicYearTitle || 'Учебный год';
    const dates = semester?.startDate && semester?.endDate
        ? ` · ${formatDate(semester.startDate)} — ${formatDate(semester.endDate)}`
        : '';
    return `${academicYear} · ${typeLabel}${dates}`;
}

export function findSemesterForDate(semesters, date) {
    return (semesters || []).find(semester => dateWithinSemester(date, semester)) || null;
}

export function scheduleRangeForSemester(selectedDate, semester) {
    const anchorDate = semester && !dateWithinSemester(selectedDate, semester)
        ? semester.startDate
        : selectedDate;
    return twoWeekRange(anchorDate);
}

function dateWithinSemester(date, semester) {
    return Boolean(
        date
        && semester?.startDate
        && semester?.endDate
        && date >= semester.startDate
        && date <= semester.endDate
    );
}

function lessonSlotKey(lesson) {
    const value = lesson.timeSlotOrder ?? lesson.timeSlotId ?? `${lesson.startTime || ''}-${lesson.endTime || ''}`;
    return value ? String(value) : 'unknown';
}

function dateSlotKey(date, slotKey) {
    return `${date}:${slotKey}`;
}

function weekStart(value) {
    const date = parseIsoDate(value);
    const day = date.getDay() || 7;
    date.setDate(date.getDate() + 1 - day);
    return toIsoDate(date);
}

function twoWeekRange(value) {
    const selected = parseIsoDate(value);
    const start = addDays(selected, 1 - isoDay(value));
    const end = addDays(start, 13);
    return {
        startDate: toIsoDate(start),
        endDate: toIsoDate(end)
    };
}

function addDays(date, days) {
    const next = new Date(date);
    next.setDate(next.getDate() + days);
    return next;
}

function parseIsoDate(value) {
    const [year, month, day] = String(value).split('-').map(Number);
    return new Date(year, month - 1, day);
}

function datesBetween(startDate, endDate) {
    const dates = [];
    for (let date = parseIsoDate(startDate); date <= parseIsoDate(endDate); date.setDate(date.getDate() + 1)) {
        dates.push(toIsoDate(date));
    }
    return dates;
}

function isoDay(value) {
    const day = parseIsoDate(value).getDay();
    return day === 0 ? 7 : day;
}

function dayShort(dayOfWeek) {
    return DAY_SHORT_LABELS[Number(dayOfWeek)] || '-';
}

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

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

function trimTime(value) {
    return String(value || '').slice(0, 5);
}

function toIsoDate(date) {
    return [
        date.getFullYear(),
        String(date.getMonth() + 1).padStart(2, '0'),
        String(date.getDate()).padStart(2, '0')
    ].join('-');
}

function lessonCountLabel(count) {
    const tail = count % 100;
    if (tail >= 11 && tail <= 14) return `${count} занятий`;
    if (count % 10 === 1) return `${count} занятие`;
    if ([2, 3, 4].includes(count % 10)) return `${count} занятия`;
    return `${count} занятий`;
}

function sectionCountLabel(count) {
    const tail = count % 100;
    if (tail >= 11 && tail <= 14) return `${count} расписаний`;
    if (count % 10 === 1) return `${count} расписание`;
    if ([2, 3, 4].includes(count % 10)) return `${count} расписания`;
    return `${count} расписаний`;
}

function compareSlots(a, b) {
    const first = Number(a.orderNumber || a.key);
    const second = Number(b.orderNumber || b.key);
    if (!Number.isNaN(first) && !Number.isNaN(second) && first !== second) {
        return first - second;
    }
    return naturalCompare(a.startTime, b.startTime)
        || naturalCompare(a.key, b.key);
}

function naturalCompare(a, b) {
    return String(a || '').localeCompare(String(b || ''), 'ru', { numeric: true, sensitivity: 'base' });
}
