import logging
import os
import random
import sqlite3  # Импортируем sqlite3
from datetime import datetime, timedelta
from io import BytesIO

import matplotlib.pyplot as plt
import pandas as pd
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from dotenv import load_dotenv
from telegram import (
    Update, InlineKeyboardButton, InlineKeyboardMarkup,
    ReplyKeyboardMarkup, KeyboardButton
)
from telegram.ext import (
    ApplicationBuilder, CommandHandler, ContextTypes,
    CallbackQueryHandler, MessageHandler, filters, ConversationHandler
)

# Загрузка переменных окружения из .env файла
load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN")

# Настройка логирования
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.DEBUG,  # Уровень DEBUG для детального логирования
    handlers=[
        logging.FileHandler("attendance_bot.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# Подключение к базе данных
conn = sqlite3.connect('attendance_bot.db', check_same_thread=False)
cursor = conn.cursor()

# Создание таблиц, если они не существуют
cursor.execute('''
    CREATE TABLE IF NOT EXISTS students (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL UNIQUE,
        duty_count INTEGER DEFAULT 0
    )
''')

cursor.execute('''
    CREATE TABLE IF NOT EXISTS attendance (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        date TEXT NOT NULL,
        student_id INTEGER,
        status TEXT,
        FOREIGN KEY(student_id) REFERENCES students(id)
    )
''')

# Таблица администраторов с ролями
cursor.execute('''
    CREATE TABLE IF NOT EXISTS admins (
        user_id INTEGER PRIMARY KEY,
        role TEXT NOT NULL DEFAULT 'user'  -- Роли: main_admin, admin, junior_admin, user
    )
''')
conn.commit()

# Проверка наличия столбца 'role' в таблице 'admins' и добавление, если отсутствует
cursor.execute("PRAGMA table_info(admins)")
columns = [info[1] for info in cursor.fetchall()]
if 'role' not in columns:
    cursor.execute("ALTER TABLE admins ADD COLUMN role TEXT NOT NULL DEFAULT 'user'")
    conn.commit()
    logger.info("Добавлен столбец 'role' в таблицу 'admins'.")
else:
    logger.info("Столбец 'role' уже существует в таблице 'admins'.")

# Ваш Telegram ID для начального администратора (замените на ваш фактический Telegram ID)
INITIAL_ADMIN_USER_ID = 1525524847  # Замените на ваш Telegram ID

# Добавление начального администратора с ролью main_admin
cursor.execute("INSERT OR IGNORE INTO admins (user_id, role) VALUES (?, ?)", (INITIAL_ADMIN_USER_ID, 'main_admin'))
conn.commit()

# Определение состояний для ConversationHandler
ADDING_STUDENT = 1
ADDING_STUDENTS_LIST = 2
DELETING_STUDENT = 3
ADDING_ADMIN = 4
REMOVING_ADMIN = 5
SELECT_REPORT_TYPE = 6
SELECT_DUTY_HISTORY_STUDENT = 7

# Инициализация планировщика
scheduler = AsyncIOScheduler()

# Форматирование имени: "Фамилия И. О."
def format_name(full_name):
    parts = full_name.strip().split()
    if len(parts) < 2:
        return full_name  # Если формат некорректен, возвращаем как есть
    surname = parts[0]
    initials = ' '.join([f"{name[0]}." for name in parts[1:]])
    return f"{surname} {initials}"

# Декоратор для проверки роли
def role_required(required_roles):
    def decorator(func):
        async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE):
            user_id = update.effective_user.id
            cursor.execute("SELECT role FROM admins WHERE user_id = ?", (user_id,))
            result = cursor.fetchone()
            if not result or result[0] not in required_roles:
                await update.message.reply_text("У вас нет доступа к этой команде.")
                logger.warning(f"Пользователь с ID {user_id} попытался использовать команду без необходимых прав.")
                # Отправка уведомления main_admin о попытке несанкционированного доступа
                cursor.execute("SELECT user_id FROM admins WHERE role = 'main_admin'")
                main_admins = cursor.fetchall()
                for admin in main_admins:
                    try:
                        if admin[0] != user_id:
                            await context.bot.send_message(
                                chat_id=admin[0],
                                text=f"⚠️ Пользователь с ID {user_id} попытался использовать команду без необходимых прав."
                            )
                    except Exception as e:
                        logger.error(f"Не удалось отправить уведомление main_admin с ID {admin[0]}: {e}")
                return
            return await func(update, context)
        return wrapped
    return decorator

# Кастомная клавиатура для главного меню с учетом роли
def main_menu_keyboard(role='user'):
    keyboard = []
    if role in ['main_admin', 'admin']:
        keyboard = [
            [KeyboardButton("➕ Добавить студента"), KeyboardButton("➕➕ Добавить нескольких студентов")],
            [KeyboardButton("❌ Удалить студента"), KeyboardButton("📅 Отметить посещаемость")],
            [KeyboardButton("🛠 Назначить дежурство"), KeyboardButton("📊 Статистика")],
            [KeyboardButton("📈 Генерация отчетов"), KeyboardButton("📊 Визуализация данных")],
            [KeyboardButton("📜 История дежурств")],
            [KeyboardButton("🔐 Добавить администратора"), KeyboardButton("🔒 Удалить администратора")],
            [KeyboardButton("🔄 Сбросить отметки"), KeyboardButton("🕹 Отмена")],
        ]
    elif role == 'junior_admin':
        keyboard = [
            [KeyboardButton("📅 Отметить посещаемость")],
            [KeyboardButton("🕹 Отмена")],
        ]
    else:  # user
        keyboard = [
            [KeyboardButton("🕹 Отмена")],
        ]
    return ReplyKeyboardMarkup(keyboard, resize_keyboard=True)

# Функция для получения роли пользователя
def get_user_role(user_id):
    cursor.execute("SELECT role FROM admins WHERE user_id = ?", (user_id,))
    result = cursor.fetchone()
    return result[0] if result else 'user'

# Обработчик неизвестных сообщений
async def unknown(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    role = get_user_role(user_id)
    await update.message.reply_text(
        "Команда не распознана. Пожалуйста, выберите действие из меню ниже:",
        reply_markup=main_menu_keyboard(role)
    )
    logger.warning(f"Пользователь {user_id} отправил неизвестную команду или сообщение.")

# Команда /start
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    role = get_user_role(user_id)

    await update.message.reply_text(
        "Привет! Я помогу вам вести посещаемость и распределять дежурства.\n\nВыберите действие из меню ниже:",
        reply_markup=main_menu_keyboard(role)
    )
    logger.info(f"Пользователь {user_id} начал взаимодействие с ботом. Роль: {role}.")

# Команда /cancel (Отмена операций)
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    role = get_user_role(user_id)

    await update.message.reply_text("Текущая операция отменена.", reply_markup=main_menu_keyboard(role))
    logger.info(f"Пользователь {user_id} отменил текущую операцию.")
    return ConversationHandler.END

# Команда /add_student (Добавить одного студента)
@role_required(['admin', 'main_admin'])
async def add_student(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    await update.message.reply_text(
        "Введите имя студента (Фамилия Имя Отчество):",
        reply_markup=ReplyKeyboardMarkup([["Отмена"]], resize_keyboard=True)
    )
    logger.info(f"Пользователь {user_id} начал добавление студента.")
    return ADDING_STUDENT

# Обработка имени одного студента
@role_required(['admin', 'main_admin'])
async def add_student_name(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    name = update.message.text.strip()
    if name.lower() == "отмена":
        await cancel(update, context)
        return

    if not name:
        await update.message.reply_text("Имя не может быть пустым. Попробуйте снова:")
        logger.warning(f"Пользователь {user_id} ввёл пустое имя студента.")
        return ADDING_STUDENT
    try:
        cursor.execute("INSERT INTO students (name) VALUES (?)", (name,))
        conn.commit()
        await update.message.reply_text(
            f"Студент '{format_name(name)}' добавлен.",
            reply_markup=main_menu_keyboard(role=get_user_role(user_id))
        )
        logger.info(f"Студент '{format_name(name)}' добавлен пользователем {user_id}.")
    except sqlite3.IntegrityError:
        await update.message.reply_text(
            f"Студент '{format_name(name)}' уже существует.",
            reply_markup=main_menu_keyboard(role=get_user_role(user_id))
        )
        logger.warning(f"Пользователь {user_id} попытался добавить уже существующего студента '{name}'.")
    return ConversationHandler.END

# Команда /add_students (Массовое добавление студентов)
@role_required(['admin', 'main_admin'])
async def add_students(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    await update.message.reply_text(
        "Введите список студентов, каждый на новой строке (Фамилия Имя Отчество):",
        reply_markup=ReplyKeyboardMarkup([["Отмена"]], resize_keyboard=True)
    )
    logger.info(f"Пользователь {user_id} начал массовое добавление студентов.")
    return ADDING_STUDENTS_LIST

# Обработка списка студентов
@role_required(['admin', 'main_admin'])
async def add_students_list(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    text = update.message.text.strip()
    if text.lower() == "отмена":
        await cancel(update, context)
        return

    names = text.split('\n')
    added = []
    skipped = []
    for name in names:
        formatted_name = format_name(name)
        if name:
            try:
                cursor.execute("INSERT INTO students (name) VALUES (?)", (name,))
                conn.commit()
                added.append(formatted_name)
                logger.info(f"Студент '{formatted_name}' добавлен пользователем {user_id}.")
            except sqlite3.IntegrityError:
                skipped.append(formatted_name)
                logger.warning(f"Пользователь {user_id} попытался добавить уже существующего студента '{formatted_name}'.")
    response = ""
    if added:
        response += "Добавлены следующие студенты:\n" + "\n".join(added) + "\n\n"
    if skipped:
        response += "Следующие студенты уже существуют и были пропущены:\n" + "\n".join(skipped)
    await update.message.reply_text(response, reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    return ConversationHandler.END

# Команда /delete_student
@role_required(['admin', 'main_admin'])
async def delete_student(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    cursor.execute("SELECT id, name FROM students ORDER BY name")
    students = cursor.fetchall()
    if not students:
        await update.message.reply_text("Список студентов пуст.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Пользователь {user_id} попытался удалить студента, но список пуст.")
        return ConversationHandler.END

    keyboard = []
    for student in students:
        formatted_name = format_name(student[1])
        keyboard.append([InlineKeyboardButton(formatted_name, callback_data=f"delete_{student[0]}")])

    reply_markup = InlineKeyboardMarkup(keyboard)
    await update.message.reply_text("Выберите студента для удаления:", reply_markup=reply_markup)
    logger.info(f"Пользователь {user_id} выбрал операцию удаления студента.")
    return DELETING_STUDENT

# Обработка удаления студента
@role_required(['admin', 'main_admin'])
async def handle_delete_student(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    data = query.data.split("_")
    if len(data) != 2 or data[0] != "delete":
        await query.edit_message_text("Некорректные данные.")
        logger.error(f"Пользователь {user_id} прислал некорректные данные для удаления студента: {query.data}")
        return ConversationHandler.END

    try:
        student_id = int(data[1])
    except ValueError:
        await query.edit_message_text("Некорректный ID студента.")
        logger.error(f"Пользователь {user_id} прислал некорректный ID студента: {data[1]}")
        return ConversationHandler.END

    student = cursor.execute("SELECT name FROM students WHERE id = ?", (student_id,)).fetchone()
    if student:
        cursor.execute("DELETE FROM students WHERE id = ?", (student_id,))
        conn.commit()
        await query.edit_message_text(f"Студент '{format_name(student[0])}' удалён.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Студент '{student[0]}' удалён пользователем {user_id}.")
    else:
        await query.edit_message_text("Студент не найден.")
        logger.warning(f"Пользователь {user_id} попытался удалить несуществующего студента с ID {student_id}.")

    return ConversationHandler.END

# Команда /add_admin
@role_required(['main_admin'])
async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    await update.message.reply_text(
        "Введите Telegram ID пользователя и роль, разделенные пробелом (например: `123456789 admin`):",
        reply_markup=ReplyKeyboardMarkup([["Отмена"]], resize_keyboard=True)
    )
    logger.info(f"Пользователь {user_id} начал добавление нового администратора.")
    return ADDING_ADMIN

# Обработка добавления администратора
@role_required(['main_admin'])
async def add_admin_id(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    text = update.message.text.strip()
    if text.lower() == "отмена":
        await cancel(update, context)
        return

    parts = text.split()
    if len(parts) != 2:
        await update.message.reply_text("Пожалуйста, введите ID пользователя и роль, например: `123456789 admin`.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        return ADDING_ADMIN

    try:
        new_user_id = int(parts[0])
        role = parts[1]
        if role not in ['main_admin', 'admin', 'junior_admin']:
            await update.message.reply_text("Недопустимая роль. Доступны: main_admin, admin, junior_admin.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
            return ADDING_ADMIN

        cursor.execute("INSERT INTO admins (user_id, role) VALUES (?, ?)", (new_user_id, role))
        conn.commit()
        await update.message.reply_text(f"Пользователь с ID {new_user_id} добавлен как {role}.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Пользователь {new_user_id} добавлен как {role} пользователем {user_id}.")
    except ValueError:
        await update.message.reply_text("Некорректный ID. Пожалуйста, введите числовой Telegram ID и роль.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.error(f"Пользователь {user_id} ввёл некорректный ID администратора для добавления: {text}")
    except sqlite3.IntegrityError:
        await update.message.reply_text(f"Пользователь с ID {new_user_id} уже является администратором.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.warning(f"Пользователь {user_id} попытался добавить уже существующего администратора с ID {new_user_id}.")
    return ConversationHandler.END

# Команда /remove_admin
@role_required(['main_admin'])
async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    cursor.execute("SELECT user_id, role FROM admins WHERE role != 'main_admin' ORDER BY user_id")
    admins = cursor.fetchall()
    if not admins:
        await update.message.reply_text("Нет администраторов для удаления.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Пользователь {user_id} попытался удалить администратора, но список пуст.")
        return ConversationHandler.END

    keyboard = []
    for admin in admins:
        if admin[0] != INITIAL_ADMIN_USER_ID:  # Исключаем начального администратора
            keyboard.append([InlineKeyboardButton(f"ID: {admin[0]} - {admin[1]}", callback_data=f"remove_admin_{admin[0]}")])

    reply_markup = InlineKeyboardMarkup(keyboard)
    await update.message.reply_text("Выберите администратора для удаления:", reply_markup=reply_markup)
    logger.info(f"Пользователь {user_id} выбрал операцию удаления администратора.")
    return REMOVING_ADMIN

# Обработка удаления администратора (CallbackQueryHandler)
@role_required(['main_admin'])
async def handle_remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    data = query.data.split("_")
    if len(data) != 3 or data[0] != "remove" or data[1] != "admin":
        await query.edit_message_text("Некорректные данные.")
        logger.error(f"Пользователь {user_id} прислал некорректные данные для удаления администратора: {query.data}")
        return ConversationHandler.END

    try:
        admin_id = int(data[2])
    except ValueError:
        await query.edit_message_text("Некорректный ID администратора.")
        logger.error(f"Пользователь {user_id} прислал некорректный ID администратора: {data[2]}")
        return ConversationHandler.END

    if admin_id == INITIAL_ADMIN_USER_ID:
        await query.edit_message_text("Нельзя удалить начального администратора.")
        logger.warning(f"Пользователь {user_id} попытался удалить начального администратора с ID {admin_id}.")
        return ConversationHandler.END

    cursor.execute("DELETE FROM admins WHERE user_id = ?", (admin_id,))
    conn.commit()

    if cursor.rowcount > 0:
        await query.edit_message_text(f"Пользователь с ID {admin_id} удалён из администраторов.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Пользователь с ID {admin_id} удалён из администраторов пользователем {user_id}.")
    else:
        await query.edit_message_text(f"Пользователь с ID {admin_id} не найден среди администраторов.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.warning(f"Пользователь {user_id} попытался удалить администратора с несуществующим ID {admin_id}.")

    return ConversationHandler.END

# Команда /mark_attendance
@role_required(['admin', 'main_admin', 'junior_admin'])
async def mark_attendance(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info(f"Пользователь {user_id} начал отметку посещаемости для даты {today}.")

    # Проверка, была ли уже отмечена посещаемость сегодня для всех студентов
    cursor.execute("SELECT COUNT(DISTINCT student_id) FROM attendance WHERE date = ?", (today,))
    existing = cursor.fetchone()[0]
    cursor.execute("SELECT COUNT(*) FROM students")
    total_students = cursor.fetchone()[0]
    if existing >= total_students:
        await update.message.reply_text(
            "Посещаемость на сегодня уже была отмечена. Если хотите отметить заново, выполните /reset_attendance.",
            reply_markup=main_menu_keyboard(role=get_user_role(user_id))
        )
        logger.info(f"Пользователь {user_id} попытался отметить посещаемость, но она уже была отмечена.")
        return

    cursor.execute("SELECT id, name FROM students ORDER BY name")
    students = cursor.fetchall()

    if not students:
        await update.message.reply_text(
            "Список студентов пуст. Добавьте студентов с помощью ➕ Добавить студента или ➕➕ Добавить нескольких студентов.",
            reply_markup=main_menu_keyboard(role=get_user_role(user_id))
        )
        logger.info(f"Пользователь {user_id} попытался отметить посещаемость, но список студентов пуст.")
        return

    # Отправляем сообщение с кнопками для каждого студента
    for student in students:
        formatted_name = format_name(student[1])
        buttons = [
            InlineKeyboardButton("Присутствовал (П)", callback_data=f"mark_P_{student[0]}"),
            InlineKeyboardButton("Не был (Н)", callback_data=f"mark_N_{student[0]}"),
            InlineKeyboardButton("Опоздал (О)", callback_data=f"mark_O_{student[0]}")
        ]
        reply_markup = InlineKeyboardMarkup([buttons])
        await update.message.reply_text(f"Отметьте статус для {formatted_name}:", reply_markup=reply_markup)
        logger.info(f"Пользователю {user_id} отправлена форма отметки для студента {formatted_name}.")

    # Добавление кнопки "Завершить отметку"
    finish_button = InlineKeyboardButton("Завершить отметку", callback_data="finish_mark_attendance")
    finish_markup = InlineKeyboardMarkup([[finish_button]])

    await update.message.reply_text("Когда закончите отмечать, нажмите 'Завершить отметку'.", reply_markup=finish_markup)
    logger.info(f"Пользователю {user_id} отправлена кнопка для завершения отметки посещаемости.")

# Обработка отметки "P", "N" или "O"
@role_required(['admin', 'main_admin', 'junior_admin'])
async def handle_attendance_mark(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    data = query.data.split("_")
    if len(data) != 3:
        await query.edit_message_text("Некорректные данные.")
        logger.error(f"Пользователь {user_id} прислал некорректные данные для отметки посещаемости: {query.data}")
        return

    action, status, student_id = data
    if action != "mark" or status not in ["P", "N", "O"]:
        await query.edit_message_text("Некорректные данные.")
        logger.error(f"Пользователь {user_id} использовал некорректный статус для отметки: {status}")
        return

    try:
        student_id = int(student_id)
    except ValueError:
        await query.edit_message_text("Некорректный ID студента.")
        logger.error(f"Пользователь {user_id} прислал некорректный ID студента: {student_id}")
        return

    # Проверка наличия уже отметки для этого студента сегодня
    today = datetime.now().strftime("%Y-%m-%d")
    exists = cursor.execute("SELECT * FROM attendance WHERE date = ? AND student_id = ?", (today, student_id)).fetchone()
    if exists:
        await query.edit_message_text("Статус уже был установлен.")
        logger.warning(f"Пользователь {user_id} попытался повторно отметить посещаемость для студента ID {student_id}.")
        return

    # Вставка отметки "P", "N" или "O"
    try:
        cursor.execute("INSERT INTO attendance (date, student_id, status) VALUES (?, ?, ?)", (today, student_id, status))
        conn.commit()
    except Exception as e:
        await query.edit_message_text("Произошла ошибка при сохранении отметки.")
        logger.error(f"Ошибка при вставке отметки посещаемости для студента ID {student_id}: {e}")
        return

    student = cursor.execute("SELECT name FROM students WHERE id = ?", (student_id,)).fetchone()
    student_name = format_name(student[0]) if student else "Неизвестный студент"

    status_text = {
        "P": "Присутствовал",
        "N": "Не был",
        "O": "Опоздал"
    }.get(status, "Неизвестный статус")

    await query.edit_message_text(f"Статус для {student_name} установлен как '{status_text}'.")
    logger.info(f"Пользователь {user_id} отметил студента {student_name} как '{status_text}'.")

# Обработка нажатия "Завершить отметку"
@role_required(['admin', 'main_admin', 'junior_admin'])
async def finish_mark_attendance(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    today = datetime.now().strftime("%Y-%m-%d")
    logger.info(f"Пользователь {user_id} завершил отметку посещаемости для даты {today}.")

    # Получение всех студентов
    cursor.execute("SELECT id, name FROM students")
    all_students = cursor.fetchall()
    all_student_ids = {s[0] for s in all_students}

    # Получение студентов, кто были отмечены как "P", "N", "O", "D"
    cursor.execute("SELECT student_id FROM attendance WHERE date = ?", (today,))
    marked_students = {row[0] for row in cursor.fetchall()}

    # Определение студентов, которым нужно назначить "P"
    present_students = all_student_ids - marked_students

    # Назначение "P" (Присутствовал) для остальных
    try:
        for student_id in present_students:
            cursor.execute("INSERT INTO attendance (date, student_id, status) VALUES (?, ?, ?)", (today, student_id, "P"))
        conn.commit()
    except Exception as e:
        await query.edit_message_text("Произошла ошибка при назначении статуса 'Присутствовал (П)'.")
        logger.error(f"Ошибка при назначении статуса 'P' для студентов: {e}")
        return

    await query.edit_message_text("Отметка посещаемости завершена. Всем не отмеченным присвоен статус 'Присутствовал (П)'.")
    logger.info(f"Статус 'P' назначен всем не отмеченным студентам для даты {today}.")

    # Отправка нового сообщения с главным меню
    await query.message.reply_text(
        "Выберите следующее действие:",
        reply_markup=main_menu_keyboard(role=get_user_role(user_id))
    )
    logger.info(f"Пользователь {user_id} вернулся в главное меню после завершения отметки посещаемости.")

# Команда /assign_duty
@role_required(['admin', 'main_admin'])
async def assign_duty(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info(f"Пользователь {user_id} начал назначение дежурств для даты {today}.")

    # Проверка, были ли уже назначены дежурства на сегодня
    cursor.execute("SELECT student_id FROM attendance WHERE date = ? AND status = 'D'", (today,))
    existing_duties = cursor.fetchall()
    if existing_duties:
        await update.message.reply_text(
            "Дежурства на сегодня уже были назначены. Если хотите назначить заново, выполните /reset_duty.",
            reply_markup=main_menu_keyboard(role=get_user_role(user_id))
        )
        logger.info(f"Дежурства на сегодня уже были назначены. Пользователь {user_id} не может назначить заново.")
        return

    # Получение студентов, которым можно назначить дежурство (не "N")
    cursor.execute("""
        SELECT s.id, s.name, s.duty_count
        FROM students s
        LEFT JOIN attendance a ON s.id = a.student_id AND a.date = ?
        WHERE a.status != 'N' OR a.status IS NULL
        ORDER BY s.duty_count ASC, s.name ASC
    """, (today,))
    eligible_students = cursor.fetchall()

    if len(eligible_students) < 2:
        await update.message.reply_text(
            "Недостаточно студентов для назначения дежурств. Нужно минимум 2 присутствующих (или опоздавших) студента.",
            reply_markup=main_menu_keyboard(role=get_user_role(user_id))
        )
        logger.info(f"Недостаточно студентов для назначения дежурств. Пользователь {user_id} не может продолжить.")
        return

    # Определяем минимальное количество дежурств среди eligible
    min_duty = eligible_students[0][2]
    # Находим всех с минимальным duty_count
    min_duty_students = [s for s in eligible_students if s[2] == min_duty]

    if len(min_duty_students) >= 2:
        # Если достаточно студентов с минимальным duty_count, выбираем из них
        selected = random.sample(min_duty_students, 2)
    else:
        # Если недостаточно, выбираем всех с минимальными и дополняем из следующих
        selected = min_duty_students.copy()
        additional_needed = 2 - len(selected)
        remaining_students = [s for s in eligible_students if s[2] > min_duty]
        if len(remaining_students) < additional_needed:
            await update.message.reply_text(
                "Недостаточно студентов с минимальным количеством дежурств для назначения. Попробуйте позже.",
                reply_markup=main_menu_keyboard(role=get_user_role(user_id))
            )
            logger.info(f"Недостаточно студентов с минимальным количеством дежурств для назначения пользователем {user_id}.")
            return
        selected += random.sample(remaining_students, additional_needed)

    duty_names = [format_name(s[1]) for s in selected]
    logger.info(f"Назначены дежурные: {', '.join(duty_names)} пользователем {user_id}.")

    # Обновляем счетчик дежурств и добавляем запись в attendance со статусом "D" (дежурный)
    try:
        for s in selected:
            cursor.execute("UPDATE students SET duty_count = duty_count + 1 WHERE id = ?", (s[0],))
            cursor.execute("INSERT INTO attendance (date, student_id, status) VALUES (?, ?, ?)", (today, s[0], "D"))
        conn.commit()
    except Exception as e:
        await update.message.reply_text("Произошла ошибка при назначении дежурств.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.error(f"Ошибка при назначении дежурств: {e}")
        return

    # Сохранение текущих дежурств в context для дальнейших операций
    context.user_data['current_duty'] = selected

    # Создание кнопок для изменения дежурств
    buttons = [
        InlineKeyboardButton("Сменить дежурного A", callback_data="swap_A"),
        InlineKeyboardButton("Сменить дежурного B", callback_data="swap_B"),
        InlineKeyboardButton("Сменить обоих дежурных", callback_data="swap_both")
    ]
    reply_markup = InlineKeyboardMarkup([buttons])

    # Отправка сообщения с кнопками
    duty_message = await update.message.reply_text(
        f"Назначенные дежурные: {duty_names[0]} и {duty_names[1]}.",
        reply_markup=reply_markup
    )
    context.user_data['duty_message_id'] = duty_message.message_id
    logger.info(f"Сообщение о назначении дежурств отправлено пользователю {user_id}.")

# Обработка кнопок после назначения дежурств
@role_required(['admin', 'main_admin'])
async def handle_duty_buttons(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    action = query.data
    today = datetime.now().strftime("%Y-%m-%d")
    duty = context.user_data.get('current_duty')

    if not duty:
        await query.edit_message_text("Нет текущих назначений дежурств для изменения.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.warning(f"Пользователь {user_id} пытался изменить дежурства, но текущих дежурств нет.")
        return

    if action == "swap_A" or action == "swap_B" or action == "swap_both":
        if action == "swap_A":
            swap_indices = [0]
            description = "сменить дежурного A"
        elif action == "swap_B":
            swap_indices = [1]
            description = "сменить дежурного B"
        elif action == "swap_both":
            swap_indices = [0, 1]
            description = "сменить обоих дежурных"

        # Получение студентов, которым можно назначить дежурство
        excluded_ids = [s[0] for s in duty]
        placeholders = ','.join(['?'] * len(excluded_ids)) if excluded_ids else ''
        if excluded_ids:
            query_sql = f"""
                SELECT s.id, s.name, s.duty_count
                FROM students s
                LEFT JOIN attendance a ON s.id = a.student_id AND a.date = ?
                WHERE (a.status != 'N' OR a.status IS NULL)
                AND s.id NOT IN ({placeholders}) 
                ORDER BY s.duty_count ASC, s.name ASC
            """
            params = [today] + excluded_ids
        else:
            query_sql = """
                SELECT s.id, s.name, s.duty_count
                FROM students s
                LEFT JOIN attendance a ON s.id = a.student_id AND a.date = ?
                WHERE (a.status != 'N' OR a.status IS NULL)
                ORDER BY s.duty_count ASC, s.name ASC
            """
            params = [today]

        cursor.execute(query_sql, params)
        eligible_students = cursor.fetchall()

        if not eligible_students:
            await query.edit_message_text("Нет доступных студентов для замены.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
            logger.info(f"Нет доступных студентов для замены дежурных пользователем {user_id}.")
            return

        # Определяем минимальное количество дежурств среди eligible
        min_duty = eligible_students[0][2]
        min_duty_students = [s for s in eligible_students if s[2] == min_duty]

        if len(min_duty_students) >= len(swap_indices):
            # Если достаточно студентов с минимальным duty_count, выбираем из них
            selected_new = random.sample(min_duty_students, len(swap_indices))
        else:
            # Если недостаточно, выбираем всех с минимальными и дополняем из следующих
            selected_new = min_duty_students.copy()
            additional_needed = len(swap_indices) - len(selected_new)
            remaining_students = [s for s in eligible_students if s[2] > min_duty]
            if len(remaining_students) < additional_needed:
                await query.edit_message_text("Недостаточно студентов с минимальным количеством дежурств для замены.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
                logger.info(f"Недостаточно студентов с минимальным количеством дежурств для замены пользователем {user_id}.")
                return

            selected_new += random.sample(remaining_students, additional_needed)

        # Обновляем duty_count и attendance
        try:
            for idx, new_student in zip(swap_indices, selected_new):
                # Удаляем предыдущего дежурного из attendance
                previous_student_id = duty[idx][0]
                cursor.execute("DELETE FROM attendance WHERE date = ? AND student_id = ?", (today, previous_student_id))
                # Обновляем duty_count для предыдущего дежурного
                cursor.execute("UPDATE students SET duty_count = duty_count - 1 WHERE id = ?", (previous_student_id,))
                # Назначаем нового дежурного
                cursor.execute("UPDATE students SET duty_count = duty_count + 1 WHERE id = ?", (new_student[0],))
                cursor.execute("INSERT INTO attendance (date, student_id, status) VALUES (?, ?, ?)", (today, new_student[0], "D"))

                # Обновляем текущие дежурства в context
                duty[idx] = new_student
            conn.commit()
        except Exception as e:
            await query.edit_message_text("Произошла ошибка при смене дежурных.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
            logger.error(f"Ошибка при смене дежурных: {e}")
            return

        duty_names = [format_name(s[1]) for s in duty]
        logger.info(f"Дежурные успешно {description}: {', '.join(duty_names)} пользователем {user_id}.")

        # Обновляем сообщение с новыми дежурными
        buttons = [
            InlineKeyboardButton("Сменить дежурного A", callback_data="swap_A"),
            InlineKeyboardButton("Сменить дежурного B", callback_data="swap_B"),
            InlineKeyboardButton("Сменить обоих дежурных", callback_data="swap_both")
        ]
        reply_markup = InlineKeyboardMarkup([buttons])

        await query.edit_message_text(
            f"Дежурные успешно {description}.\nНовые дежурные: {duty_names[0]} и {duty_names[1]}.",
            reply_markup=reply_markup
        )
        logger.info(f"Сообщение обновлено с новыми дежурными пользователем {user_id}.")

# Команда /stats
@role_required(['admin', 'main_admin'])
async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    cursor.execute("SELECT name, duty_count FROM students ORDER BY duty_count, name")
    students = cursor.fetchall()

    if not students:
        await update.message.reply_text("Список студентов пуст.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Пользователь {user_id} запросил статистику, но список студентов пуст.")
        return

    message = "Статистика дежурств:\n"
    for s in students:
        message += f"{format_name(s[0])}: {s[1]} раз(а)\n"

    await update.message.reply_text(message, reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info(f"Пользователь {user_id} получил статистику дежурств.")

# Команда /reset_attendance
@role_required(['admin', 'main_admin'])
async def reset_attendance(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info(f"Пользователь {user_id} начал сброс отметки посещаемости для даты {today}.")

    try:
        cursor.execute("DELETE FROM attendance WHERE date = ?", (today,))
        conn.commit()
        await update.message.reply_text("Отметка посещаемости на сегодня сброшена.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Отметка посещаемости для даты {today} успешно сброшена пользователем {user_id}.")
    except Exception as e:
        await update.message.reply_text("Произошла ошибка при сбросе отметки посещаемости.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.error(f"Ошибка при сбросе отметки посещаемости для даты {today}: {e}")

# Команда /reset_duty
@role_required(['admin', 'main_admin'])
async def reset_duty(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info(f"Пользователь {user_id} начал сброс дежурств для даты {today}.")

    try:
        # Получение текущих дежурств
        cursor.execute("SELECT student_id FROM attendance WHERE date = ? AND status = 'D'", (today,))
        current_duties = cursor.fetchall()
        if not current_duties:
            await update.message.reply_text("Дежурства на сегодня не назначены.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
            logger.info(f"Пользователь {user_id} попытался сбросить дежурства, но они не назначены.")
            return

        for duty in current_duties:
            student_id = duty[0]
            cursor.execute("DELETE FROM attendance WHERE date = ? AND student_id = ?", (today, student_id))
            cursor.execute("UPDATE students SET duty_count = duty_count - 1 WHERE id = ?", (student_id,))
        conn.commit()

        await update.message.reply_text("Дежурства на сегодня сброшены.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Дежурства на сегодня для даты {today} успешно сброшены пользователем {user_id}.")
    except Exception as e:
        await update.message.reply_text("Произошла ошибка при сбросе дежурств.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.error(f"Ошибка при сбросе дежурств для даты {today}: {e}")

# Команда /report
@role_required(['admin', 'main_admin'])
async def report(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    await update.message.reply_text(
        "Выберите тип отчета:",
        reply_markup=InlineKeyboardMarkup([
            [InlineKeyboardButton("Ежедневный", callback_data="report_daily")],
            [InlineKeyboardButton("Недельный", callback_data="report_weekly")],
            [InlineKeyboardButton("Месячный", callback_data="report_monthly")],
            [InlineKeyboardButton("Отмена", callback_data="report_cancel")]
        ])
    )
    logger.info(f"Пользователь {user_id} начал генерацию отчетов.")
    return SELECT_REPORT_TYPE

# Обработка выбора типа отчета (CallbackQueryHandler)
@role_required(['admin', 'main_admin'])
async def handle_report_selection_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    data = query.data
    role = get_user_role(user_id)

    if data == "report_cancel":
        await query.edit_message_text("Генерация отчетов отменена.", reply_markup=main_menu_keyboard(role=role))
        logger.info(f"Пользователь {user_id} отменил генерацию отчета.")
        return ConversationHandler.END

    report_type = data.split("_")[1]  # daily, weekly, monthly
    logger.info(f"Пользователь {user_id} выбрал тип отчета: {report_type}.")

    # Генерация отчета в зависимости от типа
    if report_type == "daily":
        await generate_daily_report(query, context, user_id)
    elif report_type == "weekly":
        await generate_weekly_report(query, context, user_id)
    elif report_type == "monthly":
        await generate_monthly_report(query, context, user_id)
    else:
        await query.edit_message_text("Некорректный тип отчета.", reply_markup=main_menu_keyboard(role=role))
        logger.error(f"Пользователь {user_id} выбрал некорректный тип отчета: {report_type}.")

    return ConversationHandler.END

# Генерация ежедневного отчета
@role_required(['admin', 'main_admin'])
async def generate_daily_report(query, context, user_id):
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info(f"Пользователь {user_id} запросил ежедневный отчет для даты {today}.")

    cursor.execute("""
        SELECT s.name, a.status, COUNT(*) as count
        FROM attendance a
        JOIN students s ON a.student_id = s.id
        WHERE a.date = ?
        GROUP BY s.name, a.status
    """, (today,))

    data = cursor.fetchall()

    if not data:
        await query.edit_message_text("Нет данных для ежедневного отчета.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для ежедневного отчета.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Студент", "Статус", "Количество"])

    # Создание графика
    fig, ax = plt.subplots(figsize=(10,6))
    for status in df['Статус'].unique():
        subset = df[df['Статус'] == status]
        ax.bar(subset['Студент'], subset['Количество'], label=status)

    ax.set_title(f'Ежедневный отчет по посещаемости ({today})')
    ax.set_xlabel('Студенты')
    ax.set_ylabel('Количество')
    ax.legend()

    # Поворот подписей для лучшей читаемости
    plt.xticks(rotation=45, ha='right')

    # Сохранение графика в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка графика как изображение
    await query.edit_message_text(f"Ежедневный отчет по посещаемости ({today}):")
    await context.bot.send_photo(
        chat_id=user_id,
        photo=bio,
        caption="Ежедневный отчет по посещаемости."
    )
    await context.bot.send_message(
        chat_id=user_id,
        text="📈 Ежедневный отчет готов.",
        reply_markup=main_menu_keyboard(role=get_user_role(user_id))
    )
    logger.info("Ежедневный отчет отправлен.")

# Генерация недельного отчета
@role_required(['admin', 'main_admin'])
async def generate_weekly_report(query, context, user_id):
    today = datetime.now()
    start_of_week = today - timedelta(days=today.weekday())  # Понедельник
    end_of_week = start_of_week + timedelta(days=6)  # Воскресенье

    logger.info(f"Пользователь {user_id} запросил недельный отчет с {start_of_week.strftime('%Y-%m-%d')} по {end_of_week.strftime('%Y-%m-%d')}.")

    cursor.execute("""
        SELECT s.name, a.status, COUNT(*) as count
        FROM attendance a
        JOIN students s ON a.student_id = s.id
        WHERE a.date BETWEEN ? AND ?
        GROUP BY s.name, a.status
    """, (start_of_week.strftime("%Y-%m-%d"), end_of_week.strftime("%Y-%m-%d")))

    data = cursor.fetchall()

    if not data:
        await query.edit_message_text("Нет данных для недельного отчета.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для недельного отчета.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Студент", "Статус", "Количество"])

    # Создание графика
    fig, ax = plt.subplots(figsize=(10,6))
    for status in df['Статус'].unique():
        subset = df[df['Статус'] == status]
        ax.bar(subset['Студент'], subset['Количество'], label=status)

    ax.set_title(f'Недельный отчет по посещаемости ({start_of_week.strftime("%d-%m-%Y")} - {end_of_week.strftime("%d-%m-%Y")})')
    ax.set_xlabel('Студенты')
    ax.set_ylabel('Количество')
    ax.legend()

    # Поворот подписей для лучшей читаемости
    plt.xticks(rotation=45, ha='right')

    # Сохранение графика в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка графика как изображение
    await query.edit_message_text(f"Недельный отчет по посещаемости с {start_of_week.strftime('%d-%m-%Y')} по {end_of_week.strftime('%d-%m-%Y')}:")
    await context.bot.send_photo(
        chat_id=user_id,
        photo=bio,
        caption="Недельный отчет по посещаемости."
    )
    await context.bot.send_message(
        chat_id=user_id,
        text="📈 Недельный отчет готов.",
        reply_markup=main_menu_keyboard(role=get_user_role(user_id))
    )
    logger.info("Недельный отчет отправлен.")

# Генерация месячного отчета
@role_required(['admin', 'main_admin'])
async def generate_monthly_report(query, context, user_id):
    today = datetime.now()
    start_of_month = today.replace(day=1)
    next_month = start_of_month + timedelta(days=32)
    start_of_next_month = next_month.replace(day=1)
    end_of_month = start_of_next_month - timedelta(days=1)

    logger.info(f"Пользователь {user_id} запросил месячный отчет с {start_of_month.strftime('%Y-%m-%d')} по {end_of_month.strftime('%Y-%m-%d')}.")

    cursor.execute("""
        SELECT s.name, a.status, COUNT(*) as count
        FROM attendance a
        JOIN students s ON a.student_id = s.id
        WHERE a.date BETWEEN ? AND ?
        GROUP BY s.name, a.status
    """, (start_of_month.strftime("%Y-%m-%d"), end_of_month.strftime("%Y-%m-%d")))

    data = cursor.fetchall()

    if not data:
        await query.edit_message_text("Нет данных для месячного отчета.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для месячного отчета.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Студент", "Статус", "Количество"])

    # Создание графика
    fig, ax = plt.subplots(figsize=(12,7))
    pivot_df = df.pivot(index='Студент', columns='Статус', values='Количество').fillna(0)
    pivot_df.plot(kind='bar', ax=ax)
    ax.set_title(f'Месячный отчет по посещаемости ({start_of_month.strftime("%B %Y")})')
    ax.set_xlabel('Студенты')
    ax.set_ylabel('Количество')
    plt.xticks(rotation=45, ha='right')
    plt.legend(title='Статус')

    # Сохранение графика в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка графика как изображение
    await query.edit_message_text(f"Месячный отчет по посещаемости ({start_of_month.strftime('%B %Y')}):")
    await context.bot.send_photo(
        chat_id=user_id,
        photo=bio,
        caption="Месячный отчет по посещаемости."
    )
    await context.bot.send_message(
        chat_id=user_id,
        text="📈 Месячный отчет готов.",
        reply_markup=main_menu_keyboard(role=get_user_role(user_id))
    )
    logger.info("Месячный отчет отправлен.")

# Обработка Callback Queries для выбора типа отчета
@role_required(['admin', 'main_admin'])
async def handle_report_selection_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    data = query.data
    role = get_user_role(user_id)

    if data == "report_cancel":
        await query.edit_message_text("Генерация отчетов отменена.", reply_markup=main_menu_keyboard(role=role))
        logger.info(f"Пользователь {user_id} отменил генерацию отчета.")
        return ConversationHandler.END

    report_type = data.split("_")[1]  # daily, weekly, monthly
    logger.info(f"Пользователь {user_id} выбрал тип отчета: {report_type}.")

    # Генерация отчета в зависимости от типа
    if report_type == "daily":
        await generate_daily_report(query, context, user_id)
    elif report_type == "weekly":
        await generate_weekly_report(query, context, user_id)
    elif report_type == "monthly":
        await generate_monthly_report(query, context, user_id)
    else:
        await query.edit_message_text("Некорректный тип отчета.", reply_markup=main_menu_keyboard(role=role))
        logger.error(f"Пользователь {user_id} выбрал некорректный тип отчета: {report_type}.")

    return ConversationHandler.END

# Команда /duty_history
@role_required(['admin', 'main_admin'])
async def duty_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    cursor.execute("SELECT id, name FROM students ORDER BY name")
    students = cursor.fetchall()

    if not students:
        await update.message.reply_text("Список студентов пуст.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info(f"Пользователь {user_id} запросил историю дежурств, но список студентов пуст.")
        return

    # Создание инлайн клавиатуры с выбором студента
    keyboard = []
    for student in students:
        formatted_name = format_name(student[1])
        keyboard.append([InlineKeyboardButton(formatted_name, callback_data=f"duty_history_{student[0]}")])

    reply_markup = InlineKeyboardMarkup(keyboard)
    await update.message.reply_text("Выберите студента для просмотра истории дежурств:", reply_markup=reply_markup)
    logger.info("Инлайн клавиатура для выбора студента отправлена.")
    return SELECT_DUTY_HISTORY_STUDENT

# Обработка запроса истории дежурств
@role_required(['admin', 'main_admin'])
async def handle_duty_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    data = query.data.split("_")
    if len(data) != 3 or data[0] != "duty" or data[1] != "history":
        await query.edit_message_text("Некорректные данные.")
        logger.error(f"Пользователь {user_id} прислал некорректные данные для истории дежурств: {query.data}")
        return ConversationHandler.END

    try:
        student_id = int(data[2])
    except ValueError:
        await query.edit_message_text("Некорректный ID студента.")
        logger.error(f"Пользователь {user_id} прислал некорректный ID студента для истории дежурств: {student_id}")
        return ConversationHandler.END

    # Получение имени студента
    student = cursor.execute("SELECT name FROM students WHERE id = ?", (student_id,)).fetchone()
    if not student:
        await query.edit_message_text("Студент не найден.")
        logger.warning(f"Студент с ID {student_id} не найден для истории дежурств.")
        return ConversationHandler.END

    student_name = format_name(student[0])

    # Получение истории дежурств
    cursor.execute("""
        SELECT date 
        FROM attendance 
        WHERE student_id = ? AND status = 'D'
        ORDER BY date DESC
    """, (student_id,))
    history = cursor.fetchall()

    if not history:
        await query.edit_message_text(f"История дежурств для {student_name} пуста.")
        logger.info(f"История дежурств для {student_name} пуста.")
        return ConversationHandler.END

    history_dates = [row[0] for row in history]
    history_text = f"История дежурств для {student_name}:\n" + "\n".join(history_dates)

    await query.edit_message_text(history_text)
    logger.info(f"История дежурств для {student_name} отправлена.")
    return ConversationHandler.END

# Визуализация данных: Тренды опозданий и отсутствий
@role_required(['admin', 'main_admin'])
async def late_absence_trends(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"Пользователь {user_id} запросил тренды опозданий и отсутствий.")

    cursor.execute("""
        SELECT date, status
        FROM attendance
        WHERE status IN ('N', 'O')
    """)
    data = cursor.fetchall()

    if not data:
        await update.message.reply_text("Нет данных для трендов опозданий и отсутствий.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для трендов опозданий и отсутствий.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Дата", "Статус"])
    df['Дата'] = pd.to_datetime(df['Дата'])
    df.set_index('Дата', inplace=True)
    df['Статус'] = df['Статус'].map({'N': 'Не было', 'O': 'Опоздал'})

    # Группировка по дате и статусу
    trend = df.groupby([pd.Grouper(freq='D'), 'Статус']).size().unstack(fill_value=0)

    # Создание графика
    fig, ax = plt.subplots(figsize=(12,7))
    trend.plot(kind='line', ax=ax)
    ax.set_title('Тренды опозданий и отсутствий')
    ax.set_xlabel('Дата')
    ax.set_ylabel('Количество')
    ax.legend(title='Статус')
    plt.xticks(rotation=45)

    # Сохранение графика в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка графика как изображение
    await update.message.reply_photo(photo=bio, caption="Тренды опозданий и отсутствий.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info("График трендов опозданий и отсутствий отправлен.")

# Визуализация данных: Распределение дежурств
@role_required(['admin', 'main_admin'])
async def duty_distribution(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"Пользователь {user_id} запросил распределение дежурств.")

    cursor.execute("""
        SELECT name, duty_count 
        FROM students
        ORDER BY duty_count DESC
    """)
    data = cursor.fetchall()

    if not data:
        await update.message.reply_text("Нет данных для распределения дежурств.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для распределения дежурств.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Студент", "Количество дежурств"])

    # Создание диаграммы распределения
    fig, ax = plt.subplots(figsize=(10,6))
    ax.pie(df['Количество дежурств'], labels=df['Студент'], autopct='%1.1f%%', startangle=140)
    ax.set_title('Распределение дежурств среди студентов')

    # Сохранение диаграммы в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка диаграммы как изображение
    await update.message.reply_photo(photo=bio, caption="Распределение дежурств среди студентов.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info("Диаграмма распределения дежурств отправлена.")

# Обработка Callback Queries для callback_data `duty_history_<id>`
@role_required(['admin', 'main_admin'])
async def handle_duty_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    data = query.data.split("_")
    if len(data) != 3 or data[0] != "duty" or data[1] != "history":
        await query.edit_message_text("Некорректные данные.")
        logger.error(f"Пользователь {user_id} прислал некорректные данные для истории дежурств: {query.data}")
        return ConversationHandler.END

    try:
        student_id = int(data[2])
    except ValueError:
        await query.edit_message_text("Некорректный ID студента.")
        logger.error(f"Пользователь {user_id} прислал некорректный ID студента для истории дежурств: {student_id}")
        return ConversationHandler.END

    # Получение имени студента
    student = cursor.execute("SELECT name FROM students WHERE id = ?", (student_id,)).fetchone()
    if not student:
        await query.edit_message_text("Студент не найден.")
        logger.warning(f"Студент с ID {student_id} не найден для истории дежурств.")
        return ConversationHandler.END

    student_name = format_name(student[0])

    # Получение истории дежурств
    cursor.execute("""
        SELECT date 
        FROM attendance 
        WHERE student_id = ? AND status = 'D'
        ORDER BY date DESC
    """, (student_id,))
    history = cursor.fetchall()

    if not history:
        await query.edit_message_text(f"История дежурств для {student_name} пуста.")
        logger.info(f"История дежурств для {student_name} пуста.")
        return ConversationHandler.END

    history_dates = [row[0] for row in history]
    history_text = f"История дежурств для {student_name}:\n" + "\n".join(history_dates)

    await query.edit_message_text(history_text)
    logger.info(f"История дежурств для {student_name} отправлена.")
    return ConversationHandler.END

# Визуализация данных: Тренды посещаемости
@role_required(['admin', 'main_admin'])
async def attendance_trends(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"Пользователь {user_id} запросил тренды посещаемости.")
    
    cursor.execute("""
        SELECT strftime('%w', date) as weekday, status, COUNT(*) as count
        FROM attendance
        GROUP BY weekday, status
    """)
    
    data = cursor.fetchall()

    if not data:
        await update.message.reply_text("Нет данных для трендов посещаемости.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для трендов посещаемости.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Weekday", "Status", "Count"])
    df['Weekday'] = df['Weekday'].astype(int)
    df['Weekday'] = df['Weekday'].apply(lambda x: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'][x])

    # Создание графика
    fig, ax = plt.subplots(figsize=(10,6))
    for status in df['Status'].unique():
        subset = df[df['Status'] == status]
        ax.plot(subset['Weekday'], subset['Count'], marker='o', label=status)
    
    ax.set_title('Тренды посещаемости по дням недели')
    ax.set_xlabel('День недели')
    ax.set_ylabel('Количество')
    ax.legend()

    # Сохранение графика в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка графика как изображение
    await update.message.reply_photo(photo=bio, caption="Тренды посещаемости по дням недели.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info("График трендов посещаемости отправлен.")

# Визуализация данных: Распределение дежурств
@role_required(['admin', 'main_admin'])
async def duty_distribution(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"Пользователь {user_id} запросил распределение дежурств.")

    cursor.execute("""
        SELECT name, duty_count 
        FROM students
        ORDER BY duty_count DESC
    """)
    data = cursor.fetchall()

    if not data:
        await update.message.reply_text("Нет данных для распределения дежурств.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для распределения дежурств.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Студент", "Количество дежурств"])

    # Создание диаграммы распределения
    fig, ax = plt.subplots(figsize=(10,6))
    ax.pie(df['Количество дежурств'], labels=df['Студент'], autopct='%1.1f%%', startangle=140)
    ax.set_title('Распределение дежурств среди студентов')

    # Сохранение диаграммы в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка диаграммы как изображение
    await update.message.reply_photo(photo=bio, caption="Распределение дежурств среди студентов.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info("Диаграмма распределения дежурств отправлена.")

# Визуализация данных: Тренды опозданий и отсутствий
@role_required(['admin', 'main_admin'])
async def late_absence_trends(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"Пользователь {user_id} запросил тренды опозданий и отсутствий.")

    cursor.execute("""
        SELECT date, status
        FROM attendance
        WHERE status IN ('N', 'O')
    """)
    data = cursor.fetchall()

    if not data:
        await update.message.reply_text("Нет данных для трендов опозданий и отсутствий.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для трендов опозданий и отсутствий.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Дата", "Статус"])
    df['Дата'] = pd.to_datetime(df['Дата'])
    df.set_index('Дата', inplace=True)
    df['Статус'] = df['Статус'].map({'N': 'Не было', 'O': 'Опоздал'})

    # Группировка по дате и статусу
    trend = df.groupby([pd.Grouper(freq='D'), 'Статус']).size().unstack(fill_value=0)

    # Создание графика
    fig, ax = plt.subplots(figsize=(12,7))
    trend.plot(kind='line', ax=ax)
    ax.set_title('Тренды опозданий и отсутствий')
    ax.set_xlabel('Дата')
    ax.set_ylabel('Количество')
    ax.legend(title='Статус')
    plt.xticks(rotation=45)

    # Сохранение графика в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка графика как изображение
    await update.message.reply_photo(photo=bio, caption="Тренды опозданий и отсутствий.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info("График трендов опозданий и отсутствий отправлен.")

# Обработка Callback Queries для callback_data `duty_history_<id>`
@role_required(['admin', 'main_admin'])
async def handle_duty_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    user_id = query.from_user.id
    await query.answer()

    data = query.data.split("_")
    if len(data) != 3 or data[0] != "duty" or data[1] != "history":
        await query.edit_message_text("Некорректные данные.")
        logger.error(f"Пользователь {user_id} прислал некорректные данные для истории дежурств: {query.data}")
        return ConversationHandler.END

    try:
        student_id = int(data[2])
    except ValueError:
        await query.edit_message_text("Некорректный ID студента.")
        logger.error(f"Пользователь {user_id} прислал некорректный ID студента для истории дежурств: {student_id}")
        return ConversationHandler.END

    # Получение имени студента
    student = cursor.execute("SELECT name FROM students WHERE id = ?", (student_id,)).fetchone()
    if not student:
        await query.edit_message_text("Студент не найден.")
        logger.warning(f"Студент с ID {student_id} не найден для истории дежурств.")
        return ConversationHandler.END

    student_name = format_name(student[0])

    # Получение истории дежурств
    cursor.execute("""
        SELECT date 
        FROM attendance 
        WHERE student_id = ? AND status = 'D'
        ORDER BY date DESC
    """, (student_id,))
    history = cursor.fetchall()

    if not history:
        await query.edit_message_text(f"История дежурств для {student_name} пуста.")
        logger.info(f"История дежурств для {student_name} пуста.")
        return ConversationHandler.END

    history_dates = [row[0] for row in history]
    history_text = f"История дежурств для {student_name}:\n" + "\n".join(history_dates)

    await query.edit_message_text(history_text)
    logger.info(f"История дежурств для {student_name} отправлена.")
    return ConversationHandler.END

# Визуализация данных: Тренды посещаемости
@role_required(['admin', 'main_admin'])
async def attendance_trends(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"Пользователь {user_id} запросил тренды посещаемости.")
    
    cursor.execute("""
        SELECT strftime('%w', date) as weekday, status, COUNT(*) as count
        FROM attendance
        GROUP BY weekday, status
    """)
    
    data = cursor.fetchall()

    if not data:
        await update.message.reply_text("Нет данных для трендов посещаемости.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для трендов посещаемости.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Weekday", "Status", "Count"])
    df['Weekday'] = df['Weekday'].astype(int)
    df['Weekday'] = df['Weekday'].apply(lambda x: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'][x])

    # Создание графика
    fig, ax = plt.subplots(figsize=(10,6))
    for status in df['Status'].unique():
        subset = df[df['Status'] == status]
        ax.plot(subset['Weekday'], subset['Count'], marker='o', label=status)
    
    ax.set_title('Тренды посещаемости по дням недели')
    ax.set_xlabel('День недели')
    ax.set_ylabel('Количество')
    ax.legend()

    # Сохранение графика в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка графика как изображение
    await update.message.reply_photo(photo=bio, caption="Тренды посещаемости по дням недели.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info("График трендов посещаемости отправлен.")

# Визуализация данных: Распределение дежурств
@role_required(['admin', 'main_admin'])
async def duty_distribution(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"Пользователь {user_id} запросил распределение дежурств.")

    cursor.execute("""
        SELECT name, duty_count 
        FROM students
        ORDER BY duty_count DESC
    """)
    data = cursor.fetchall()

    if not data:
        await update.message.reply_text("Нет данных для распределения дежурств.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для распределения дежурств.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Студент", "Количество дежурств"])

    # Создание диаграммы распределения
    fig, ax = plt.subplots(figsize=(10,6))
    ax.pie(df['Количество дежурств'], labels=df['Студент'], autopct='%1.1f%%', startangle=140)
    ax.set_title('Распределение дежурств среди студентов')

    # Сохранение диаграммы в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка диаграммы как изображение
    await update.message.reply_photo(photo=bio, caption="Распределение дежурств среди студентов.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info("Диаграмма распределения дежурств отправлена.")

# Визуализация данных: Тренды опозданий и отсутствий
@role_required(['admin', 'main_admin'])
async def late_absence_trends(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"Пользователь {user_id} запросил тренды опозданий и отсутствий.")

    cursor.execute("""
        SELECT date, status
        FROM attendance
        WHERE status IN ('N', 'O')
    """)
    data = cursor.fetchall()

    if not data:
        await update.message.reply_text("Нет данных для трендов опозданий и отсутствий.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
        logger.info("Нет данных для трендов опозданий и отсутствий.")
        return

    # Преобразование данных в DataFrame
    df = pd.DataFrame(data, columns=["Дата", "Статус"])
    df['Дата'] = pd.to_datetime(df['Дата'])
    df.set_index('Дата', inplace=True)
    df['Статус'] = df['Статус'].map({'N': 'Не было', 'O': 'Опоздал'})

    # Группировка по дате и статусу
    trend = df.groupby([pd.Grouper(freq='D'), 'Статус']).size().unstack(fill_value=0)

    # Создание графика
    fig, ax = plt.subplots(figsize=(12,7))
    trend.plot(kind='line', ax=ax)
    ax.set_title('Тренды опозданий и отсутствий')
    ax.set_xlabel('Дата')
    ax.set_ylabel('Количество')
    ax.legend(title='Статус')
    plt.xticks(rotation=45)

    # Сохранение графика в байтовый поток
    bio = BytesIO()
    plt.tight_layout()
    plt.savefig(bio, format='png')
    bio.seek(0)
    plt.close(fig)

    # Отправка графика как изображение
    await update.message.reply_photo(photo=bio, caption="Тренды опозданий и отсутствий.", reply_markup=main_menu_keyboard(role=get_user_role(user_id)))
    logger.info("График трендов опозданий и отсутствий отправлен.")

# Кнопка для визуализации данных: Тренды посещаемости
application.add_handler(MessageHandler(filters.Regex("^📈 Тренды посещаемости$"), attendance_trends))
# Кнопка для раздела распределения дежурств
application.add_handler(MessageHandler(filters.Regex("^📊 Распределение дежурств$"), duty_distribution))
# Кнопка для трендов опозданий и отсутствий
application.add_handler(MessageHandler(filters.Regex("^📉 Тренды опозданий и отсутствий$"), late_absence_trends))

# Обработка Callback Queries для выбора типа отчета и истории дежурств
application.add_handler(CallbackQueryHandler(handle_attendance_mark, pattern=r"^mark_[PNO]_\d+$"))
application.add_handler(CallbackQueryHandler(finish_mark_attendance, pattern=r"^finish_mark_attendance$"))
application.add_handler(CallbackQueryHandler(handle_duty_buttons, pattern=r"^(swap_A|swap_B|swap_both)$"))
application.add_handler(CallbackQueryHandler(handle_duty_history, pattern=r"^duty_history_\d+$"))
application.add_handler(CallbackQueryHandler(handle_report_selection_callback, pattern=r"^report_(daily|weekly|monthly|cancel)$"))
application.add_handler(CallbackQueryHandler(handle_remove_admin, pattern=r"^remove_admin_\d+$"))

# Command handlers
conversation_handlers = [
    ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^➕ Добавить студента$"), add_student)],
        states={
            ADDING_STUDENT: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_student_name)]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    ),
    ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^➕➕ Добавить нескольких студентов$"), add_students)],
        states={
            ADDING_STUDENTS_LIST: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_students_list)]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    ),
    ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^❌ Удалить студента$"), delete_student)],
        states={
            DELETING_STUDENT: [CallbackQueryHandler(handle_delete_student, pattern=r"^delete_\d+$")]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    ),
    ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^🔐 Добавить администратора$"), add_admin)],
        states={
            ADDING_ADMIN: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_admin_id)]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    ),
    ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^🔒 Удалить администратора$"), remove_admin)],
        states={
            REMOVING_ADMIN: [CallbackQueryHandler(handle_remove_admin, pattern=r"^remove_admin_\d+$")]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    ),
    ConversationHandler(
        entry_points=[CommandHandler("report", report)],
        states={
            SELECT_REPORT_TYPE: [CallbackQueryHandler(handle_report_selection_callback, pattern=r"^report_(daily|weekly|monthly|cancel)$")]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    ),
    ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^📜 История дежурств$"), duty_history_handler)],
        states={
            SELECT_DUTY_HISTORY_STUDENT: [CallbackQueryHandler(handle_duty_history, pattern=r"^duty_history_\d+$")]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    ),
]

for conv in conversation_handlers:
    application.add_handler(conv)

# Запланированные задания
def schedule_jobs(application):
    scheduler.add_job(
        send_daily_reminder,
        'cron',
        hour=8,
        minute=0,
        args=[application],
        timezone='Europe/Moscow'  # Установите ваш часовой пояс
    )
    scheduler.add_job(
        send_weekly_summary,
        'cron',
        day_of_week='fri',
        hour=18,
        minute=0,
        args=[application],
        timezone='Europe/Moscow'
    )
    scheduler.add_job(
        auto_reassign_duty_wrapper,
        'cron',
        hour=20,
        minute=0,  # Время после завершения отметки посещаемости
        args=[application],
        timezone='Europe/Moscow'
    )
    scheduler.start()
    logger.info("Планировщик заданий запущен с ежедневными, еженедельными задачами и авто переназначением дежурств.")

# Функция-обертка для переназначения дежурств
async def auto_reassign_duty_wrapper(application: ApplicationBuilder):
    await auto_reassign_duty(application)

# Обработка автоматического переназначения дежурств
async def auto_reassign_duty(application):
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info("Автоматическое переназначение дежурств запускается.")

    # Получение дежурств на сегодня
    cursor.execute("""
        SELECT student_id 
        FROM attendance 
        WHERE date = ? AND status = 'D'
    """, (today,))
    current_duties = cursor.fetchall()

    if not current_duties:
        logger.info("Дежурства на сегодня не назначены. Пропуск переназначения.")
        return

    unavailable = []
    for duty in current_duties:
        student_id = duty[0]
        # Определение недоступности: студент помечен как "N" или отсутствует
        cursor.execute("""
            SELECT status 
            FROM attendance 
            WHERE date = ? AND student_id = ?
        """, (today, student_id))
        status = cursor.fetchone()
        if not status or status[0] not in ['P', 'O', 'D']:
            unavailable.append(student_id)

    if not unavailable:
        logger.info("Все дежурные доступны. Переназначение не требуется.")
        return

    for student_id in unavailable:
        # Поиск нового дежурного
        cursor.execute("""
            SELECT s.id, s.name, s.duty_count
            FROM students s
            LEFT JOIN attendance a ON s.id = a.student_id AND a.date = ?
            WHERE (a.status != 'N' OR a.status IS NULL)
            AND s.id NOT IN (SELECT student_id FROM attendance WHERE date = ? AND status = 'D')
            ORDER BY s.duty_count ASC, s.name ASC
            LIMIT 1
        """, (today, today))
        new_duty = cursor.fetchone()

        if new_duty:
            new_student_id = new_duty[0]
            cursor.execute("UPDATE students SET duty_count = duty_count + 1 WHERE id = ?", (new_student_id,))
            cursor.execute("INSERT INTO attendance (date, student_id, status) VALUES (?, ?, ?)", (today, new_student_id, 'D'))
            cursor.execute("DELETE FROM attendance WHERE date = ? AND student_id = ?", (today, student_id))
            conn.commit()
            # Предполагаем, что student_id соответствует chat_id (это может отличаться в вашей реализации)
            try:
                await application.bot.send_message(
                    chat_id=new_student_id,  # Убедитесь, что chat_id и student_id совпадают
                    text="🔄 Ваше дежурство было автоматически переназначено."
                )
                logger.info(f"Дежурство переназначено на студента ID {new_student_id}.")
            except Exception as e:
                logger.error(f"Ошибка при отправке уведомления студенту {new_student_id}: {e}")
        else:
            logger.warning(f"Не удалось найти замену для студента ID {student_id}.")

# Отправка ежедневных напоминаний
async def send_daily_reminder(application):
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info(f"Отправка ежедневного напоминания администраторам для даты {today}.")
    cursor.execute("SELECT user_id FROM admins")
    admins = cursor.fetchall()
    if not admins:
        logger.warning("Нет администраторов для отправки напоминаний.")
        return

    for admin in admins:
        try:
            await application.bot.send_message(
                chat_id=admin[0],
                text="🔔 Напоминание: Пожалуйста, отметьте посещаемость за сегодня.",
                reply_markup=main_menu_keyboard(role=get_user_role(admin[0]))
            )
            logger.info(f"Напоминание отправлено администратору с ID {admin[0]}.")
        except Exception as e:
            logger.error(f"Ошибка при отправке напоминания админу {admin[0]}: {e}")

# Отправка еженедельных сводок
async def send_weekly_summary(application):
    today = datetime.now()
    start_of_week = today - timedelta(days=today.weekday())  # Понедельник
    end_of_week = start_of_week + timedelta(days=6)  # Воскресенье
    logger.info(f"Отправка еженедельной сводки администраторам за период {start_of_week.strftime('%Y-%m-%d')} - {end_of_week.strftime('%Y-%m-%d')}.")

    cursor.execute("""
        SELECT s.name, a.status, COUNT(*) as count
        FROM attendance a
        JOIN students s ON a.student_id = s.id
        WHERE a.date BETWEEN ? AND ?
        GROUP BY s.name, a.status
    """, (start_of_week.strftime("%Y-%m-%d"), end_of_week.strftime("%Y-%m-%d")))

    data = cursor.fetchall()
    if not data:
        summary = "Еженедельная сводка:\nНет данных за прошлую неделю."
    else:
        df = pd.DataFrame(data, columns=["Студент", "Статус", "Количество"])
        summary = f"Еженедельная сводка с {start_of_week.strftime('%d-%m-%Y')} по {end_of_week.strftime('%d-%m-%Y')}:\n"
        for student in df['Студент'].unique():
            student_data = df[df['Студент'] == student]
            statuses = ", ".join([f"{row['Статус']}: {row['Количество']}" for index, row in student_data.iterrows()])
            summary += f"{student}: {statuses}\n"

    cursor.execute("SELECT user_id FROM admins")
    admins = cursor.fetchall()
    if not admins:
        logger.warning("Нет администраторов для отправки еженедельных сводок.")
        return

    for admin in admins:
        try:
            await application.bot.send_message(
                chat_id=admin[0],
                text=summary,
                reply_markup=main_menu_keyboard(role=get_user_role(admin[0]))
            )
            logger.info(f"Еженедельная сводка отправлена администратору с ID {admin[0]}.")
        except Exception as e:
            logger.error(f"Ошибка при отправке еженедельной сводки админу {admin[0]}: {e}")

# Обработка автоматического переназначения дежурств
async def auto_reassign_duty_wrapper(application: ApplicationBuilder):
    await auto_reassign_duty(application)

# Основная функция
def main():
    application = ApplicationBuilder().token(BOT_TOKEN).build()

    # Определение ConversationHandlers
    conv_handler_add = ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^➕ Добавить студента$"), add_student)],
        states={
            ADDING_STUDENT: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_student_name)]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    )

    conv_handler_add_multiple = ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^➕➕ Добавить нескольких студентов$"), add_students)],
        states={
            ADDING_STUDENTS_LIST: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_students_list)]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    )

    conv_handler_delete = ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^❌ Удалить студента$"), delete_student)],
        states={
            DELETING_STUDENT: [CallbackQueryHandler(handle_delete_student, pattern=r"^delete_\d+$")]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    )

    conv_handler_add_admin = ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^🔐 Добавить администратора$"), add_admin)],
        states={
            ADDING_ADMIN: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_admin_id)]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    )

    conv_handler_remove_admin = ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^🔒 Удалить администратора$"), remove_admin)],
        states={
            REMOVING_ADMIN: [CallbackQueryHandler(handle_remove_admin, pattern=r"^remove_admin_\d+$")]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    )

    conv_handler_report = ConversationHandler(
        entry_points=[CommandHandler("report", report)],
        states={
            SELECT_REPORT_TYPE: [CallbackQueryHandler(handle_report_selection_callback, pattern=r"^report_(daily|weekly|monthly|cancel)$")]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    )

    conv_handler_duty_history = ConversationHandler(
        entry_points=[MessageHandler(filters.Regex("^📜 История дежурств$"), duty_history)],
        states={
            SELECT_DUTY_HISTORY_STUDENT: [CallbackQueryHandler(handle_duty_history, pattern=r"^duty_history_\d+$")]
        },
        fallbacks=[CommandHandler('cancel', cancel)]
    )

    conversation_handlers = [
        conv_handler_add,
        conv_handler_add_multiple,
        conv_handler_delete,
        conv_handler_add_admin,
        conv_handler_remove_admin,
        conv_handler_report,
        conv_handler_duty_history
    ]

    for conv in conversation_handlers:
        application.add_handler(conv)

    # Обработчики кнопок символов
    application.add_handler(MessageHandler(filters.Regex("^📈 Тренды посещаемости$"), attendance_trends))
    application.add_handler(MessageHandler(filters.Regex("^📊 Распределение дежурств$"), duty_distribution))
    application.add_handler(MessageHandler(filters.Regex("^📉 Тренды опозданий и отсутствий$"), late_absence_trends))

    # Обработчики callback queries
    application.add_handler(CallbackQueryHandler(handle_attendance_mark, pattern=r"^mark_[PNO]_\d+$"))
    application.add_handler(CallbackQueryHandler(finish_mark_attendance, pattern=r"^finish_mark_attendance$"))
    application.add_handler(CallbackQueryHandler(handle_duty_buttons, pattern=r"^(swap_A|swap_B|swap_both)$"))
    application.add_handler(CallbackQueryHandler(handle_duty_history, pattern=r"^duty_history_\d+$"))
    application.add_handler(CallbackQueryHandler(handle_report_selection_callback, pattern=r"^report_(daily|weekly|monthly|cancel)$"))
    application.add_handler(CallbackQueryHandler(handle_remove_admin, pattern=r"^remove_admin_\d+$"))

    # Обработчик неизвестных сообщений
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, unknown))

    # Запланированные задания
    schedule_jobs(application)

    # Запуск бота
    logger.info("Бот запускается...")
    application.run_polling()

# Функции для отправки напоминаний и сводок
async def send_daily_reminder(application):
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info(f"Отправка ежедневного напоминания администраторам для даты {today}.")
    cursor.execute("SELECT user_id FROM admins")
    admins = cursor.fetchall()
    if not admins:
        logger.warning("Нет администраторов для отправки напоминаний.")
        return

    for admin in admins:
        try:
            await application.bot.send_message(
                chat_id=admin[0],
                text="🔔 Напоминание: Пожалуйста, отметьте посещаемость за сегодня.",
                reply_markup=main_menu_keyboard(role=get_user_role(admin[0]))
            )
            logger.info(f"Напоминание отправлено администратору с ID {admin[0]}.")
        except Exception as e:
            logger.error(f"Ошибка при отправке напоминания админу {admin[0]}: {e}")

async def send_weekly_summary(application):
    today = datetime.now()
    start_of_week = today - timedelta(days=today.weekday())  # Понедельник
    end_of_week = start_of_week + timedelta(days=6)  # Воскресенье
    logger.info(f"Отправка еженедельной сводки администраторам за период {start_of_week.strftime('%Y-%m-%d')} - {end_of_week.strftime('%Y-%m-%d')}.")

    cursor.execute("""
        SELECT s.name, a.status, COUNT(*) as count
        FROM attendance a
        JOIN students s ON a.student_id = s.id
        WHERE a.date BETWEEN ? AND ?
        GROUP BY s.name, a.status
    """, (start_of_week.strftime("%Y-%m-%d"), end_of_week.strftime("%Y-%m-%d")))

    data = cursor.fetchall()
    if not data:
        summary = "Еженедельная сводка:\nНет данных за прошлую неделю."
    else:
        df = pd.DataFrame(data, columns=["Студент", "Статус", "Количество"])
        summary = f"Еженедельная сводка с {start_of_week.strftime('%d-%m-%Y')} по {end_of_week.strftime('%d-%m-%Y')}:\n"
        for student in df['Студент'].unique():
            student_data = df[df['Студент'] == student]
            statuses = ", ".join([f"{row['Статус']}: {row['Количество']}" for index, row in student_data.iterrows()])
            summary += f"{student}: {statuses}\n"

    cursor.execute("SELECT user_id FROM admins")
    admins = cursor.fetchall()
    if not admins:
        logger.warning("Нет администраторов для отправки еженедельных сводок.")
        return

    for admin in admins:
        try:
            await application.bot.send_message(
                chat_id=admin[0],
                text=summary,
                reply_markup=main_menu_keyboard(role=get_user_role(admin[0]))
            )
            logger.info(f"Еженедельная сводка отправлена администратору с ID {admin[0]}.")
        except Exception as e:
            logger.error(f"Ошибка при отправке еженедельной сводки админу {admin[0]}: {e}")

# Обработка автоматического переназначения дежурств
async def auto_reassign_duty(application):
    today = datetime.now().strftime("%Y-%m-%d")
    logger.info("Автоматическое переназначение дежурств запускается.")

    # Получение дежурств на сегодня
    cursor.execute("""
        SELECT student_id 
        FROM attendance 
        WHERE date = ? AND status = 'D'
    """, (today,))
    current_duties = cursor.fetchall()

    if not current_duties:
        logger.info("Дежурства на сегодня не назначены. Пропуск переназначения.")
        return

    unavailable = []
    for duty in current_duties:
        student_id = duty[0]
        # Определение недоступности: студент помечен как "N" или отсутствует
        cursor.execute("""
            SELECT status 
            FROM attendance 
            WHERE date = ? AND student_id = ?
        """, (today, student_id))
        status = cursor.fetchone()
        if not status or status[0] not in ['P', 'O', 'D']:
            unavailable.append(student_id)

    if not unavailable:
        logger.info("Все дежурные доступны. Переназначение не требуется.")
        return

    for student_id in unavailable:
        # Поиск нового дежурного
        cursor.execute("""
            SELECT s.id, s.name, s.duty_count
            FROM students s
            LEFT JOIN attendance a ON s.id = a.student_id AND a.date = ?
            WHERE (a.status != 'N' OR a.status IS NULL)
            AND s.id NOT IN (SELECT student_id FROM attendance WHERE date = ? AND status = 'D')
            ORDER BY s.duty_count ASC, s.name ASC
            LIMIT 1
        """, (today, today))
        new_duty = cursor.fetchone()

        if new_duty:
            new_student_id = new_duty[0]
            cursor.execute("UPDATE students SET duty_count = duty_count + 1 WHERE id = ?", (new_student_id,))
            cursor.execute("INSERT INTO attendance (date, student_id, status) VALUES (?, ?, ?)", (today, new_student_id, 'D'))
            cursor.execute("DELETE FROM attendance WHERE date = ? AND student_id = ?", (today, student_id))
            conn.commit()
            # Предполагаем, что student_id соответствует chat_id (это может отличаться в вашей реализации)
            try:
                await application.bot.send_message(
                    chat_id=new_student_id,  # Убедитесь, что chat_id и student_id совпадают
                    text="🔄 Ваше дежурство было автоматически переназначено."
                )
                logger.info(f"Дежурство переназначено на студента ID {new_student_id}.")
            except Exception as e:
                logger.error(f"Ошибка при отправке уведомления студенту {new_student_id}: {e}")
        else:
            logger.warning(f"Не удалось найти замену для студента ID {student_id}.")

# Обработка Callback Queries для выбора типа отчета и истории дежурств
application.add_handler(CallbackQueryHandler(handle_attendance_mark, pattern=r"^mark_[PNO]_\d+$"))
application.add_handler(CallbackQueryHandler(finish_mark_attendance, pattern=r"^finish_mark_attendance$"))
application.add_handler(CallbackQueryHandler(handle_duty_buttons, pattern=r"^(swap_A|swap_B|swap_both)$"))
application.add_handler(CallbackQueryHandler(handle_duty_history, pattern=r"^duty_history_\d+$"))
application.add_handler(CallbackQueryHandler(handle_report_selection_callback, pattern=r"^report_(daily|weekly|monthly|cancel)$"))
application.add_handler(CallbackQueryHandler(handle_remove_admin, pattern=r"^remove_admin_\d+$"))

# Обработка автопереназначения дежурств через планировщик
def schedule_jobs(application):
    scheduler.add_job(
        send_daily_reminder,
        'cron',
        hour=8,
        minute=0,
        args=[application],
        timezone='Europe/Moscow'  # Установите ваш часовой пояс
    )
    scheduler.add_job(
        send_weekly_summary,
        'cron',
        day_of_week='fri',
        hour=18,
        minute=0,
        args=[application],
        timezone='Europe/Moscow'
    )
    scheduler.add_job(
        auto_reassign_duty_wrapper,
        'cron',
        hour=20,
        minute=0,  # Время после завершения отметки посещаемости
        args=[application],
        timezone='Europe/Moscow'
    )
    scheduler.start()
    logger.info("Планировщик заданий запущен с ежедневными, еженедельными задачами и авто переназначением дежурств.")

# Функция-обертка для переназначения дежурств
async def auto_reassign_duty_wrapper(application: ApplicationBuilder):
    await auto_reassign_duty(application)

# Запуск основной функции при запуске скрипта
if __name__ == '__main__':
    main()   [2024-09-27 21:25:55,227 - __main__ - INFO - Столбец 'role' уже существует в таблице 'admins'.
2024-09-27 21:25:55,228 - tzlocal - DEBUG - Looking up time zone info from registry
2024-09-27 21:25:55,254 - matplotlib.pyplot - DEBUG - Loaded backend tkagg version 8.6.
Backend tkagg is interactive backend. Turning interactive mode on. , application.add_handler(MessageHandler(filters.Regex("^📈 Тренды посещаемости$"), attendance_trends))-name 'application' is not defined , Серьезность	Код	
Предупреждение (активно)	reportMissingModuleSource	Import "pandas" could not be resolved from source		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	9	
Предупреждение (активно)	reportMissingImports	Import "apscheduler.schedulers.asyncio" could not be resolved		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	10	
Предупреждение (активно)	reportMissingImports	Import "dotenv" could not be resolved		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	11	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1622	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1624	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1626	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1629	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1630	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1631	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1632	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1633	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1634	
Предупреждение (активно)	reportUndefinedVariable	"duty_history_handler" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1681	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	1690	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	2085	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	2086	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	2087	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	2088	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	2089	
Предупреждение (активно)	reportUndefinedVariable	"application" is not defined		c:\Users\Admin\source\repos\bot_on_duty\bot_on_duty\bot_on_duty.py	2090	]