#!/usr/bin/python3

import datetime
import json
import os
import shutil
import subprocess
import sys
import xml.etree.ElementTree as ET
import zipfile
from random import shuffle

from PyQt6.QtCore import Qt, QLockFile
from PyQt6.QtGui import QCursor, QIcon, QCloseEvent, QAction
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QMainWindow, QHBoxLayout, QVBoxLayout, \
    QGroupBox, QTableWidget, QGridLayout, QScrollArea, QPushButton, QFileDialog, QTableWidgetItem, QAbstractItemView, \
    QMenu, QInputDialog, QComboBox, QStackedLayout, QHeaderView

from school_interview_system_prepare_station_modules.config import icon_file, version, root_config_folder, \
    marks_headers, possible_statuses, experts_file, log_file
from school_interview_system_prepare_station_modules.design import GroupChangeStatus, FilterDialog
from school_interview_system_prepare_station_modules.export_functions import export_current_exam_to_pdf, \
    print_auditories_dict_to_pdf
from school_interview_system_prepare_station_modules.mos_oral_rus_prepare_station_classes import NewExpert
from school_interview_system_prepare_station_modules.system_functions import alert, question, tree_indent, \
    enter_password, get_config_folder_for_current_exam, get_audio_records_folder, get_config_file, \
    get_experts_distribution_info_file, get_loaded_works_info_file, get_md5_folder, get_pdf_folder, \
    check_if_program_is_not_hacked, reset_log_to_last_n_days
import logging


class MyWindow(QMainWindow):

    def load_exam_info(self):
        try:
            self.tree = ET.parse(self.current_exam_file)
            root = self.tree.getroot()
            exam_params = ('Регион', 'Код_ППЭ', 'Дата', 'Код_предмета', 'Название_предмета')
            exam_params_values = [''] * len(exam_params)
            for i in root.iter('Block'):
                for col in range(len(exam_params)):
                    if i.text and exam_params[col] in i.attrib['BlockName']:
                        exam_params_values[col] = i.text
            self.region_label.setText(exam_params_values[0])
            self.school_number_label.setText(exam_params_values[1])
            self.examen_date_label.setText(exam_params_values[2])
            self.discipline_label.setText(f'{exam_params_values[3]} - {exam_params_values[4]}')

            self.number_of_participants_label.setText(f'{self.students_table.rowCount()}')
            self.number_of_recordings_label.setText('0')

        except Exception as e:
            logging.error(f'Выбранный файл {self.current_exam_file} имеет неверный формат. Исключение: {e}')
            # Предполагается, что программа работает только с одним экзаменом
            alert(message='Выбранный файл имеет неверный формат!')
            shutil.rmtree(self.config_folder)
            sys.exit(0)

    def load_exam(self):
        try:
            self.students_table.setRowCount(0)
            tree = ET.parse(self.current_exam_file)
            self.root = tree.getroot()
            # РезервI - это ОВЗ
            students_params = ('Фамилия', 'Имя', 'Отчество', 'РезервI', 'Аудитория', 'Класс')
            row = -1
            for i in self.root.iter('Block'):
                # если указана фамилия - создать новую строку
                if 'Фамилия' in i.attrib['BlockName'] and i.text:
                    row += 1
                    self.students_table.setRowCount(row + 1)
                    status_combobox = QComboBox()
                    status_combobox.addItems(possible_statuses)
                    status_combobox.currentIndexChanged.connect(self.update_student_status)
                    self.students_table.setItem(row, 6, QTableWidgetItem())
                    self.students_table.setCellWidget(row, 6, status_combobox)
                for col in range(len(students_params)):
                    if i.text and students_params[col] in i.attrib['BlockName']:
                        # ОВЗ в xml: 22 - да, значение отсутствует - нет
                        if col != 3:
                            self.students_table.setItem(row, col, QTableWidgetItem(i.text))
                        else:
                            self.students_table.setItem(row, col, QTableWidgetItem('Да'))
                if 'Не_закончил' in i.attrib['BlockName'] and i.text in possible_statuses:
                    if i.text:
                        status_combobox.setCurrentText(i.text)
                        self.students_table.item(row, 6).setData(0, i.text)
                    else:
                        status_combobox.setCurrentText('')
                        self.students_table.item(row, 6).setData(0, '')
                # установка кода участника в последний (невидимый) столбец
                if i.text and 'Код_участника' in i.attrib['BlockName']:
                    self.students_table.setItem(row, self.students_table.columnCount() - 1, QTableWidgetItem(i.text))
                    self.students_codes_list.append(i.text)

            self.load_exam_info()
            self.students_codes_list_without_sorting = self.students_codes_list.copy()

            # отображение загруженных работ
            existing_recordings_list = [i for i in os.listdir(self.recordings_folder) if i.endswith('.ogg')]
            for row in range(self.students_table.rowCount()):
                try:
                    code = self.students_table.item(row, 13).text()
                except Exception:
                    continue
                if f'{code}.ogg' in existing_recordings_list:
                    self.students_table.setItem(row, 7, QTableWidgetItem('Да'))
                    if code not in self.filenames_of_recordings:
                        self.filenames_of_recordings.append(code)
        except Exception as e:
            logging.error(f'Выбранный файл {self.current_exam_file} имеет неверный формат. Исключение: {e}')
            alert(message='Выбранный файл имеет неверный формат!')
            shutil.rmtree(self.config_folder)
            sys.exit(0)
        # Если экзамен загрузился, можно делать все действия, кроме начала экспертизы
        else:
            self.switch_enable_all_buttons(True)
            self.number_of_recordings_label.setText(f'{len(self.filenames_of_recordings)}')
        for col in range(self.students_table.columnCount() - 1):
            self.students_table.horizontalHeader().setSectionResizeMode(col, QHeaderView.ResizeMode.ResizeToContents)

    def update_loaded_works_info(self):
        if os.path.isfile(self.loaded_works_info_file):
            loaded_works_info = dict()
            try:
                loaded_works_info = json.loads(open(self.loaded_works_info_file).read())
            except Exception:
                logging.info('Не удалось получить информацию о загруженных работах')
            if loaded_works_info:
                for row in range(self.students_table.rowCount()):
                    try:
                        code = self.students_table.item(row, 13).text()
                    except Exception:
                        continue
                    if code in loaded_works_info:
                        info = loaded_works_info[code]
                        if 'Работа проверена' in info:
                            try:
                                self.students_table.item(row, 8).setText(info['Работа проверена'])
                            except Exception:
                                self.students_table.setItem(row, 8, QTableWidgetItem(info['Работа проверена']))
                        if 'Вариант' in info:
                            try:
                                self.students_table.item(row, 9).setText(info['Вариант'])
                            except Exception:
                                self.students_table.setItem(row, 9, QTableWidgetItem(info['Вариант']))
                        if 'Дата загрузки' in info:
                            try:
                                self.students_table.item(row, 10).setText(info['Дата загрузки'])
                            except Exception:
                                self.students_table.setItem(row, 10, QTableWidgetItem(info['Дата загрузки']))
                        if 'Эксперт' in info:
                            try:
                                self.students_table.item(row, 11).setText(info['Эксперт'])
                            except Exception:
                                self.students_table.setItem(row, 11, QTableWidgetItem(info['Эксперт']))
                        if 'Балл' in info:
                            try:
                                self.students_table.item(row, 12).setText(info['Балл'])
                            except Exception:
                                self.students_table.setItem(row, 12, QTableWidgetItem(info['Балл']))
                        if 'Статус' in info:
                            self.students_table.cellWidget(row, 6).setCurrentText(info['Статус'])
                            # при досрочном завершении не нужно показывать итоговый балл
                            if info['Статус'] == 'Досрочное завершение':
                                self.students_table.item(row, 12).setText('')
                        if 'Оценки' in info:
                            self.separate_marks_dict[code] = info['Оценки']

    def load_dat_file(self):
        chosen_file, _ = QFileDialog.getOpenFileName(
            parent=None, caption='Выберите DAT-файл с информацией об экзамене',
            directory=os.getenv('HOME') + '/Загрузки',
            filter='DAT Files (*.dat)'
        )
        if chosen_file:
            if not self.check_dat_file(chosen_file):
                alert(message=f'Файл {chosen_file} повреждён или не является корректным файлом с вариантами.')
                return
            if not self.check_exam_date_in_dat_file(chosen_file):
                alert(message=f'Дата экзамена в файле {chosen_file} не совпадает с датой на станции. '
                              f'Варианты не загружены.')
                return
            shutil.copyfile(chosen_file, os.path.join(self.config_folder, os.path.basename(chosen_file)))
            self.current_dat_file = os.path.join(self.config_folder, os.path.basename(chosen_file))
            subprocess.run(['py-ini-config', 'set', self.config_file, '-n', 'current_dat_file', self.current_dat_file])
            alert(message=f'Импорт вариантов выполнен.')
            if not os.path.isfile(self.experts_distribution_info_file):
                self.start_expertise_button.setEnabled(True)
                self.start_expertise_button.setToolTip('')

    def check_dat_file(self, filename):
        try:
            decoded_variants_xml = subprocess.run(['base64', '-d', filename], capture_output=True).stdout.decode()
            dat_file_tree = ET.fromstring(decoded_variants_xml)
            for i in dat_file_tree.iter('Variant'):
                if i.text:
                    break
            # Если не встретился хотя бы один номер варианта, файл некорректный
            else:
                return False
        # при любом исключении файл также некорректный
        except Exception as e:
            logging.warning(f'При проверка файла с вариантами {filename} возникло исключение: {e}')
            return False
        return True

    def check_exam_date_in_dat_file(self, filename):
        """
        Функция проверяет соответствие даты в загруженном на станцию экзамене и в dat-файле с вариантами.
        Проверка происходит исходя из следующих представлений даты в файлах примеров
        Файл XML с экзаменом: 14-02-24
        Файл с вариантами: 2024-02-14T00:00:00
        :param filename: имя файла с вариантами
        """
        exam_date_in_dat_file = ''
        try:
            decoded_variants_xml = subprocess.run(['base64', '-d', filename], capture_output=True).stdout.decode()
            dat_file_tree = ET.fromstring(decoded_variants_xml)
            for i in dat_file_tree.iter('ExamDate'):
                if i.text:
                    exam_date_in_dat_file = datetime.datetime.strptime(i.text, '%Y-%m-%dT%H:%M:%S').date()
                    break
            exam_date_in_xml = datetime.datetime.strptime(self.examen_date_label.text(), '%d-%m-%y').date()
            return exam_date_in_xml == exam_date_in_dat_file
        except Exception as e:
            logging.warning(f'При проверке соответствия дат в XML экзамена и файле с вариантами {filename} '
                            f'возникло исключение: {e}')
            return False

    def switch_enable_all_buttons(self, enable=False):
        for button in (
                self.export_xml_button,
                self.import_recordings_button,
                self.switch_tables_button,
                self.load_marks_button,
                self.export_all_exam_button
        ):
            button.setEnabled(enable)

    def update_student_status(self):
        # изменение дерева для xml только после загрузки таблицы и при ручном изменении статуса
        if self.tree:
            students_index = [self.students_table.cellWidget(i, 6) for i in
                              range(self.students_table.rowCount())].index(self.sender())
            students_index_in_xml_tree = self.students_codes_list_without_sorting.index(
                self.students_codes_list[students_index])
            self.add_value_to_xml_tree(
                students_index=students_index_in_xml_tree, tag_name='Не_закончил',
                added_value=self.students_table.cellWidget(students_index, 6).currentText()
            )
            self.students_table.item(students_index, 6).setData(0, self.students_table.cellWidget(students_index,
                                                                                                  6).currentText())

    def context_menu_called(self):
        self.popMenu = QMenu(self)
        self.action_enter_auditory = QAction("Ввести аудиторию", self.popMenu)
        self.action_enter_auditory.triggered.connect(self.enter_auditory)
        self.popMenu.addAction(self.action_enter_auditory)
        self.action_change_status = QAction("Изменить статус", self.popMenu)
        self.action_change_status.triggered.connect(self.group_change_status)
        self.popMenu.addAction(self.action_change_status)
        self.popMenu.exec(QCursor.pos())

    def add_value_to_xml_tree(self, students_index, tag_name, added_value=''):
        """
        Добавление значения параметра в дерево для последующего экспорта xml -
        перебор учеников и добавление параметра к указанному номеру.
        К сожалению, организация исходного (и требуемого) файла xml не даёт реализовать по-другому.
        :param students_index: индекс ученика в таблице и, соответственно, в файле (на 1 меньше порядкового номера)
        :param tag_name: слово, которое должно содержаться в атрибуте BlockName.
        Например, Аудитория (в файле - Аудитория01, Аудитория02 и т.д.)
        :param added_value: значение, которое нужно установить
        """
        row = -1
        for i in self.tree.getroot().iter('Block'):
            # если указана фамилия - перейти на новую строку
            if 'Фамилия' in i.attrib['BlockName'] and i.text:
                row += 1
            if row == students_index and tag_name in i.attrib['BlockName']:
                i.text = added_value
        self.tree.write(self.current_exam_file, encoding='utf-8')

    def get_value_from_source_xml(self, students_index, tag_name):
        """
        Получение значения из дерева для последующего экспорта итогового xml -
        перебор учеников и возвращение текста параметра ученика по указанному индексу.
        :param students_index: индекс ученика в таблице и, соответственно, в файле (на 1 меньше порядкового номера)
        :param tag_name: слово, которое должно содержаться в атрибуте BlockName.
        Например, Аудитория (в файле - Аудитория01, Аудитория02 и т.д.)
        """
        row = -1
        for i in self.tree.getroot().iter('Block'):
            # если указана фамилия - перейти на новую строку
            if 'Фамилия' in i.attrib['BlockName'] and i.text:
                row += 1
            if row == students_index and tag_name in i.attrib['BlockName'] and i.text:
                return i.text
        return ''

    def fill_marks_in_xml_tree(self, students_index, marks_array):
        """
        Заполняет экспертные оценки в xml-файле.
        Для начала придётся искать, как эти параметры называются для конкретного ученика :(
        :param students_index: индекс студента в файле
        :param marks_array: список оценок, в файле для них 19 параметров, реально их 14
        :return: None
        """
        row = -1
        for i in self.tree.getroot().iter('Block'):
            # если указана фамилия - перейти на новую строку
            if 'Фамилия' in i.attrib['BlockName'] and i.text:
                row += 1
            if row == students_index and 'С_' in i.attrib['BlockName']:
                # C_0601, C_0602, ... C_0619. Реально оценок меньше
                c_attrib = i.attrib['BlockName'][:-2]
                for j in range(len(marks_array)):
                    attrib_name = f'{c_attrib}{j + 1:02}'
                    self.add_value_to_xml_tree(students_index, attrib_name, marks_array[j])
                return

    def get_marks_from_xml_by_index(self, students_index):
        """
        Возвращает список отдельных оценок ученика по индексу из xml-файла.
        Если оценок нет, возвращает пустой список.
        """
        res = []
        row = -1
        for i in self.tree.getroot().iter('Block'):
            # если указана фамилия - перейти на новую строку
            if 'Фамилия' in i.attrib['BlockName'] and i.text:
                row += 1
            if row == students_index and 'С_' in i.attrib['BlockName'] and i.text:
                res.append(i.text)
        return res

    def enter_auditory(self):
        auditory, ok = QInputDialog.getInt(self, 'Ввод аудитории', 'Введите номер аудитории (1-9999):',
                                           min=1, max=9999)
        if ok and 1 <= auditory <= 9999:
            selected_indexes = self.students_table.selectedIndexes()
            for index in selected_indexes:
                if index.column() == 4:
                    self.students_table.setItem(index.row(), 4, QTableWidgetItem(f'{auditory:04}'))
                    students_index_in_xml_tree = self.students_codes_list_without_sorting.index(
                        self.students_codes_list[index.row()])
                    self.add_value_to_xml_tree(students_index_in_xml_tree, 'Аудитория', f'{auditory:04}')
            try:
                self.tree.write(self.current_exam_file, encoding='utf-8')
            except Exception as e:
                logging.error(f'Не удалось сохранить файл с введёнными аудиториями. Ошибка: {e}')

    def group_change_status(self):
        status_window = GroupChangeStatus()
        if status_window.exec():
            status = status_window.statuses_list.currentItem().text()
        else:
            return
        for index in self.students_table.selectedIndexes():
            row = index.row()
            try:
                self.students_table.cellWidget(row, 6).setCurrentText(status)
            except Exception:
                continue

    def export_xml_for_recording(self):
        if not all(self.students_table.item(row, 4) for row in range(self.students_table.rowCount())):
            alert(message='Не заполнены все номера аудиторий!')
            return
        filename, ok = QFileDialog.getSaveFileName(
            parent=None,
            caption='Выберите имя файла для сохранения',
            directory=f"{os.getenv('HOME')}/{self.examen_date_label.text()}-to-rus-record-station.xml",
            filter="XML files (*.xml)"
        )
        if ok:
            self.tree.write(filename, encoding='utf-8')
            alert(message=f'Xml для станций записи экспортирован в {filename}.')

    def print_auditories_lists_function(self):
        filename, ok = QFileDialog.getSaveFileName(
            parent=None,
            caption='Выберите имя файла для печати поаудиторных списков',
            directory=os.getenv('HOME'),
            filter="PDF files (*.pdf)"
        )
        if not ok:
            return
        auditories_dict = dict()
        for row in range(self.students_table.rowCount()):
            surname = self.students_table.item(row, 0).text() if self.students_table.item(row, 0) else ''
            name = self.students_table.item(row, 1).text() if self.students_table.item(row, 1) else ''
            patronymic = self.students_table.item(row, 2).text() if self.students_table.item(row, 2) else ''
            auditory = self.students_table.item(row, 4).text() if self.students_table.item(row, 4) else None
            fio = f'{surname} {name} {patronymic}'.strip()
            if auditory and auditory in auditories_dict:
                auditories_dict[auditory].append(fio)
            else:
                auditories_dict[auditory] = [fio]
        if print_auditories_dict_to_pdf(auditories_dict=auditories_dict, filename=filename):
            alert(message=f'Списки сохранены в {filename}.')
        else:
            alert(message='При сохранении поаудиторных списков возникла ошибка.')

    def check_archive_with_recordings(self, files_list):
        """
        Проверяет архив с записями на корректность
        :param files_list: список файлов в архиве
        :return: True, если есть хотя бы по одному файлу ogg, pdf, txt и xml, иначе False
        """
        return any(i.endswith('.ogg') for i in files_list) and any(i.endswith('.xml') for i in files_list) and \
            any(i.endswith('.pdf') for i in files_list) and any(i.endswith('.txt') for i in files_list)

    def import_recordings_function(self):
        filenames, _ = QFileDialog.getOpenFileNames(
            parent=None, caption='Выберите архивы со станций записи', directory=os.getcwd(),
            filter="ZIP archives (*.zip)"
        )
        if not filenames:
            return
        for filename in filenames:
            temp_dir = subprocess.run(['mktemp', '-d'], capture_output=True).stdout.decode().strip()
            try:
                with zipfile.ZipFile(filename, 'r') as archive:
                    archive.extractall(temp_dir)
            except Exception:
                alert(message=f'Архив {filename} повреждён.')
                continue
            if not self.check_archive_with_recordings(os.listdir(temp_dir)):
                logging.error(f'Архив {filename} повреждён.')
                continue
            else:
                for file in os.listdir(temp_dir):
                    full_filename = os.path.join(temp_dir, file)
                    if file.endswith('.ogg'):
                        shutil.copyfile(full_filename, os.path.join(self.recordings_folder, file))
                        file_without_extension = file.replace('.ogg', '')
                        if file_without_extension in self.students_codes_list:
                            if file_without_extension in self.filenames_of_recordings:
                                logging.warning(f'Запись {file_without_extension} уже была загружена!')
                                continue
                            self.filenames_of_recordings.append(file_without_extension)
                            students_index = self.students_codes_list.index(file.replace('.ogg', ''))
                            self.students_table.setItem(students_index, 7, QTableWidgetItem('Да'))
                    elif file.endswith('.txt'):
                        shutil.copyfile(full_filename, os.path.join(self.md5_folder, file))
                    elif file.endswith('.pdf'):
                        shutil.copyfile(full_filename, os.path.join(self.pdf_folder, file))
                    elif file.endswith('.xml'):
                        try:
                            imported_tree = ET.parse(full_filename)
                            imported_root = imported_tree.getroot()
                            statuses_dict = dict()
                            auditories_dict = dict()
                            not_finished_text = ''
                            auditory_number = ''
                            for i in imported_root.iter('Block'):
                                # Код участника в xml идёт позже статуса,
                                # поэтому нужно хранить наличие статуса и записывать по коду, если он есть
                                # Если ученик не закончил экзамен, в XML будет соответствующее значение.
                                # Если закончил, значение будет пустое.
                                if 'Не_закончил' in i.attrib['BlockName']:
                                    not_finished_text = i.text if i.text else ''
                                # Также со станции записи нужно импортировать реальный номер аудитории, в которой
                                # производилась запись, на случай, если ученик был распределён в одну аудиторию,
                                # а сдавал в другой (на станции записи есть возможность снять фильтр по аудиториям).
                                elif 'Аудитория' in i.attrib['BlockName']:
                                    auditory_number = i.text if i.text else ''
                                elif 'Код_участника' in i.attrib['BlockName']:
                                    if not_finished_text:
                                        statuses_dict[i.text] = not_finished_text
                                        not_finished_text = ''
                                    if auditory_number:
                                        auditories_dict[i.text] = auditory_number
                                        auditory_number = ''
                            for code in statuses_dict:
                                students_index = self.students_codes_list.index(code)
                                if statuses_dict[code] in possible_statuses:
                                    self.add_value_to_xml_tree(students_index, 'Не_закончил', statuses_dict[code])
                                    self.students_table.cellWidget(students_index, 6).setCurrentIndex(
                                        possible_statuses.index(statuses_dict[code]))
                            for code in auditories_dict:
                                students_index = self.students_codes_list.index(code)
                                # Если со станции записи пришла информация об аудитории, где сдавал ученик, добавить её
                                if code in auditories_dict and auditories_dict[code]:
                                    self.add_value_to_xml_tree(students_index, 'Аудитория', auditories_dict[code])
                                    if self.students_table.item(students_index, 4):
                                        self.students_table.item(students_index, 4).setText(auditories_dict[code])
                                    else:
                                        self.students_table.setItem(students_index, 4,
                                                                    QTableWidgetItem(auditories_dict[code]))

                        except Exception as e:
                            logging.error(f'Во время импорта {filename} возникла ошибка: {e}')
        self.number_of_recordings_label.setText(f'{len(self.filenames_of_recordings)}')

    def add_new_expert(self):
        self.new_expert_window = NewExpert()
        self.new_expert_window.ok_button.clicked.connect(self.save_expert_info)
        self.new_expert_window.show()

    def edit_expert_function(self):
        if len(self.experts_table.selectedIndexes()) != self.experts_table.columnCount():
            alert(message='Выберите ровно одного эксперта из таблицы')
            return
        item = self.experts_table.selectedItems()[0]
        f, i, o, *unused = item.text().split() + ['', '', '']
        self.new_expert_window = NewExpert()
        self.new_expert_window.surname_input_field.setText(f)
        self.new_expert_window.name_input_field.setText(i)
        self.new_expert_window.patronymic_input_field.setText(o)
        self.new_expert_window.ok_button.clicked.connect(lambda: self.save_expert_info(replace=True))
        self.new_expert_window.show()

    def remove_expert_function(self):
        if len(self.experts_table.selectedIndexes()) != self.experts_table.columnCount():
            alert(message='Выберите ровно одного эксперта из таблицы')
            return
        if not question(
                message=f'Вы действительно хотите удалить эксперта {self.experts_table.item(self.experts_table.currentRow(), 0).text()}?'):
            return
        self.experts_table.removeRow(self.experts_table.currentRow())
        self.experts_list = [self.experts_table.item(row, 0).text() for row in range(self.experts_table.rowCount())]
        with open(experts_file, 'w') as out:
            print(*self.experts_list, sep='\n', file=out)

    def save_expert_info(self, replace=False):
        surname = self.new_expert_window.surname_input_field.text().strip()
        name = self.new_expert_window.name_input_field.text().strip()
        patronymic = self.new_expert_window.patronymic_input_field.text().strip()
        fio = f'{surname} {name} {patronymic}'.strip()
        if not fio.strip():
            alert(message='Введены пустые значения.')
            return
        if not replace:
            self.experts_table.setRowCount(self.experts_table.rowCount() + 1)
            row = self.experts_table.rowCount() - 1
        else:
            row = self.experts_table.selectedIndexes()[0].row()
        number = 2
        while fio in self.experts_list:
            fio = f'{surname} {name} {patronymic}'.strip() + f'_{number}'
            number += 1
        self.experts_table.setItem(row, 0, QTableWidgetItem(fio))
        self.experts_list = [self.experts_table.item(row, 0).text() for row in range(self.experts_table.rowCount())]
        with open(experts_file, 'w') as out:
            print(*self.experts_list, sep='\n', file=out)
        for col in range(self.experts_table.columnCount()):
            self.experts_table.horizontalHeader().setSectionResizeMode(col, QHeaderView.ResizeMode.ResizeToContents)

    def switch_stacked_layout(self):
        self.stacked_tables_layout.setCurrentIndex((self.stacked_tables_layout.currentIndex() + 1) % 2)
        if self.stacked_tables_layout.currentIndex() == 0:
            self.switch_tables_button.setText('Перейти к экспертам')
        else:
            self.switch_tables_button.setText('Перейти к ученикам')

    def start_expertise_function(self):
        if self.experts_table.rowCount() == 0:
            alert(message='Не добавлено ни одного эксперта')
            return
        if len(self.experts_table.selectedIndexes()) == 0:
            alert(message='Не выбрано ни одного эксперта')
            return
        if len(self.filenames_of_recordings) == 0:
            alert(message='Не загружено ни одной работы')
            return
        # Проверка, есть ли хотя бы одна работа с загруженной записью или проверкой по потоковой аудиозаписи
        for row in range(self.students_table.rowCount()):
            try:
                # это XOR - должна быть ЛИБО проверка по потоку, ЛИБО аудиозапись
                if (self.students_table.cellWidget(row, 6).currentText() == 'Проверка по потоковой аудиозаписи') ^ \
                        (self.students_table.item(row, 7).text() == 'Да'):
                    break
            except Exception:
                continue
        else:
            alert(message='Не найдено ни одной загруженной работы или работы с проверкой по потоковой аудиозаписи. '
                          'Экспертиза невозможна.')
            return
        # Проверять будут не все эксперты, а только выделенные
        experts_to_work, experts_to_substitute = [], []
        for row in range(self.experts_table.rowCount()):
            if self.experts_table.item(row, 0).isSelected():
                experts_to_work.append(self.experts_table.item(row, 0).text())
            else:
                experts_to_substitute.append(self.experts_table.item(row, 0).text())
        experts_to_work_string = ', '.join(experts_to_work)
        experts_to_substitute_string = ', '.join(experts_to_substitute)
        if not experts_to_substitute:
            experts_to_substitute_string = '-'
        if not question(
                message=f'Выбранные для проверки эксперты: {experts_to_work_string}\n'
                        f'Эксперты, которые могут их заменить в процессе проверки: {experts_to_substitute_string}\n'
                        f'Подтвердите корректность данных.'
        ):
            return
        folder_to_export_experts_archives = QFileDialog.getExistingDirectory(
            parent=None, caption='Выберите папку для экспорта', directory=os.getcwd()
        )
        if not folder_to_export_experts_archives:
            return

        # Составление списка экспертизы - все записанные файлы + проверка по потоковой аудиозаписи (но не одновременно!)
        codes_to_export = self.filenames_of_recordings.copy()
        for row in range(self.students_table.rowCount()):
            # Если есть запись, но выставлен статус, такая работа не должна попасть к эксперту
            if self.students_table.cellWidget(row, 6).currentText() != "" and self.students_codes_list[
                row] in codes_to_export:
                codes_to_export.remove(self.students_codes_list[row])
            # Но работа со статусом "Проверка по потоковой аудиозаписи" даже при наличии записи должна быть проверена
            if self.students_table.cellWidget(row, 6).currentText() == 'Проверка по потоковой аудиозаписи':
                if self.students_codes_list[row] not in codes_to_export:
                    codes_to_export.append(self.students_codes_list[row])

        # Распределение работ по экспертам
        shuffle(codes_to_export)
        distribution_by_experts = []
        distribution_by_experts_info = dict()

        for i in range(len(experts_to_work)):
            distribution_by_experts.append([])
        for i in range(len(codes_to_export)):
            distribution_by_experts[i % len(experts_to_work)].append(codes_to_export[i])
        current_exam_info = {
            'examen_date': self.examen_date_label.text(),
            'school_number': self.school_number_label.text()
        }
        for i in range(len(experts_to_work)):
            current_expert = experts_to_work[i]
            current_expert_files = distribution_by_experts[i]
            with open('expert_file_list.txt', 'w') as out:
                print(*current_expert_files, sep='\n', file=out)
            with open('current_exam_info.json', 'w') as out:
                json.dump(current_exam_info, out, indent=4)
            with zipfile.ZipFile(os.path.join(folder_to_export_experts_archives, f'{current_expert}.zip'),
                                 'w') as archive:
                for file in current_expert_files:
                    if os.path.isfile(os.path.join(self.recordings_folder, f'{file}.ogg')):
                        archive.write(os.path.join(self.recordings_folder, f'{file}.ogg'), f'{file}.ogg')
                archive.write(self.current_dat_file, os.path.basename(self.current_dat_file))
                archive.write('expert_file_list.txt', 'expert_file_list.txt')
                archive.write('current_exam_info.json', 'current_exam_info.json')
                archive.write(experts_file, os.path.basename(experts_file))
            if os.path.isfile('expert_file_list.txt'):
                os.remove('expert_file_list.txt')
            if os.path.isfile('current_exam_info.json'):
                os.remove('current_exam_info.json')
            # Выделение могло сняться - ищем в таблице текущего эксперта
            for row in range(self.experts_table.rowCount()):
                if self.experts_table.item(row, 0).text() == experts_to_work[i]:
                    self.experts_table.setItem(row, 1, QTableWidgetItem(f'{len(current_expert_files)}'))
                    self.experts_table.setItem(row, 3, QTableWidgetItem('Да'))
                    self.experts_table.setItem(row, 4,
                                               QTableWidgetItem(datetime.datetime.now().strftime('%d.%m.%Y %H:%M')))
                    break
            # Сохранение информации о времени и количестве выданных экспертам работ
            if experts_to_work[i] not in distribution_by_experts_info:
                distribution_by_experts_info[experts_to_work[i]] = dict()
            distribution_by_experts_info[experts_to_work[i]]['План работы'] = len(current_expert_files)
            distribution_by_experts_info[experts_to_work[i]]['Проверено'] = 0
            distribution_by_experts_info[experts_to_work[i]]['Материалы выданы'] = 'Да'
            distribution_by_experts_info[experts_to_work[i]]['Дата выдачи'] = datetime.datetime.now().strftime(
                '%d.%m.%Y %H:%M')

        with open(self.experts_distribution_info_file, 'w') as out:
            json.dump(distribution_by_experts_info, out, indent=4, ensure_ascii=False)
        alert(message=f'Архивы сформированы и находятся в папке {folder_to_export_experts_archives}.')

        self.start_expertise_button.setEnabled(False)
        # после начала экспертизы нельзя добавлять экспертов
        self.add_expert_button.setEnabled(False)
        self.edit_expert_button.setEnabled(False)
        self.remove_expert_button.setEnabled(False)

    def load_marks_function(self):
        if not os.path.isfile(self.experts_distribution_info_file):
            alert(message='Невозможно загрузить оценки, поскольку экспертиза не была начата.')
            return
        files_to_import_from, _ = QFileDialog.getOpenFileNames(
            parent=None, caption='Выберите файлы с оценками', directory=os.getcwd(), filter='Файлы JSON (*.json)'
        )
        if not files_to_import_from:
            return
        experts_distribution_info = json.loads(open(self.experts_distribution_info_file).read())
        loaded_works_info = dict()
        if not os.path.isfile(self.loaded_works_info_file):
            subprocess.run(['touch', self.loaded_works_info_file])
        else:
            try:
                loaded_works_info = json.loads(open(self.loaded_works_info_file).read())
            except Exception as e:
                logging.warning(f'Не удалось получить информацию о загруженных работах. Исключение: {e}')
        successful_load = False
        for file in files_to_import_from:
            try:
                imported_marks = json.load(open(file))
                for key in imported_marks.keys():
                    all_criteria = set(imported_marks[key].keys())
                    # У каждого ученика должны быть все оценки или ключ impossible (в таком случае можно без варианта),
                    # также обязательно должен быть эксперт
                    if not all_criteria.issuperset(set(marks_headers)) and 'impossible' not in all_criteria or \
                            'expert' not in all_criteria or 'impossible' not in all_criteria and 'Variant' not in all_criteria:
                        alert(
                            message=f'Файл {file} повреждён. Для ученика с кодом {key} некорректно проставлены оценки.')
                        # Если хотя бы у одного ученика не хватает оценок, повреждённым считается весь файл
                        raise AssertionError
                    expert = imported_marks[key]['expert']
                    if expert not in self.experts_list:
                        alert(message=f'Данный архив принаждежит другой проверке. Эксперт {expert} не найден.')
                        raise AssertionError
            except Exception:
                alert(message=f'Файл {file} повреждён или не является файлом с оценками.')
                continue

            successful_load = True
            # Перебор всех кодов учеников из json с оценками
            for key in imported_marks.keys():
                current_expert = imported_marks[key]['expert']

                try:
                    i = self.students_codes_list.index(key)
                except IndexError:
                    logging.warning(f'Ученик с кодом {key} не найден в текущем экзамене!')
                    continue
                # заполнение таблицы и текущего xml
                if key not in loaded_works_info:
                    loaded_works_info[key] = dict()
                else:
                    logging.warning(
                        f'Работа ученика с кодом {key} уже была проверена экспертом {loaded_works_info[key]["Эксперт"]}!')
                    continue
                self.students_table.setItem(i, 8, QTableWidgetItem('Да'))
                loaded_works_info[key]['Работа проверена'] = 'Да'
                # В случае, если работу невозможно проверить, варианта может не быть
                if 'Variant' in imported_marks[key]:
                    self.students_table.setItem(i, 9, QTableWidgetItem(imported_marks[key]['Variant']))
                    loaded_works_info[key]['Вариант'] = imported_marks[key]['Variant']
                    self.add_value_to_xml_tree(i, 'Номер_варианта', imported_marks[key]['Variant'])
                self.students_table.setItem(i, 10, QTableWidgetItem(datetime.datetime.now().strftime('%d.%m.%Y %H:%M')))
                loaded_works_info[key]['Дата загрузки'] = datetime.datetime.now().strftime('%d.%m.%Y %H:%M')
                self.add_value_to_xml_tree(i, 'ФИО Эксперта', imported_marks[key]['expert'])
                self.students_table.setItem(i, 11, QTableWidgetItem(imported_marks[key]['expert']))
                loaded_works_info[key]['Эксперт'] = imported_marks[key]['expert']
                # Если работа проверена
                if 'impossible' not in imported_marks[key]:
                    # все оценки без номера варианта
                    marks_array = [imported_marks[key][i] for i in marks_headers[:-1]]
                    self.students_table.cellWidget(i, 6).setCurrentIndex(0)
                # Если работу невозможно проверить - досрочное завершение
                else:
                    # TODO нужно ли вносить прочерки, если работу нельзя проверить?
                    marks_array = ['-'] * (len(marks_headers) - 1)
                    # TODO какой статус ожидается в случае невозможности проверки работы?
                    self.add_value_to_xml_tree(i, 'Не_закончил', 'невозможно проверить')
                    self.students_table.cellWidget(i, 6).setCurrentText('Досрочное завершение')
                    loaded_works_info[key]['Статус'] = 'Досрочное завершение'
                experts_distribution_info[current_expert]['Проверено'] += 1
                for row in range(self.experts_table.rowCount()):
                    if self.experts_table.item(row, 0).text() == current_expert:
                        try:
                            self.experts_table.item(row, 2).setText(
                                str(experts_distribution_info[current_expert]['Проверено']))
                        except Exception:
                            self.experts_table.setItem(row, 2, QTableWidgetItem(
                                str(experts_distribution_info[current_expert]['Проверено'])))
                self.separate_marks_dict[key] = marks_array
                loaded_works_info[key]['Оценки'] = marks_array
                self.fill_marks_in_xml_tree(i, marks_array)
                self.add_value_to_xml_tree(i, 'Итоговый_Балл', imported_marks[key]['Итого'])
                if 'impossible' not in loaded_works_info[key]:
                    self.students_table.setItem(i, 12, QTableWidgetItem(imported_marks[key]['Итого']))
                else:
                    self.students_table.setItem(i, 12, QTableWidgetItem())
                loaded_works_info[key]['Балл'] = imported_marks[key]['Итого']

                self.export_all_exam_button.setEnabled(True)
            with open(self.experts_distribution_info_file, 'w') as out:
                json.dump(experts_distribution_info, out, indent=4, ensure_ascii=False)
            with open(self.loaded_works_info_file, 'w') as out:
                json.dump(loaded_works_info, out, indent=4, ensure_ascii=False)
        if successful_load:
            alert(message=f'Оценки успешно загружены.')

    def get_param_from_source_xml_and_set_to_export_xml(self, students_index, input_param,
                                                        current_output_xml_participant, output_param):
        value = self.get_value_from_source_xml(students_index, input_param)
        new_tag = ET.SubElement(current_output_xml_participant, output_param)
        if input_param != 'РезервI':
            new_tag.text = value
        else:
            if value == '':
                new_tag.text = 'False'
            else:
                new_tag.text = 'True'

    def create_export_xml_from_source(self):
        self.tree_for_ERP = ET.ElementTree(ET.Element('Protocol'))
        self.fill_tree_for_ERP_header()
        # заполнение xml для экспорта в МЦКО
        input_and_output_tag_pairs = {
            'Код_в_базе': 'Id',
            'Код_участника': 'RegNumber',
            # TODO так и оставить опечатку?
            'Аудитория': 'AudiotryCode',
            'Класс': 'Class',
            'Номер_КИМ': 'KimNumber',
            'Штрих_код': 'Barcode',
            'РезервI': 'IsOvz',
            'Не_закончил': 'Status',
            'Номер_варианта': 'Variant'
        }

        indx = -1
        for item in self.root.iter('Block'):
            if 'Фамилия' in item.attrib['BlockName'] and item.text:
                indx += 1
                surname = item.text
                name = self.get_value_from_source_xml(indx, 'Имя')
                patronymic = self.get_value_from_source_xml(indx, 'Отчество')
                fio = f'{surname} {name} {patronymic}'.strip()
                current_expert = self.get_value_from_source_xml(indx, 'Эксперт')

                # if not current_expert:
                #     continue

                # если эксперт уже есть, ему добавляется ученик
                if len([i for i in self.tree_for_ERP.findall('.//ExpertFullName') if i.text == current_expert]) != 0:
                    expert_full_name = \
                        [i for i in self.tree_for_ERP.findall('.//ExpertFullName') if i.text == current_expert][0]
                    expertise = [i for i in self.tree_for_ERP.getroot().iter() if expert_full_name in i][0]
                    participants = expertise.find('.//Participants')
                # Если эксперта нет, создаётся экспертиза
                else:
                    expertise_packages = self.tree_for_ERP.find('ExpertisePackages')
                    expertise = ET.SubElement(expertise_packages, 'Expertise')
                    expert_full_name = ET.SubElement(expertise, 'ExpertFullName')
                    expert_full_name.text = current_expert
                    participants = ET.SubElement(expertise, 'Participants')

                export_xml_participant = ET.SubElement(participants, 'Participant')

                export_xml_fullname = ET.SubElement(export_xml_participant, 'FullName')
                export_xml_fullname.text = fio

                for input_and_output_tag_pairs_key in input_and_output_tag_pairs:
                    self.get_param_from_source_xml_and_set_to_export_xml(
                        students_index=indx, input_param=input_and_output_tag_pairs_key,
                        current_output_xml_participant=export_xml_participant,
                        output_param=input_and_output_tag_pairs[input_and_output_tag_pairs_key]
                    )

                current_marks = self.get_marks_from_xml_by_index(indx)
                for j in range(len(marks_headers)):
                    if marks_headers[j] != 'Variant':
                        score = ET.SubElement(export_xml_participant, 'Score')
                        score.set('Name', marks_headers[j])
                        score.set('Number', f'{j + 1}')
                        if current_marks:
                            score.text = current_marks[j]

    def export_all_exam_function(self):
        if not question(message='Вы действительно хотите завершить экзамен и экспортировать его результаты?'):
            return
        # if not enter_password():
        #     return
        export_folder = QFileDialog.getExistingDirectory(
            parent=None, caption='Выберите папку для экспорта', directory=os.getcwd()
        )
        if not export_folder:
            return
        self.tree.write(
            os.path.join(export_folder, f'{self.discipline_label.text()}-{self.examen_date_label.text()}.xml'),
            encoding='utf-8')

        self.create_export_xml_from_source()
        formatted_date = datetime.datetime.today().strftime('%d%m%Y')
        formatted_datetime = datetime.datetime.now().strftime('%d.%m.%Y %H:%M')
        tree_indent(self.tree_for_ERP.getroot())
        exam_basename = f'ERP_77_{self.school_number_label.text()}_{formatted_date}_{self.examen_date_label.text()}'
        # TODO как добавлять в экспортный xml тех, кому на станции управления выставили досрочное завершени?
        self.tree_for_ERP.write(
            os.path.join(export_folder, f'{exam_basename}.xml'),
            encoding='utf-8', xml_declaration=True
        )
        export_current_exam_to_pdf(
            students_table=self.students_table,
            export_folder=export_folder,
            export_filename=f'{exam_basename}.pdf',
            region=self.region_label.text(),
            school_code=self.school_number_label.text(),
            discipline=self.discipline_label.text(),
            examen_date=self.examen_date_label.text(),
            export_datetime=formatted_datetime,
            separate_marks_dict=self.separate_marks_dict
        )

        alert(message=f'Экзамен экспортирован.\n'
                      f'Заполненный основной XML: {self.discipline_label.text()}-{self.examen_date_label.text()}.xml\n'
                      f'XML для экспорта: {exam_basename}.xml\n'
                      f'Ведомость PDF: {exam_basename}.pdf'
              )
        self.delete_all_exam_info_button.setEnabled(True)

    def fill_tree_for_ERP_header(self):
        self.tree_for_ERP.getroot().set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
        self.tree_for_ERP.getroot().set('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema')
        region = ET.SubElement(self.tree_for_ERP.getroot(), 'Region')
        region.text = self.region_label.text()
        ppe = ET.SubElement(self.tree_for_ERP.getroot(), 'Ppe')
        ppe.text = self.school_number_label.text()
        subject_code = ET.SubElement(self.tree_for_ERP.getroot(), 'SubjectCode')
        subject_code.text = '20'
        subject_name = ET.SubElement(self.tree_for_ERP.getroot(), 'SubjectName')
        subject_name.text = 'Итоговое собеседование по русскому языку'
        exam_date = ET.SubElement(self.tree_for_ERP.getroot(), 'ExamDate')
        exam_date.text = self.examen_date_label.text()
        export_date = ET.SubElement(self.tree_for_ERP.getroot(), 'ExportDate')
        export_date.text = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f')
        expertise_packages = ET.SubElement(self.tree_for_ERP.getroot(), 'ExpertisePackages')

    def switch_two_audio_function(self):
        if not enter_password():
            return
        selected_indexes = self.students_table.selectedIndexes()
        # Если не выбрано ровно две строки (без последнего столбца - он скрыт), поменять местами аудио нельзя
        if len(selected_indexes) != 2 * (self.students_table.columnCount() - 1):
            alert(message='Чтобы поменять местами аудиофайлы двух учеников, выберите ровно две строки в таблице.')
            return
        for index in selected_indexes:
            if index.column() == 7 and not self.students_table.item(index.row(), index.column()):
                alert(message='У обоих выбранных учеников должны присутствовать аудиозаписи')
                return
        selected_rows = []
        for index in selected_indexes:
            if index.row() not in selected_rows:
                selected_rows.append(index.row())
        selected_students_codes = [self.students_codes_list[i] for i in selected_rows]
        selected_audio_files = [
            os.path.join(self.recordings_folder, f'{i}.ogg') for i in selected_students_codes
        ]
        temp_file = subprocess.run(['mktemp'], capture_output=True).stdout.decode().strip()
        os.remove(temp_file)
        shutil.copyfile(selected_audio_files[0], temp_file)
        shutil.copyfile(selected_audio_files[1], selected_audio_files[0])
        shutil.copyfile(temp_file, selected_audio_files[1])
        fio1 = f'{self.students_table.item(selected_rows[0], 0).text()} {self.students_table.item(selected_rows[0], 1).text()} {self.students_table.item(selected_rows[0], 2).text()}'
        fio2 = f'{self.students_table.item(selected_rows[1], 0).text()} {self.students_table.item(selected_rows[1], 1).text()} {self.students_table.item(selected_rows[1], 2).text()}'
        alert(message=f'Успешно поменяли местами аудиофайлы для учеников {fio1} и {fio2}.')

    def update_students_codes_list_after_sorting(self):
        """
        Функция нужна для того, чтобы в списке кодов учеников они шли в таком же порядке, в котором в таблице,
        так как они взяимосвязаны во многих других функциях.
        """
        self.students_codes_list = []
        for row in range(self.students_table.rowCount()):
            self.students_codes_list.append(
                self.students_table.item(row, self.students_table.columnCount() - 1).text()
            )

    def delete_all_axam_config(self):
        if not enter_password():
            return
        if not question(message='Вы действительно хотите удалить всю информацию об экзамене? '
                                'Это действие невозможно отменить.'):
            return
        shutil.rmtree(self.config_folder)
        alert(message='Информация удалена.')
        self.choose_examen()

    def closeEvent(self, a0: QCloseEvent):
        if not question(message='Вы действительно хотите выйти из программы?'):
            a0.ignore()
            return
        a0.accept()

    def choose_examen(self):
        old_config_folder = self.config_folder
        self.config_folder = get_config_folder_for_current_exam()
        if not self.config_folder and not old_config_folder:
            alert(message='Не выбрано ни одного экзамена. Программа завершит работу.')
            sys.exit(0)
        elif not self.config_folder and old_config_folder:
            self.config_folder = old_config_folder
            return
        self.config_folder = os.path.join(root_config_folder, self.config_folder)
        self.recordings_folder = get_audio_records_folder(self.config_folder)
        self.config_file = get_config_file(self.config_folder)
        self.experts_distribution_info_file = get_experts_distribution_info_file(self.config_folder)
        self.loaded_works_info_file = get_loaded_works_info_file(self.config_folder)
        self.md5_folder = get_md5_folder(self.config_folder)
        self.pdf_folder = get_pdf_folder(self.config_folder)

        subprocess.run(['mkdir', '-p', self.config_folder])
        subprocess.run(['touch', self.config_file])
        self.files = [os.path.join(self.config_folder, i) for i in os.listdir(self.config_folder) if i.endswith('.xml')]

        self.tree = None

        self.current_exam_file = subprocess.run(['py-ini-config', 'get', self.config_file, '-n', 'current_exam_file'],
                                                capture_output=True).stdout.decode().strip()
        if not self.current_exam_file:
            chosen_file, _ = QFileDialog.getOpenFileName(
                parent=None, caption='Выберите xml-файл с информацией об экзамене',
                directory=os.getenv('HOME') + '/Загрузки',
                filter='XML Files (*.xml)'
            )
            if not chosen_file:
                alert(message='Для подготовки к экзамену необходимо выбрать XML-файл. Программа завершит работу.')
                sys.exit(0)
            if not chosen_file.endswith('.xml'):
                alert(message='Выбранный файл не является файлом xml.')
                sys.exit(0)
            else:
                # XML-файл сразу копируется в папку конфига на случай, если его открыли со съёмноо носителя
                shutil.copyfile(chosen_file, os.path.join(self.config_folder, os.path.basename(chosen_file)))
                self.current_exam_file = os.path.join(self.config_folder, os.path.basename(chosen_file))
                subprocess.run(
                    ['py-ini-config', 'set', self.config_file, '-n', 'current_exam_file', self.current_exam_file])

        # DAT-файл загружается отдельно (в день экзамена)
        self.current_dat_file = subprocess.run(['py-ini-config', 'get', self.config_file, '-n', 'current_dat_file'],
                                               capture_output=True).stdout.decode().strip()

        for folder in (self.recordings_folder, self.pdf_folder, self.md5_folder):
            subprocess.run(['mkdir', '-p', folder])
        self.students_codes_list = []
        self.students_codes_list_without_sorting = []
        # имена аудиозаписей без расширения
        self.filenames_of_recordings = []
        self.experts_list = []
        if os.path.isfile(experts_file):
            self.experts_list = open(experts_file, 'r').readlines()
            for i in range(len(self.experts_list)):
                self.experts_list[i] = self.experts_list[i].strip()
            while '' in self.experts_list:
                self.experts_list.remove('')
        self.separate_marks_dict = dict()
        try:
            if self.students_table:
                self.load_exam()
                self.update_loaded_works_info()
            if self.experts_table:
                self.update_experts_table()
        except Exception as e:
            logging.warning(f'Во время выбора экзамена возникло исключение: {e}')
        # не сработает в первый раз, до инициализации интерфейса
        try:
            self.delete_all_exam_info_button.setEnabled(False)
            self.add_expert_button.setEnabled(True)
            self.edit_expert_button.setEnabled(True)
            self.remove_expert_button.setEnabled(True)
            if os.path.isfile(self.experts_distribution_info_file) or not self.current_dat_file:
                self.start_expertise_button.setEnabled(False)
                if os.path.isfile(self.experts_distribution_info_file):
                    self.start_expertise_button.setToolTip('Экспертиза уже была начата')
                else:
                    self.start_expertise_button.setToolTip('Необходимо загрузить варианты')
            else:
                self.start_expertise_button.setEnabled(True)
                self.start_expertise_button.setToolTip('')
        except Exception as e:
            logging.warning(f'Во время обновления интерфейса при выборе экзамена возникло исключение: {e}')

    def update_experts_table(self):
        # очистить таблицу после предыдущего экзамена
        self.experts_table.setRowCount(0)
        self.experts_table.setRowCount(len(self.experts_list))
        for row in range(len(self.experts_list)):
            self.experts_table.setItem(row, 0, QTableWidgetItem(self.experts_list[row]))
        if os.path.isfile(self.experts_distribution_info_file):
            experts_distribution_info = json.loads(open(self.experts_distribution_info_file).read())
            for row in range(len(self.experts_list)):
                if self.experts_list[row] in experts_distribution_info:
                    try:
                        self.experts_table.setItem(row, 1, QTableWidgetItem(
                            str(experts_distribution_info[self.experts_list[row]]['План работы'])))
                        self.experts_table.setItem(row, 2, QTableWidgetItem(
                            str(experts_distribution_info[self.experts_list[row]]['Проверено'])))
                        self.experts_table.setItem(row, 3, QTableWidgetItem(
                            experts_distribution_info[self.experts_list[row]]['Материалы выданы']))
                        self.experts_table.setItem(row, 4, QTableWidgetItem(
                            experts_distribution_info[self.experts_list[row]]['Дата выдачи']))
                    except Exception as e:
                        logging.error(f'Во время заполнения таблицы экспертов возникла ошибка: {e}')

    def reset_all_filters(self):
        for i in range(len(self.filter_rows_for_students_table)):
            self.filter_rows_for_students_table[i] = ''
            self.students_table.horizontalHeaderItem(i).setIcon(QIcon())
        for row in range(self.students_table.rowCount()):
            self.students_table.setRowHidden(row, False)

    def filter_students_table(self, entered_filter_text):
        self.filter_rows_for_students_table[self.clicked_header_index] = entered_filter_text.strip()
        if self.filter_rows_for_students_table[self.clicked_header_index]:
            self.students_table.horizontalHeaderItem(self.clicked_header_index).setIcon(QIcon.fromTheme('view-filter'))
        else:
            self.students_table.horizontalHeaderItem(self.clicked_header_index).setIcon(QIcon())

        for row in range(self.students_table.rowCount()):
            self.students_table.setRowHidden(row, False)
        for column in range(len(self.filter_rows_for_students_table)):
            for row in range(self.students_table.rowCount()):
                # Если в ячейке вообще нет виджета
                if not self.students_table.item(row, column):
                    # Если в фильтре любой текст, кроме (пусто), скрыть строку, иначе оставить
                    if self.filter_rows_for_students_table[column].strip() and self.filter_rows_for_students_table[
                        column].strip() != '(пусто)':
                        self.students_table.setRowHidden(row, True)
                    else:
                        continue
                # Если виджет есть
                else:
                    # Если текст фильтра не содержится в ячейке, то скрыть
                    if self.filter_rows_for_students_table[column].strip().lower() \
                            not in self.students_table.item(row, column).text().lower():
                        self.students_table.setRowHidden(row, True)
                    # Если текст пустой, а фильтр равен (пусто), то отобразить
                    if self.students_table.item(row, column).text().strip() == '' and \
                            self.filter_rows_for_students_table[column] == '(пусто)':
                        self.students_table.setRowHidden(row, False)

    def show_filter_window(self, position):
        self.clicked_header_index = self.sender().logicalIndexAt(position)
        if self.filter_window:
            try:
                self.filter_window.close()
            except Exception:
                pass
        possible_values = set()
        for row in range(self.students_table.rowCount()):
            if self.students_table.item(row, self.clicked_header_index):
                possible_values.add(self.students_table.item(row, self.clicked_header_index).text())
        possible_values = ['(пусто)'] + sorted(list(possible_values))
        if '' in possible_values:
            possible_values.remove('')
        self.filter_window = FilterDialog(
            cursor_pos=self.cursor().pos(),
            text_in_field=self.filter_rows_for_students_table[self.clicked_header_index],
            possible_values=list(possible_values))
        self.filter_window.filter_input.currentTextChanged.connect(
            lambda text: self.filter_students_table(text))

    def __init__(self):
        self.clicked_header_index = None
        if not check_if_program_is_not_hacked():
            alert(message='В код программы были внесены изменения. Переустановите программу и запустите заново.')
            sys.exit(0)
        self.separate_marks_dict = dict()
        self.experts_list = []
        self.current_exam_file = ''
        self.current_dat_file = ''
        self.filenames_of_recordings = []
        self.students_codes_list_without_sorting = []
        self.students_codes_list = []
        self.tree = None
        self.xml_with_current_exam = ''
        self.files = []
        self.pdf_folder = ''
        self.md5_folder = ''
        self.loaded_works_info_file = ''
        self.experts_distribution_info_file = ''
        self.config_file = ''
        self.recordings_folder = ''
        self.config_folder = ''
        self.students_table = None
        self.filter_window = None
        if not enter_password():
            sys.exit(0)
        QMainWindow.__init__(self)

        subprocess.run(['mkdir', '-p', root_config_folder])

        self.choose_examen()
        self.initUI()

    def initUI(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout()
        central_widget.setLayout(layout)

        info_about_exam_groupbox = QGroupBox()
        info_about_exam_groupbox_layout = QGridLayout()
        info_about_exam_groupbox.setLayout(info_about_exam_groupbox_layout)
        region_horizontal_widget = QWidget()
        region_horizontal_layout = QHBoxLayout()
        region_horizontal_widget.setLayout(region_horizontal_layout)
        region_horizontal_layout.addWidget(QLabel('Регион: '))
        self.region_label = QLabel()
        region_horizontal_layout.addWidget(self.region_label)
        region_horizontal_layout.addStretch()
        info_about_exam_groupbox_layout.addWidget(region_horizontal_widget, 0, 0, 1, 2)
        school_number_horizontal_widget = QWidget()
        school_number_horizontal_layout = QHBoxLayout()
        school_number_horizontal_widget.setLayout(school_number_horizontal_layout)
        school_number_horizontal_layout.addWidget(QLabel('ОО: '))
        self.school_number_label = QLabel()
        school_number_horizontal_layout.addWidget(self.school_number_label)
        school_number_horizontal_layout.addStretch()
        info_about_exam_groupbox_layout.addWidget(school_number_horizontal_widget, 0, 2, 1, 2)
        exam_date_horizontal_widget = QWidget()
        exam_date_horizontal_layout = QHBoxLayout()
        exam_date_horizontal_widget.setLayout(exam_date_horizontal_layout)
        exam_date_horizontal_layout.addWidget(QLabel('Дата экзамена: '))
        self.examen_date_label = QLabel()
        exam_date_horizontal_layout.addWidget(self.examen_date_label)
        exam_date_horizontal_layout.addStretch()
        info_about_exam_groupbox_layout.addWidget(exam_date_horizontal_widget, 0, 4, 1, 2)

        discipline_horizontal_widget = QWidget()
        discipline_horizontal_layout = QHBoxLayout()
        discipline_horizontal_widget.setLayout(discipline_horizontal_layout)
        discipline_horizontal_layout.addWidget(QLabel('Предмет: 20 - Итоговое собеседование по русскому языку'))
        discipline_horizontal_layout.addStretch()
        info_about_exam_groupbox_layout.addWidget(discipline_horizontal_widget, 1, 0, 1, 6)
        self.discipline_label = QLabel()
        # info_about_exam_groupbox_layout.addWidget(self.discipline_label, 1, 3, 1, 3)
        participants_number_horizontal_widget = QWidget()
        participants_number_horizontal_layout = QHBoxLayout()
        participants_number_horizontal_widget.setLayout(participants_number_horizontal_layout)
        participants_number_horizontal_layout.addWidget(QLabel('Участников: '))
        self.number_of_participants_label = QLabel()
        participants_number_horizontal_layout.addWidget(self.number_of_participants_label)
        participants_number_horizontal_layout.addStretch()
        info_about_exam_groupbox_layout.addWidget(participants_number_horizontal_widget, 2, 0, 1, 2)
        works_number_horizontal_widget = QWidget()
        works_number_horizontal_layout = QHBoxLayout()
        works_number_horizontal_widget.setLayout(works_number_horizontal_layout)
        works_number_horizontal_layout.addWidget(QLabel('Кол-во работ: '))
        self.number_of_recordings_label = QLabel()
        works_number_horizontal_layout.addWidget(self.number_of_recordings_label)
        works_number_horizontal_layout.addStretch()
        info_about_exam_groupbox_layout.addWidget(works_number_horizontal_widget, 2, 2, 1, 2)

        layout.addWidget(info_about_exam_groupbox)

        left_and_right_panels_layout = QHBoxLayout()

        left_panel_layout = QVBoxLayout()

        self.load_dat_file_button = QPushButton('Загрузить варианты')
        self.load_dat_file_button.clicked.connect(self.load_dat_file)
        left_panel_layout.addWidget(self.load_dat_file_button)

        self.export_xml_button = QPushButton('Экспортировать xml')
        self.export_xml_button.clicked.connect(self.export_xml_for_recording)
        left_panel_layout.addWidget(self.export_xml_button)

        self.import_recordings_button = QPushButton('Загрузить записи')
        self.import_recordings_button.clicked.connect(self.import_recordings_function)
        left_panel_layout.addWidget(self.import_recordings_button)

        self.switch_tables_button = QPushButton('Перейти к экспертам')
        self.switch_tables_button.clicked.connect(self.switch_stacked_layout)
        left_panel_layout.addWidget(self.switch_tables_button)

        self.load_marks_button = QPushButton('Загрузить оценки')
        self.load_marks_button.clicked.connect(self.load_marks_function)
        left_panel_layout.addWidget(self.load_marks_button)

        left_panel_layout.addStretch()

        self.export_all_exam_button = QPushButton('Завершить экзамен')
        self.export_all_exam_button.clicked.connect(self.export_all_exam_function)
        self.export_all_exam_button.setEnabled(False)
        left_panel_layout.addWidget(self.export_all_exam_button)

        self.delete_all_exam_info_button = QPushButton('Удалить всю информацию')
        self.delete_all_exam_info_button.clicked.connect(self.delete_all_axam_config)
        self.delete_all_exam_info_button.setEnabled(False)
        left_panel_layout.addWidget(self.delete_all_exam_info_button)

        if not self.current_dat_file:
            self.switch_enable_all_buttons(False)

        left_and_right_panels_layout.addLayout(left_panel_layout)

        right_panel_layout = QVBoxLayout()

        self.scrollable_table_area = QScrollArea()

        self.students_widget = QWidget()
        students_widget_layout = QGridLayout()
        self.students_widget.setLayout(students_widget_layout)
        students_widget_layout.addWidget(QLabel('Участники'), 0, 0)

        self.enter_auditory_button = QPushButton('Ввести аудиторию')
        self.enter_auditory_button.clicked.connect(self.enter_auditory)
        students_widget_layout.addWidget(self.enter_auditory_button, 0, 1)

        self.switch_two_audio_files_button = QPushButton('Поменять местами аудио')
        self.switch_two_audio_files_button.clicked.connect(self.switch_two_audio_function)
        students_widget_layout.addWidget(self.switch_two_audio_files_button, 0, 2)

        self.filter_button = QPushButton('Сбросить все фильтры')
        self.filter_button.clicked.connect(self.reset_all_filters)
        students_widget_layout.addWidget(self.filter_button, 0, 3)

        self.students_table = QTableWidget()
        labels = [
            'Фамилия',
            'Имя',
            'Отчество',
            'ОВЗ',
            'Аудитория',
            'Класс',
            'Статус',
            'Работа загружена',
            'Работа проверена',
            'Вариант',
            'Дата загрузки',
            'Эксперт',
            'Балл'
        ]
        # Последний столбец будет содержать коды учеников и будет скрыт.
        # Его наличие необходимо, чтобы коды сортировались так же, как и видимые столбцы.
        self.students_table.setColumnCount(len(labels) + 1)
        self.students_table.setRowCount(0)
        self.students_table.setColumnHidden(len(labels), True)
        self.students_table.setHorizontalHeaderLabels(labels)
        self.students_table.setSortingEnabled(True)
        self.students_table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.students_table.customContextMenuRequested.connect(self.context_menu_called)
        self.students_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.students_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.load_exam()
        self.update_loaded_works_info()

        self.students_table.horizontalHeader().setStretchLastSection(True)
        self.students_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
        self.students_table.horizontalHeader().sectionClicked.connect(self.update_students_codes_list_after_sorting)

        self.students_table.horizontalHeader().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.students_table.horizontalHeader().customContextMenuRequested.connect(
            lambda position: self.show_filter_window(position))
        self.students_table.horizontalHeaderItem(3).setIcon(QIcon.fromTheme('device-notifier'))
        self.students_table.horizontalHeaderItem(3).setIcon(QIcon())

        students_widget_layout.addWidget(self.students_table, 1, 0, 1, 4)

        # Список строк с текущими фильтрами
        self.filter_rows_for_students_table = [''] * len(labels)
        # не по количеству стоблцов - последний скрыт!

        self.scrollable_table_area.setWidgetResizable(True)
        right_panel_layout.addWidget(self.scrollable_table_area)

        stacked_tables_widget = QWidget()
        self.stacked_tables_layout = QStackedLayout()
        stacked_tables_widget.setLayout(self.stacked_tables_layout)
        self.stacked_tables_layout.addWidget(self.students_widget)
        self.scrollable_table_area.setWidget(stacked_tables_widget)

        # Создание таблицы экспертов - изначально она не отображается
        self.experts_widget = QWidget()
        experts_widget_layout = QGridLayout()
        self.experts_widget.setLayout(experts_widget_layout)
        self.add_expert_button = QPushButton('Добавить эксперта')
        self.add_expert_button.clicked.connect(self.add_new_expert)
        experts_widget_layout.addWidget(self.add_expert_button, 0, 0)

        self.edit_expert_button = QPushButton('Редактировать эксперта')
        self.edit_expert_button.clicked.connect(self.edit_expert_function)
        experts_widget_layout.addWidget(self.edit_expert_button, 0, 1)

        self.remove_expert_button = QPushButton('Удалить эксперта')
        self.remove_expert_button.clicked.connect(self.remove_expert_function)
        experts_widget_layout.addWidget(self.remove_expert_button, 0, 2)

        self.start_expertise_button = QPushButton('Начать экспертизу')
        if not self.current_dat_file:
            self.start_expertise_button.setEnabled(False)
            self.start_expertise_button.setToolTip('Необходимо загрузить варианты')
        if os.path.isfile(self.experts_distribution_info_file):
            self.start_expertise_button.setEnabled(False)
            self.start_expertise_button.setToolTip('Экспертиза уже была начата')
        self.start_expertise_button.clicked.connect(self.start_expertise_function)
        experts_widget_layout.addWidget(self.start_expertise_button, 0, 3)

        self.experts_table = QTableWidget()
        self.experts_table.setRowCount(len(self.experts_list))
        experts_headers = [
            'ФИО эксперта',
            "План работы",
            "Проверено",
            "Материалы выданы",
            "Дата выдачи",
        ]
        self.experts_table.setColumnCount(len(experts_headers))
        self.experts_table.setHorizontalHeaderLabels(experts_headers)
        self.experts_table.horizontalHeader().setStretchLastSection(True)
        for col in range(self.experts_table.columnCount()):
            self.experts_table.horizontalHeader().setSectionResizeMode(col, QHeaderView.ResizeMode.ResizeToContents)
        self.experts_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.experts_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)

        self.update_experts_table()

        experts_widget_layout.addWidget(self.experts_table, 1, 0, 1, 4)

        expertise_info_label = QLabel('Для начала экспертизы выделите в таблице тех экспертов, которые будут '
                                      'участвовать в проверке.\nУбедитесь, что все эксперты, которые могут принять '
                                      'участие в проверке, внесены в таблицу!')
        expertise_info_label.setWordWrap(True)
        experts_widget_layout.addWidget(expertise_info_label, 2, 0, 1, 4)

        self.stacked_tables_layout.addWidget(self.experts_widget)

        left_and_right_panels_layout.addLayout(right_panel_layout)

        layout.addLayout(left_and_right_panels_layout)

        menubar = self.menuBar()
        self.window_menu = QMenu('Файл')
        menubar.addMenu(self.window_menu)
        self.switch_examen_action = QAction('Перейти к другому экзамену')
        self.switch_examen_action.triggered.connect(self.choose_examen)
        self.window_menu.addAction(self.switch_examen_action)
        self.print_auditories_lists_action = QAction('Печать поаудиторных списков')
        self.print_auditories_lists_action.triggered.connect(self.print_auditories_lists_function)
        self.window_menu.addAction(self.print_auditories_lists_action)
        self.quit_action = QAction('Выйти')
        self.quit_action.triggered.connect(lambda: self.close())
        self.window_menu.addAction(self.quit_action)

        self.setGeometry(100, 100, 1200, 800)
        # Если программа уже установлена
        if os.path.isfile(f'/usr/share/icons/hicolor/scalable/apps/{icon_file}'):
            self.setWindowIcon(QIcon.fromTheme(icon_file.replace('.svg', '')))
        # Если ещё тестируется
        else:
            self.setWindowIcon(QIcon(icon_file))
        self.setWindowTitle(f"Станция управления и планирования ИС {version}")
        logging.info('Программа запущена')
        self.show()


if __name__ == '__main__':
    root = logging.getLogger()
    if root.handlers:
        for handler in root.handlers:
            root.removeHandler(handler)
    filename = log_file
    logging.basicConfig(
        filename=filename,
        format=u'%(asctime)s %(filename)s line:%(lineno)d %(funcName)s() %(message)s',
        level=logging.DEBUG)
    reset_log_to_last_n_days(log_file=log_file, n=7)
    
    try:
        app = QApplication(sys.argv)
    
        lockfile = QLockFile('/tmp/school-interview-system-prepare.lock')
        if not lockfile.tryLock(100):
            alert(message='Программа уже запущена. Запуск второго экземпляра не допускается.')
            sys.exit(0)
        window = MyWindow()
        sys.exit(app.exec())
    except Exception as e:
        logging.error(f'Во время выполнения программы возникла ошибка: {e}')
