import logging
import os
import random
import 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 ConversationHandler.END

    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 ConversationHandler.END

    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 ConversationHandler.END

    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

# Генерация ежедневного отчета
@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("Месячный отчет отправлен.")

# Обработка выбора типа отчета (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

# Команда /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("Диаграмма распределения дежурств отправлена.")

# Визуализация данных: Тренды посещаемости
@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("График трендов посещаемости отправлен.")

# Команда /report создается один раз в ConversationHandler
# Поэтому удаляем повторяющиеся обработчики

# Основная функция
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("^📈 Генерация отчетов$"), report))
    application.add_handler(MessageHandler(filters.Regex("^📜 История дежурств$"), duty_history))
    application.add_handler(MessageHandler(filters.Regex("^📊 Визуализация данных$"), attendance_trends))
    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()

# Запланированные задания
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):
    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}")

# Основной запуск
if __name__ == '__main__':
    main()       (application = ApplicationBuilder().token(BOT_TOKEN).build() -You must pass the token you received from https://t.me/Botfather!