from flask import Flask, render_template, redirect, url_for, flash, request, abort, send_from_directory, render_template_string
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_wtf import FlaskForm
from flask_wtf.csrf import CSRFProtect
from wtforms import StringField, PasswordField, SubmitField, FileField, TextAreaField, IntegerField, BooleanField, SelectField, SelectMultipleField
from wtforms.validators import DataRequired, Length, EqualTo
from werkzeug.utils import secure_filename
from datetime import datetime, timedelta
from flask_migrate import Migrate
from functools import wraps
from flask import send_file
import random
import json
import io
# from PIL import Image, ImageDraw
# import numpy as np
import os
import random
import string
import markdown
import json
import bleach
import pytz
from datetime import datetime
import math
# Инициализация Flask приложения

app = Flask(__name__)
app.config['SECRET_KEY'] = 'killerVovan1995'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'static/audio'
app.config['AVATAR_UPLOAD_FOLDER'] = 'static/avatars'
app.config['UPLOAD_FOLDER'] = os.path.join(app.static_folder, 'uploads')
app.config['EGG_FOLDER'] = os.path.join(app.config['UPLOAD_FOLDER'], 'eggs')
os.makedirs(app.config['EGG_FOLDER'], exist_ok=True)
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
os.environ["NUMEXPR_NUM_THREADS"] = "1"
os.environ["OMP_NUM_THREADS"] = "1"
# Инициализация базы данных
db = SQLAlchemy(app)
migrate = Migrate(app, db)
csrf = CSRFProtect(app)
# Инициализация Flask-Login
login_manager = LoginManager(app)
login_manager.login_view = 'login'
@app.template_filter('clean_achievement_name')
def clean_achievement_name(name):
    return name.split('_', 1)[1] if '_' in name else name
@app.template_filter('datetimeformat')
def datetimeformat(value, format='%d.%m.%Y %H:%M'):
    if value is None:
        return ""
    return value.strftime(format)
def draw_stars(draw, width, height, color, num_stars=15):
    """Рисует звёзды на поверхности яйца"""
    for _ in range(num_stars):
        x = random.randint(50, width-50)
        y = random.randint(50, height-50)

        # Проверяем, что точка внутри яйца
        if ((x - width/2)**2)/((width/2-50)**2) + ((y - height/2)**2)/((height/2-50)**2) <= 1:
            size = random.randint(5, 15)

            # Рисуем звезду как 5 лучей
            outer_radius = size
            inner_radius = size // 2
            points = []

            for i in range(10):
                angle = 2 * math.pi * i / 10
                radius = inner_radius if i % 2 == 0 else outer_radius
                px = x + radius * math.cos(angle)
                py = y + radius * math.sin(angle)
                points.append((px, py))

            draw.polygon(points, fill=color)
def draw_hearts(draw, width, height, color, num_hearts=10):
    """Рисует сердечки на поверхности яйца"""
    for _ in range(num_hearts):
        x = random.randint(50, width-50)
        y = random.randint(50, height-50)

        if ((x - width/2)**2)/((width/2-50)**2) + ((y - height/2)**2)/((height/2-50)**2) <= 1:
            size = random.randint(6, 12)

            # Рисуем сердечко из двух дуг и треугольника
            left = x - size
            top = y - size
            right = x + size
            bottom = y + size

            # Левая дуга
            draw.arc([left, top, x, bottom], 0, 180, fill=color, width=2)
            # Правая дуга
            draw.arc([x, top, right, bottom], 0, 180, fill=color, width=2)

            # Треугольник внизу
            draw.polygon([
                (left, y + size//2),
                (right, y + size//2),
                (x, bottom + size//2)
            ], fill=color)
def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_admin:
            abort(403)  # Доступ запрещен
        return f(*args, **kwargs)
    return decorated_function
# Разрешенные HTML теги и атрибуты
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
    'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br', 'pre', 'code',
    'ul', 'ol', 'li', 'hr', 'img', 'span', 'div'
]
# Зарегистрируем функцию enumerate как глобальную функцию в Jinja2
app.jinja_env.globals.update(enumerate=enumerate)
ALLOWED_ATTRIBUTES = {
    **bleach.sanitizer.ALLOWED_ATTRIBUTES,
    'img': ['src', 'alt', 'title'],
    'a': ['href', 'title', 'rel']
}
import hashlib
def generate_user_code(user_id):
    # Секретный ключ для дополнительной безопасности
    SECRET_KEY = 'killerVovan1995'  # Используем тот же, что и в app.config

    # Создаем хеш на основе ID пользователя и секретного ключа
    hash_input = f"{user_id}{SECRET_KEY}".encode('utf-8')
    hash_digest = hashlib.sha256(hash_input).hexdigest()

    # Преобразуем хеш в 6-значное число
    # Берем первые 6 цифр из числового представления хеша
    numeric_hash = int(hash_digest[:8], 16)  # Берем первые 8 символов для большего числа
    code = numeric_hash % 1000000  # Обеспечиваем 6 цифр

    # Форматируем с ведущими нулями
    return f"{code:06d}"
@app.template_filter('achievement_icon')
def achievement_icon_filter(icon_filename):
    return url_for('static', filename=f'achievements/icons/{icon_filename}')
@app.template_filter('user_code')
def user_code_filter(user_id):
    return generate_user_code(user_id)
@app.template_filter('markdown')
def markdown_filter(text):
    if not text:
        return ""
    html = markdown.markdown(text)
    return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
# Модель пользователя
class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(100), unique=True, nullable=False)
    real_name = db.Column(db.String(100), nullable=False)
    password = db.Column(db.String(100), nullable=False)
    avatar = db.Column(db.String(200))
    improvisation_count = db.Column(db.Integer, default=0)  # Количество импровизаций
    last_improvisation_date = db.Column(db.DateTime)  # Дата последней записи
    is_admin = db.Column(db.Boolean, default=False)  # Администратор
    can_toggle_visibility = db.Column(db.Boolean, default=True)
    improvisations = db.relationship('Improvisation', backref='user', lazy=True)
    current_streak = db.Column(db.Integer, default=0)  # Текущая серия дней
    longest_streak = db.Column(db.Integer, default=0)  # Самая длинная серия
    streak_frozen = db.Column(db.Boolean, default=False)  # Заморожена ли серия
    last_activity_date = db.Column(db.Date)  # Дата последней активности
    selected_egg_id = db.Column(db.Integer, db.ForeignKey('easter_egg.id'))
    selected_egg = db.relationship('EasterEgg', foreign_keys=[selected_egg_id])

class AchievementsSystem:
    @staticmethod
    def add_achievement(user_id, name, description, icon_filename):
        try:
            # Make the name unique by including user_id
            unique_name = f"{user_id}_{name}"

            achievement = Achievement(
                user_id=user_id,
                name=unique_name,
                description=description,
                icon=icon_filename
            )
            print("ok")
            db.session.add(achievement)
            db.session.commit()
            return True
        except Exception as e:
            db.session.rollback()
            return False

    @staticmethod
    def check_solfege_achievement(user_id, score):
        if score >= 8:  # 8 out of 10 correct answers
            return AchievementsSystem.add_achievement(
                user_id,
                'Кролик-сольфеджист',
                f'Пройден квест по сольфеджио с результатом {score}/10',
                'rabbit.png'
            )
        return False
class SolfegeQuiz(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    score = db.Column(db.Integer, nullable=False)
    max_score = db.Column(db.Integer, default=10)
    completed_at = db.Column(db.DateTime, default=datetime.utcnow)

    user = db.relationship('User', backref=db.backref('solfege_quizzes', lazy=True))
class Achievement(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    name = db.Column(db.String(50), nullable=False)  # Removed unique=True
    description = db.Column(db.String(200))
    icon = db.Column(db.String(10))
    date_achieved = db.Column(db.DateTime, default=datetime.utcnow)

    # Composite unique constraint (user_id + name)
    __table_args__ = (
        db.UniqueConstraint('user_id', 'name', name='uix_user_name'),
    )

    user = db.relationship('User', backref=db.backref('achievements', lazy=True))
class ActivityRecord(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    date = db.Column(db.Date, nullable=False, default=datetime.utcnow().date)
    streak_length = db.Column(db.Integer, default=0)  # Добавляем это поле
    duration = db.Column(db.Integer)  # Длительность в секундах
    improvisation_id = db.Column(db.Integer, db.ForeignKey('improvisation.id'))

    user = db.relationship('User', backref='activity_records')
    improvisation = db.relationship('Improvisation', backref='activity_records')
# Модель сообщения
class Message(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    recipient_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    content = db.Column(db.Text, nullable=False)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)
    improvisation_id = db.Column(db.Integer, db.ForeignKey('improvisation.id'))  # Опционально

    sender = db.relationship('User', foreign_keys=[sender_id], backref='sent_messages')
    recipient = db.relationship('User', foreign_keys=[recipient_id], backref='received_messages')

# Модель импровизации
class Improvisation(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    audio_path = db.Column(db.String(200))
    is_public = db.Column(db.Boolean, default=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Форма для записи импровизации
class ImprovisationForm(FlaskForm):
    title = StringField('Название', validators=[DataRequired(), Length(min=2, max=100)])
    submit = SubmitField('Сохранить импровизацию')

# Форма для отправки сообщения
class MessageForm(FlaskForm):
    content = TextAreaField('Сообщение', validators=[DataRequired()])
    improvisation_id = IntegerField('Айди импровизации')  # Опционально
    submit = SubmitField('Послать сообщение')

# Форма регистрации
class RegistrationForm(FlaskForm):
    username = StringField('Имя', validators=[DataRequired(), Length(min=2, max=20)])
    real_name = StringField('Настоящее имя', validators=[DataRequired(), Length(min=2, max=100)])  # Настоящее имя
    password = PasswordField('Пароль', validators=[DataRequired()])
    confirm_password = PasswordField('Повторите пароль', validators=[DataRequired(), EqualTo('password')])
    improvisation_count = IntegerField('Уже записанные импровизации', default=0)  # Количество импровизаций
    submit = SubmitField('Зарегистрироваться!')
class AdminUserForm(FlaskForm):
    is_admin = BooleanField('Администратор')
    submit = SubmitField('Сохранить')
# Форма авторизации
class LoginForm(FlaskForm):
    username = StringField('Имя', validators=[DataRequired(), Length(min=2, max=20)])
    password = PasswordField('Пароль', validators=[DataRequired()])
    submit = SubmitField('Войти')

# Форма изменения профиля
class ProfileForm(FlaskForm):
    username = StringField('Имя', validators=[DataRequired(), Length(min=2, max=20)])
    avatar = FileField('Изменить аватар')
    current_password = PasswordField('Текущий пароль')
    new_password = PasswordField('Новый пароль', validators=[EqualTo('confirm_password', message='Пароли должны совпадать')])
    confirm_password = PasswordField('Повторите новый пароль')
    submit = SubmitField('Обновить профиль')
# Добавим новые модели
class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)
    access_type = db.Column(db.String(20), default='public')  # 'public' или 'private'
    recipients = db.relationship('PostRecipient', backref='post', lazy=True, cascade='all, delete-orphan')

    reactions = db.relationship('Reaction', backref='post', cascade='all, delete-orphan')

    # Добавляем связь с пользователем
    author = db.relationship('User', backref='posts')
class EasterEgg(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    pattern_data = db.Column(db.Text, nullable=False)  # JSON с параметрами паттерна
    image_path = db.Column(db.String(255))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)


class PostRecipient(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
class Reaction(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
    reaction_type = db.Column(db.String(20), nullable=False)

    __table_args__ = (
        db.UniqueConstraint('user_id', 'post_id', name='unique_user_post_reaction'),
    )

class PostForm(FlaskForm):
    content = TextAreaField(
        'Содержание (поддерживается Markdown)',
        validators=[DataRequired()],
        render_kw={"placeholder": "## Заголовок\n**жирный** *курсив*\n[ссылка](https://example.com)"}
    )
    access_type = SelectField(
        'Тип доступа',
        choices=[('public', 'Для всех'), ('private', 'Для выбранных')],
        default='public'
    )
    recipients = SelectMultipleField(
        'Получатели',
        coerce=int,
        choices=[]
    )
    submit = SubmitField('Опубликовать')

    def __init__(self, *args, **kwargs):
        super(PostForm, self).__init__(*args, **kwargs)
        # Заполняем список пользователей для выбора получателей
        self.recipients.choices = [(user.id, user.username) for user in User.query.all()]
REACTION_EMOJI = {
    'like': '👍',
    'dislike': '👎',
    'okay': '👌',
    'fire': '🔥',
    'heart': '❤️',
    'laugh': '😂',
    'strawberry': '🍓',
    'hundred': '💯'
}
def allowed_audio_file(filename):
    ALLOWED_EXTENSIONS = {'wav', 'mp3', 'ogg'}
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def calculate_audio_duration(filepath):
    """Примерная функция для определения длительности аудио"""
    try:
        # Здесь можно использовать библиотеку like mutagen или pydub
        # Для примера - заглушка
        return 60  # Возвращаем 60 секунд по умолчанию
    except:
        return 0

def check_achievements(user):
    milestones = {3: '🥉 Бронза', 5: '🥈 Серебро', 10: '🥇 Золото',
                 30: '💎 Алмаз', 50: '🏆 Чемпион', 100: '🔥 Легенда'}
    achievements = user.achievements or {}

    for milestone, title in milestones.items():
        if user.current_streak >= milestone and str(milestone) not in achievements:
            achievements[str(milestone)] = {
                'title': title,
                'date': datetime.utcnow().isoformat()
            }
            # Можно добавить отправку уведомления или письма
            flash(f'🎖 Поздравляем! Вы получили достижение "{title}" за {milestone} дней серии!', 'success')

    user.achievements = achievements
    user.longest_streak = max(user.longest_streak or 0, user.current_streak)
@app.before_request
def check_maintenance():
    if os.path.exists('maintenance.lock') and request.path != url_for('admin_panel'):
        abort(503)

@app.errorhandler(503)
def maintenance_page(e):
    return render_template('maintenance.html'), 503

@app.template_filter('moscow_time')
def moscow_time_filter(value):
    if not value:
        return ""

    utc_time = value.replace(tzinfo=pytz.UTC)  # Указываем что время в UTC
    moscow_tz = pytz.timezone('Europe/Moscow')
    moscow_time = utc_time.astimezone(moscow_tz)
    return moscow_time.strftime('%d.%m.%Y')
@app.template_filter('reaction_emoji')
def reaction_emoji_filter(reaction_type):
    return REACTION_EMOJI.get(reaction_type, '')
# Добавим маршруты
@app.route('/easter')
def easter_activities():
    return render_template('easter.html')
@app.route('/metronome')
def metronome():
    return render_template('metronome.html')
@app.route('/solfege_quiz', methods=['GET', 'POST'])
@login_required
def solfege_quiz():
    questions = [
    {
        'question': 'Какой вид модуляции происходит через общий аккорд, являющийся диатоническим в обеих тональностях?',
        'options': ['Хроматическая', 'Энгармоническая', 'Диатоническая', 'Родственная'],
        'answer': 'Диатоническая',
        'explanation': 'Диатоническая модуляция использует аккорд, который является диатоническим и в исходной, и в новой тональности. Это самый плавный вид модуляции.'
    },
    {
        'question': 'Какой интервал образуется между III и VI ступенями гармонического мажора?',
        'options': ['Уменьшенная кварта', 'Увеличенная кварта', 'Уменьшенная квинта', 'Чистая кварта'],
        'answer': 'Уменьшенная кварта',
        'explanation': 'В гармоническом мажоре VI ступень понижается, поэтому интервал между III (медиантой) и VI ступенями становится уменьшенной квартой.'
    },
    {
        'question': 'Какое трезвучие строится на VII ступени мелодического минора?',
        'options': ['Увеличенное', 'Уменьшенное', 'Мажорное', 'Минорное'],
        'answer': 'Увеличенное',
        'explanation': 'В мелодическом миноре VII ступень повышается при восходящем движении, что создаёт увеличенное трезвучие на этой ступени.'
    },
    {
        'question': 'Какой септаккорд имеет структуру: б.3 + м.3 + б.3?',
        'options': ['Малый мажорный', 'Малый минорный', 'Уменьшенный', 'Доминантсептаккорд'],
        'answer': 'Малый мажорный',
        'explanation': 'Это структура малого мажорного септаккорда (например, C7). Он состоит из мажорного трезвучия + малой терции (б.3+м.3+б.3).'
    },
    {
        'question': 'Какой вид контрапункта допускает диссонансы на сильной доле?',
        'options': ['Строгий', 'Свободный', 'Подвижной', 'Обратимый'],
        'answer': 'Свободный',
        'explanation': 'В свободном контрапункте допускаются диссонансы на сильных долях, в отличие от строгого стиля, где они разрешаются на слабых долях.'
    },
    {
        'question': 'Какое количество тональностей в квинтовом круге с использованием энгармонизма?',
        'options': ['12', '15', '24', '30'],
        'answer': '30',
        'explanation': 'В квинтовом круге 30 тональностей: 15 мажорных и 15 минорных (с учётом энгармонически равных, например, C#-dur и Db-dur).'
    },
    {
        'question': 'Какой аккорд является характерным для целотонового лада?',
        'options': ['Увеличенное трезвучие', 'Уменьшенный септаккорд', 'Доминантсептаккорд', 'Неаполитанский аккорд'],
        'answer': 'Увеличенное трезвучие',
        'explanation': 'Целотоновый лад состоит только из целых тонов, и увеличенное трезвучие (из двух больших терций) является его характерным аккордом.'
    },
    {
        'question': 'Какой интервал между основным тоном доминантсептаккорда и его септимой?',
        'options': ['Малая септима', 'Большая септима', 'Уменьшенная септима', 'Увеличенная секста'],
        'answer': 'Малая септима',
        'explanation': 'Доминантсептаккорд (например, G7 в C-dur) состоит из мажорного трезвучия и малой септимы (G-F в этом случае).'
    },
    {
        'question': 'Какой вид имитации строго сохраняет интервальную структуру темы?',
        'options': ['Точная', 'Тональная', 'Ритмическая', 'Обратная'],
        'answer': 'Точная',
        'explanation': 'Точная имитация полностью повторяет интервальную с труктуру темы, в отличие от тональной, которая может изменять интервалы для сохранения лада.'
    },
    {
        'question': 'Какой аккорд выполняет функцию S в гармоническом мажоре?',
        'options': ['Трезвучие II ступени', 'Трезвучие IV ступени', 'Неаполитанский аккорд', 'Уменьшенное трезвучие VII ступени'],
        'answer': 'Трезвучие II ступени',
        'explanation': 'В гармоническом мажоре пониженная VI ступень делает трезвучие II ступени уменьшенным, что усиливает его субдоминантовую функцию.'
    }
]

    form = FlaskForm()

    if request.method == 'POST':
        if not form.validate():
            flash('Ошибка валидации формы. Пожалуйста, попробуйте снова.', 'danger')
            return redirect(url_for('solfege_quiz'))

        score = 0
        user_answers = []

        for i, question in enumerate(questions):
            user_answer = request.form.get(f'q{i}')
            user_answers.append({
                'question': question['question'],
                'user_answer': user_answer,
                'correct_answer': question['answer'],
                'explanation': question['explanation'],
                'is_correct': user_answer == question['answer']
            })
            if user_answers[-1]['is_correct']:
                score += 1

        # Сохраняем результат
        try:
            quiz = SolfegeQuiz(user_id=current_user.id, score=score)
            db.session.add(quiz)
            db.session.commit()

            # Проверяем достижение
            if score >= 8:
                AchievementsSystem.add_achievement(
                    current_user.id,
                    'Кролик-сольфеджист',
                    f'Пройден квест по сольфеджио с результатом {score}/10',
                    'rabbit.png'
                )
                flash('🎉 Поздравляем! Вы получили достижение "Кролик-сольфеджист"!', 'success')
            else:
                flash(f'Вы ответили правильно на {score} из {len(questions)} вопросов. Попробуйте ещё раз!', 'info')

            return render_template('quiz_result.html',
                                score=score,
                                max_score=len(questions),
                                questions=questions,
                                user_answers=user_answers)

        except Exception as e:
            db.session.rollback()
            flash('Произошла ошибка при сохранении результатов. Пожалуйста, попробуйте снова.', 'danger')
            app.logger.error(f"Error saving quiz results: {str(e)}")

    return render_template('solfege_quiz.html', questions=questions, form=form)


@app.route('/generate_egg', methods=['GET'])
@login_required
def generate_egg():
    # Генерируем уникальный паттерн
    pattern = {
        'base_color': (random.randint(100, 255), random.randint(100, 255), random.randint(100, 255)),
        'stripes': False,  # Полоски всегда отключены
        'dots': random.choice([True, False]),
        'dots_color': (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)),
        'dots_size': random.randint(2, 8),
        'dots_count': random.randint(10, 100),
        'special_pattern': random.choice(['flowers', 'stars', 'hearts', 'none']),
        'special_color': (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)),
        'seed': random.getrandbits(64)
    }

    # Остальной код остается без изменений
    egg = EasterEgg(
        user_id=current_user.id,
        pattern_data=json.dumps(pattern)
    )
    db.session.add(egg)
    db.session.commit()

    img = generate_egg_image(pattern)
#
    filename = f"egg_{current_user.id}_{egg.id}.png"
    save_path = os.path.join(app.config['UPLOAD_FOLDER'], 'eggs', filename)
    os.makedirs(os.path.dirname(save_path), exist_ok=True)
    img.save(save_path, 'PNG')

    egg.image_path = f'eggs/{filename}'
    db.session.commit()

    return jsonify({
        'success': True,
        'image_url': url_for('static', filename=f'uploads/eggs/{filename}'),
        'egg_id': egg.id
    })

@app.route('/select_egg/<int:egg_id>', methods=['POST'])
@login_required
def select_egg(egg_id):
    try:
        egg = EasterEgg.query.filter_by(id=egg_id, user_id=current_user.id).first_or_404()

        current_user.selected_egg_id = egg_id
        db.session.commit()

        # Явно возвращаем JSON-ответ
        return jsonify({
            'success': True,
            'message': 'Яйцо успешно выбрано!',
            'egg_id': egg_id,
            'image_url': "/static/uploads/"+egg.image_path
        })

    except Exception as e:
        return jsonify({
            'success': False,
            'error': str(e)
        }), 400
@app.route('/kulich_game')
@login_required
def kulich_game():
    return render_template('kulich_game.html')
@app.route('/invite')
def invite_user():
    return render_template('invite.html')

# Маршрут для сохранения достижения
@app.route('/earn_achievement', methods=['POST'])
@login_required
def earn_achievement():
    achievement_name = request.json.get('name')

    # Проверяем, есть ли уже такое достижение
    existing = Achievement.query.filter_by(
        user_id=current_user.id,
        name=achievement_name
    ).first()

    if not existing:
        # Если это достижение за кулич, формируем название "1_кулич" (где 1 — user_id)
        if achievement_name == "Мастер освящения":
            achievement_name = f"{current_user.id}_Кулич"
            description = "Успешно освятил кулич, сыграв мелодию"
            icon = "kulich_master.png"
        else:
            description = "Достижение получено"
            icon = "default_achievement.png"

        achievement = Achievement(
            user_id=current_user.id,
            name=achievement_name,  # Например: "1_кулич"
            description=description,
            icon=icon
        )
        db.session.add(achievement)
        db.session.commit()

        # Отправляем уведомление
        flash(f'🎉 Получено достижение: {achievement_name.split("_")[1]}!', 'success')
        return jsonify({'success': True})

    return jsonify({'success': False, 'message': 'Достижение уже получено'})
@app.route('/egg_image/<int:egg_id>')
def egg_image(egg_id):
    egg = EasterEgg.query.get_or_404(egg_id)
    return send_file(os.path.join(app.config['UPLOAD_FOLDER'], egg.image_path))

def generate_egg_image(pattern):
    width, height = 300, 400
    img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
    draw = ImageDraw.Draw(img)

    # Основное яйцо
    draw.ellipse([(50, 50), (width-50, height-50)], fill=pattern['base_color'])

    # Убрали блок с полосками (stripes)

    # Точки
    if pattern['dots']:
        for _ in range(pattern['dots_count']):
            x = random.randint(50, width-50)
            y = random.randint(50, height-50)
            if ((x - width/2)**2)/((width/2-50)**2) + ((y - height/2)**2)/((height/2-50)**2) <= 1:
                radius = pattern['dots_size']
                draw.ellipse([(x-radius, y-radius), (x+radius, y+radius)],
                            fill=pattern['dots_color'])

    # Специальный паттерн
    if pattern['special_pattern'] == 'flowers':
        draw_flowers(draw, width, height, pattern['special_color'])
    elif pattern['special_pattern'] == 'stars':
        draw_stars(draw, width, height, pattern['special_color'])
    elif pattern['special_pattern'] == 'hearts':
        draw_hearts(draw, width, height, pattern['special_color'])

    return img

def draw_flowers(draw, width, height, color):
    # Упрощенная реализация цветочков
    for _ in range(15):
        x = random.randint(50, width-50)
        y = random.randint(50, height-50)
        if ((x - width/2)**2)/((width/2-50)**2) + ((y - height/2)**2)/((height/2-50)**2) <= 1:
            size = random.randint(5, 15)
            draw.ellipse([(x-size, y-size), (x+size, y+size)], fill=color)
            for angle in range(0, 360, 45):
                dx = int(size * 1.5 * np.cos(np.radians(angle)))
                dy = int(size * 1.5 * np.sin(np.radians(angle)))
                draw.ellipse([(x+dx-size/2, y+dy-size/2), (x+dx+size/2, y+dy+size/2)], fill=color)

# Аналогично реализуйте draw_stars и draw_hearts
@app.route('/achievements')
@login_required
def achievements():
    # Get all possible achievements
    all_achievements = {
        'solfege_rabbit': {
            'name': 'Кролик-сольфеджист',
            'description': 'Пройден квест по сольфеджио с результатом 8/10',
            'icon': 'rabbit.png',
            'category': 'Знания'
        },
        'streak_3': {
            'name': '🥉 Бронзовая серия',
            'description': '3 дня подряд с записями',
            'icon': 'bronze.png',
            'category': 'Последовательность'
        },
        'streak_5': {
            'name': '🥈 Серебряная серия',
            'description': '5 дней подряд с записями',
            'icon': 'silver.png',
            'category': 'Последовательность'
        },
        'streak_10': {
            'name': '🥇 Золотая серия',
            'description': '10 дней подряд с записями',
            'icon': 'gold.png',
            'category': 'Последовательность'
        },
        'kulich_master': {
            'name': 'кулич',  # Изменено на просто "кулич"
            'description': 'Успешно освятил кулич, сыграв мелодию',
            'icon': 'kulich_master.png',
            'category': 'Пасхальные'
        },
        'first_recording': {
            'name': 'Первый шаг',
            'description': 'Записана первая импровизация',
            'icon': 'first.png',
            'category': 'Прогресс'
        }
    }

    # Get user's unlocked achievements from database
    user_achievements = Achievement.query.filter_by(user_id=current_user.id).all()

    achieved = []
    locked = []

    for ach_id, ach_data in all_achievements.items():
        # Ищем достижение, имя которого содержит ach_data['name'] после user_id_
        found = next(
            (a for a in user_achievements if a.name.endswith(ach_data['name'])),
            None
        )

        if found:
            achieved.append({
                'id': ach_id,
                'name': ach_data['name'],  # Отображаем только название (без user_id_)
                'description': ach_data['description'],
                'icon': ach_data['icon'],
                'category': ach_data['category'],
                'date': found.date_achieved.strftime('%d.%m.%Y')
            })
        else:
            locked.append({
                'id': ach_id,
                'name': ach_data['name'],
                'description': ach_data['description'],
                'icon': ach_data['icon'],
                'category': ach_data['category'],
                'locked': True
            })

    return render_template(
        'achievements.html',
        achieved=achieved,
        locked=locked,
        total_count=len(all_achievements),
        unlocked_count=len(achieved)
    )
@app.route('/streak')
@login_required
def streak():
    today = datetime.utcnow().date()
    yesterday = today - timedelta(days=1)

    # Проверяем и обновляем серию
    if current_user.last_activity_date:
        if current_user.last_activity_date == today:
            # Уже записывали сегодня - ничего не делаем
            pass
        elif current_user.last_activity_date == yesterday:
            # Продолжаем серию
            current_user.current_streak += 1
            current_user.last_activity_date = today
            db.session.commit()

            # Проверяем достижения
            check_achievements(current_user)
        else:
            # Пропустили день
            days_missed = (today - current_user.last_activity_date).days - 1

            if days_missed == 1:
                # Пропустили 1 день - замораживаем
                current_user.streak_frozen = True
            else:
                # Пропустили 2+ дней - сбрасываем
                current_user.current_streak = 0
                current_user.streak_frozen = False

            current_user.last_activity_date = today
            db.session.commit()

    # Определяем следующую цель
    milestones = [3, 5, 10, 30, 50, 100]
    next_milestone = next((m for m in milestones if m > current_user.current_streak), 100)

    # Готовим данные для графика истории
    records = ActivityRecord.query.filter_by(user_id=current_user.id)\
        .order_by(ActivityRecord.date.desc()).limit(30).all()

    streak_history_dates = [r.date.strftime('%d.%m') for r in records]
    streak_history_values = [r.streak_length for r in records]

    return render_template('streak.html',
                         current_streak=current_user.current_streak,
                         longest_streak=current_user.longest_streak,
                         streak_frozen=current_user.streak_frozen,
                         achievements=current_user.achievements or [],
                         next_milestone=next_milestone,
                         streak_history_dates=streak_history_dates,
                         streak_history_values=streak_history_values)

def check_achievements(user):
    milestones = [3, 5, 10, 30, 50, 100]
    achievements = user.achievements or {}

    for milestone in milestones:
        if user.current_streak >= milestone and str(milestone) not in achievements:
            achievements[str(milestone)] = datetime.utcnow().isoformat()
            # Здесь можно добавить уведомление о новом достижении

    user.achievements = achievements
    user.longest_streak = max(user.longest_streak, user.current_streak)
    db.session.commit()
@app.route('/logs-raw')
def logs_raw():
    return log_stream.getvalue()
@app.route('/posts')
@login_required
def posts():
    # Загружаем все посты с получателями
    posts = Post.query.options(
        db.joinedload(Post.recipients),
        db.joinedload(Post.author)
    ).order_by(Post.timestamp.desc()).all()

    return render_template('posts.html', posts=posts)
import logging
from io import StringIO
log_stream = StringIO()

# Настраиваем логгер Flask
handler = logging.StreamHandler(log_stream)
handler.setLevel(logging.INFO)
app.logger.addHandler(handler)

@app.route('/logs')
def show_logs():
    # Получаем логи из буфера
    logs = log_stream.getvalue()
    
    # HTML-шаблон для отображения
    html_template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>Логи Flask</title>
        <style>
            body { font-family: Arial; padding: 20px; }
            pre { 
                background: #f0f0f0; 
                padding: 10px; 
                border: 1px solid #ccc; 
                white-space: pre-wrap;
            }
        </style>
        <script>
setInterval(() => {
    fetch('/logs-raw')
        .then(response => response.text())
        .then(data => {
            document.querySelector('pre').textContent = data;
        });
}, 1000);  // Обновлять каждую секунду
</script>
    </head>
    <body>
        <h1>Логи Flask</h1>
        <pre>{{ logs }}</pre>
        <a href="/">На главную</a>
    </body>
    </html>
    """
    
    return render_template_string(html_template, logs=logs)
@app.route('/favicon.ico')
def favicon():
    return send_from_directory(os.path.join(app.root_path, 'static'),
                              'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/admin/users', methods=['GET', 'POST'])
@admin_required
def admin_users():
    if not current_user.is_admin:  # Проверка прав доступа
        flash('У вас нет прав доступа к этой странице.', 'danger')
        return redirect(url_for('index'))

    users = User.query.all()  # Получаем всех пользователей
    return render_template('admin_users.html', users=users)



@app.route('/admin/user/<int:user_id>', methods=['GET', 'POST'])
def admin_edit_user(user_id):
    if not current_user.is_admin:
        flash('У вас нет прав доступа к этой странице.', 'danger')
        return redirect(url_for('index'))

    user = User.query.get_or_404(user_id)  # Находим пользователя
    form = AdminUserForm(obj=user)  # Заполняем форму данными пользователя

    if form.validate_on_submit():
        user.is_admin = form.is_admin.data  # Обновляем статус администратора
        db.session.commit()
        flash(f'Права пользователя {user.username} обновлены.', 'success')
        return redirect(url_for('admin_users'))

    return render_template('admin_edit_user.html', user=user, form=form)
@app.route('/admin/create_post', methods=['GET', 'POST'])
@login_required
def create_post():
    if not current_user.is_admin:
        flash('Доступно только администраторам', 'danger')
        return redirect(url_for('home'))

    form = PostForm()
    if form.validate_on_submit():
        post = Post(
            content=form.content.data,
            author_id=current_user.id,
            access_type=form.access_type.data
        )
        db.session.add(post)
        db.session.flush()  # Чтобы получить post.id

        if form.access_type.data == 'private':
            for user_id in form.recipients.data:
                recipient = PostRecipient(post_id=post.id, user_id=user_id)
                db.session.add(recipient)

        db.session.commit()
        flash('Пост опубликован!', 'success')
        return redirect(url_for('posts'))
    potential_recipients = [
        {'id': user.id, 'username': user.username, 'avatar_url': user.avatar or '/static/default_avatar.png'}
        for user in User.query.all()
    ]
    potential_recipients_json = json.dumps(potential_recipients)

    return render_template('edit_post.html', form=form, potential_recipients_json=potential_recipients_json)

@app.route('/admin/edit_post/<int:post_id>', methods=['GET', 'POST'])
@login_required
def edit_post(post_id):
    post = Post.query.get_or_404(post_id)
    if not current_user.is_admin or post.author_id != current_user.id:
        flash('Нет прав для редактирования', 'danger')
        return redirect(url_for('posts'))

    form = PostForm()
    if form.validate_on_submit():
        post.content = form.content.data
        post.access_type = form.access_type.data

        if form.access_type.data == 'private':
            # Удаляем старых получателей
            PostRecipient.query.filter_by(post_id=post.id).delete()

            # Добавляем новых получателей
            for user_id in form.recipients.data:
                recipient = PostRecipient(post_id=post.id, user_id=user_id)
                db.session.add(recipient)

        db.session.commit()
        flash('Пост обновлен!', 'success')
        return redirect(url_for('posts'))

    elif request.method == 'GET':
        form.content.data = post.content
        form.access_type.data = post.access_type
        form.recipients.data = [r.user_id for r in post.recipients]

    # Передаем список пользователей в JSON
    potential_recipients = [
        {'id': user.id, 'username': user.username, 'avatar_url': user.avatar or '/static/default_avatar.png'}
        for user in User.query.all()
    ]
    potential_recipients_json = json.dumps(potential_recipients)

    return render_template('edit_post.html', form=form, post=post, potential_recipients_json=potential_recipients_json)

@app.route('/admin/toggle_visibility/<int:improvisation_id>', methods=['POST'])
@login_required
@admin_required
def admin_toggle_visibility(improvisation_id):
    try:
        data = request.get_json()
        if not data or 'is_public' not in data:
            return jsonify({'success': False, 'message': 'Invalid request'}), 400

        improvisation = Improvisation.query.get_or_404(improvisation_id)
        improvisation.is_public = data['is_public']
        db.session.commit()

        return jsonify({
            'success': True,
            'message': f'Видимость изменена на {"публичная" if improvisation.is_public else "приватная"}',
            'is_public': improvisation.is_public
        })
    except Exception as e:
        return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/admin/delete_post/<int:post_id>', methods=['POST'])
@login_required
def delete_post(post_id):
    try:
        post = Post.query.get_or_404(post_id)
        if not current_user.is_admin or post.author_id != current_user.id:
            return jsonify({
                'success': False,
                'message': 'У вас нет прав для удаления этого поста'
            }), 403

        db.session.delete(post)
        db.session.commit()

        return jsonify({'success': True, 'message': 'Пост успешно удален'})

    except Exception as e:
        return jsonify({
            'success': False,
            'message': str(e)
        }), 500
@app.route('/about')
def about():
    try:
        # Получаем абсолютный путь к static folder
        static_folder = app.static_folder or os.path.join(app.root_path, 'static')
        sandbox_dir = os.path.join(static_folder, 'sandbox')
        sandbox_path = os.path.join(sandbox_dir, 'sandbox.txt')
        
        app.logger.info(f"Attempting to access sandbox at: {sandbox_path}")
        
        # Создаем директорию, если не существует
        os.makedirs(sandbox_dir, exist_ok=True)
        
        # Создаем файл с содержимым по умолчанию, если не существует
        if not os.path.exists(sandbox_path):
            with open(sandbox_path, 'w', encoding='utf-8') as f:
                f.write("Добро пожаловать в песочницу!")
        
        # Читаем содержимое файла
        with open(sandbox_path, 'r', encoding='utf-8', errors='replace') as f:
            sandbox_content = f.read()
            
        return render_template('about.html', sandbox_content=sandbox_content)
        
    except Exception as e:
        app.logger.error(f"Error in /about: {str(e)}", exc_info=True)
        return render_template('error.html', error_message="Не удалось загрузить страницу"), 500

@app.route('/update_sandbox', methods=['POST'])
def update_sandbox():
    try:
        content = request.form.get('content', '')
        if not content:
            flash('Содержимое не может быть пустым', 'danger')
            return redirect('/about')
        
        static_folder = app.static_folder or os.path.join(app.root_path, 'static')
        sandbox_dir = os.path.join(static_folder, 'sandbox')
        sandbox_path = os.path.join(sandbox_dir, 'sandbox.txt')
        
        # Создаем директорию, если не существует
        os.makedirs(sandbox_dir, exist_ok=True)
        
        # Записываем содержимое
        with open(sandbox_path, 'w', encoding='utf-8') as f:
            f.write(content)
            
        flash('Содержимое успешно обновлено!', 'success')
        return redirect('/about')
        
    except Exception as e:
        app.logger.error(f"Error updating sandbox: {str(e)}", exc_info=True)
        flash('Ошибка при обновлении содержимого', 'danger')
        return redirect('/about')
@app.route('/react_post/<int:post_id>/<reaction_type>')
@login_required
def react_post(post_id, reaction_type):
    post = Post.query.get_or_404(post_id)
    valid_reactions = ['like', 'dislike','okay' , 'fire', 'heart', 'laugh', 'strawberry', 'hundred']

    if reaction_type not in valid_reactions:
        flash('Неверная реакция', 'danger')
        return redirect(url_for('posts'))

    # Проверяем существующую реакцию
    reaction = Reaction.query.filter_by(user_id=current_user.id, post_id=post.id).first()

    if reaction:
        if reaction.reaction_type == reaction_type:
            # Удаляем реакцию если тот же тип
            db.session.delete(reaction)
            db.session.commit()
        else:
            # Меняем тип реакции
            reaction.reaction_type = reaction_type
            db.session.commit()
    else:
        # Создаем новую реакцию
        new_reaction = Reaction(
            user_id=current_user.id,
            post_id=post.id,
            reaction_type=reaction_type
        )
        db.session.add(new_reaction)
        db.session.commit()

    return redirect(url_for('posts'))
# Загрузка пользователя для Flask-Login
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

def pluralize_improvizations(count):
    if count==0:
        return f"Еще не записал."
    if count % 100 in (11, 12, 13, 14):
        return f"{count} импровизаций"
    if count % 10 == 1:
        return f"{count} импровизация"
    if count % 10 in (2, 3, 4):
        return f"{count} импровизации"
    return f"{count} импровизаций"

# Регистрируем фильтр в Jinja2
app.jinja_env.filters['pluralize_improv'] = pluralize_improvizations
# Генерация случайного имени файла
def generate_random_filename():
    random_part = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
    return f"{random_part}record.wav"

# Главная страница
@app.route('/')
def home():
    return render_template('home.html')

# Регистрация
@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        # Проверяем, существует ли пользователь с таким же именем
        existing_user = User.query.filter_by(username=form.username.data).first()
        if existing_user:
            flash('Пользователь с таким именем уже существует. Выберите другое имя.', 'danger')
            return redirect(url_for('register'))

        # Создаем нового пользователя
        user = User(
            username=form.username.data,
            real_name=form.real_name.data,
            password=form.password.data,
            improvisation_count=form.improvisation_count.data  # Уже записанные импровизации
        )
        db.session.add(user)
        db.session.commit()
        flash('Аккаунт успешно создан! Добро пожаловать в хаос!', 'success')
        return redirect(url_for('login'))
    return render_template('register.html', form=form)
@app.route('/tuner')
@login_required
def tuner():
    return render_template('tuner.html')
@app.route('/recover_password', methods=['GET', 'POST'])
def recover_password():
    if request.method == 'POST':
        user_code = request.form.get('user_code')
        new_password = request.form.get('new_password')
        confirm_password = request.form.get('confirm_password')

        if not user_code or not new_password or not confirm_password:
            flash('Все поля обязательны для заполнения', 'danger')
            return redirect(url_for('recover_password'))

        if new_password != confirm_password:
            flash('Пароли не совпадают', 'danger')
            return redirect(url_for('recover_password'))

        # Находим пользователя по коду
        user = None
        for u in User.query.all():
            if generate_user_code(u.id) == user_code:
                user = u
                break

        if not user:
            flash('Неверный код пользователя', 'danger')
            return redirect(url_for('recover_password'))

        # Обновляем пароль
        user.password = new_password
        db.session.commit()

        flash('Пароль успешно изменен! Теперь вы можете войти с новым паролем.', 'success')
        return redirect(url_for('login'))

    return render_template('recover_password.html')
# Авторизация
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and user.password == form.password.data:
            login_user(user)
            return redirect(url_for('home'))
        else:
            flash('Эх... Ты где то ошибся, дружок. Проверь пароль и имя еще раз.', 'danger')
    return render_template('login.html', form=form)
@app.route('/update_avatar', methods=['POST'])
@login_required
def update_avatar():
    if 'avatar' not in request.files:
        flash('Файл не выбран', 'danger')
        return redirect(url_for('profile'))

    avatar = request.files['avatar']
    if avatar.filename == '':
        flash('Файл не выбран', 'danger')
        return redirect(url_for('profile'))

    filename = secure_filename(avatar.filename)
    avatar_path = os.path.join(app.config['AVATAR_UPLOAD_FOLDER'], filename)
    avatar.save(avatar_path)

    current_user.avatar = f'avatars/{filename}'
    db.session.commit()

    flash('Аватар успешно обновлен', 'success')
    return redirect(url_for('profile'))

@app.route('/update_password', methods=['POST'])
@login_required
def update_password():
    current_password = request.form.get('current_password')
    new_password = request.form.get('new_password')
    confirm_password = request.form.get('confirm_password')

    if not current_password or not new_password or not confirm_password:
        flash('Все поля обязательны для заполнения', 'danger')
        return redirect(url_for('profile'))

    if new_password != confirm_password:
        flash('Новые пароли не совпадают', 'danger')
        return redirect(url_for('profile'))

    if current_user.password != current_password:
        flash('Неверный текущий пароль', 'danger')
        return redirect(url_for('profile'))

    current_user.password = new_password
    db.session.commit()

    flash('Пароль успешно изменен', 'success')
    return redirect(url_for('profile'))
# Выход из системы
@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('home'))
from flask import request, jsonify

@app.route('/activity')
@login_required
def activity():
    # Получаем даты записей пользователя
    records = ActivityRecord.query.filter_by(user_id=current_user.id)\
        .order_by(ActivityRecord.date.desc()).all()

    # Подготавливаем данные для графика
    today = datetime.utcnow().date()

    # За неделю
    week_dates = [today - timedelta(days=i) for i in range(6, -1, -1)]
    week_data = [sum(1 for r in records if r.date == d) for d in week_dates]
    week_labels = [d.strftime('%a %d.%m') for d in week_dates]

    # За месяц (последние 30 дней)
    month_dates = [today - timedelta(days=i) for i in range(29, -1, -1)]
    month_data = [sum(1 for r in records if r.date == d) for d in month_dates]
    month_labels = [d.strftime('%d.%m') for d in month_dates]

    # За полгода (по неделям)
    half_year_dates = [today - timedelta(weeks=i) for i in range(25, -1, -1)]
    half_year_data = [
        sum(1 for r in records if d <= r.date < d + timedelta(weeks=1))
        for d in half_year_dates
    ]
    half_year_labels = [d.strftime('%d.%m') for d in half_year_dates]

    # Подсчет текущей серии
    current_streak = 0
    day = today
    while day in [r.date for r in records]:
        current_streak += 1
        day -= timedelta(days=1)

    # Максимальная серия
    max_streak = 0
    temp_streak = 0
    sorted_dates = sorted([r.date for r in records], reverse=True)
    for i in range(1, len(sorted_dates)):
        if (sorted_dates[i-1] - sorted_dates[i]).days == 1:
            temp_streak += 1
            max_streak = max(max_streak, temp_streak)
        else:
            temp_streak = 0

    # Последние 10 записей для таблицы
    recent_activity = records[:10]

    return render_template('activity.html',
                         week_labels=week_labels,
                         week_data=week_data,
                         month_labels=month_labels,
                         month_data=month_data,
                         half_year_labels=half_year_labels,
                         half_year_data=half_year_data,
                         current_streak=current_streak,
                         max_streak=max_streak,
                         recent_activity=recent_activity)
# Админская панель
@app.route('/admin', methods=['GET'])
@login_required
def admin_panel():
    if not current_user.is_admin:
        flash('У вас нет доступа к админской панели.', 'danger')
        return redirect(url_for('home'))

    # Получаем всех пользователей
    users = User.query.all()

    # Получаем все импровизации
    improvisations = Improvisation.query.all()

    # Передаем данные в шаблон
    return render_template('admin_panel.html', users=users, improvisations=improvisations)

# Удаление пользователя
@app.route('/admin/delete_user/<int:user_id>', methods=['POST'])
@login_required
def delete_user(user_id):
    if not current_user.is_admin:
        return jsonify({'success': False, 'message': 'У вас нет доступа.'})

    user = User.query.get_or_404(user_id)

    # Удаление связанных записей в таблице activity_record
    ActivityRecord.query.filter_by(user_id=user_id).delete()

    # Удаление пользователя
    db.session.delete(user)
    db.session.commit()

    return jsonify({'success': True, 'message': 'Пользователь удален.'})

# Изменение данных пользователя
@app.route('/admin/update_user/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def update_user(user_id):
    try:
        data = request.get_json()
        if not data:
            return jsonify({'success': False, 'message': 'Invalid request'}), 400

        user = User.query.get_or_404(user_id)

        # Обновляем только разрешенные поля
        if 'username' in data:
            user.username = data['username']
        if 'real_name' in data:
            user.real_name = data['real_name']
        if 'improvisation_count' in data:
            user.improvisation_count = data['improvisation_count']

        db.session.commit()

        return jsonify({
            'success': True,
            'message': 'Данные обновлены',
            'user': {
                'id': user.id,
                'username': user.username,
                'real_name': user.real_name,
                'is_admin': user.is_admin,
                'improvisation_count': user.improvisation_count
            }
        })
    except Exception as e:
        return jsonify({'success': False, 'message': str(e)}), 500

# Удаление импровизации
@app.route('/delete_improvisation/<int:improvisation_id>', methods=['POST'])
@login_required
def delete_improvisation(improvisation_id):
    try:
        if not current_user.is_admin:
            return jsonify({
                'success': False,
                'message': 'У вас нет прав администратора'
            }), 403

        improvisation = Improvisation.query.get_or_404(improvisation_id)
        user = improvisation.user  # Получаем пользователя

        # Удаляем связанные записи активности
        ActivityRecord.query.filter_by(improvisation_id=improvisation_id).delete()

        # Удаляем файл с сервера (если существует)
        if improvisation.audio_path:
            try:
                file_path = os.path.join(app.config['UPLOAD_FOLDER'], improvisation.audio_path)
                if os.path.exists(file_path):
                    os.remove(file_path)
            except Exception as e:
                app.logger.error(f"Error deleting audio file: {str(e)}")

        # Уменьшаем счётчик импровизаций (но не ниже 0)
        if user.improvisation_count > 0:
            user.improvisation_count -= 1

        # Обновляем информацию о последней активности и серии
        if hasattr(improvisation, 'created_at') and improvisation.created_at:
            # Проверяем, была ли это последняя запись пользователя
            last_improvisation = Improvisation.query.filter(
                Improvisation.user_id == user.id,
                Improvisation.id != improvisation_id
            ).order_by(Improvisation.created_at.desc()).first()

            if last_improvisation and hasattr(last_improvisation, 'created_at') and last_improvisation.created_at:
                # Обновляем дату последней активности
                user.last_activity_date = last_improvisation.created_at.date()
            else:
                # Это была последняя запись пользователя
                user.last_activity_date = None
                user.current_streak = 0
                user.streak_frozen = False

        # Удаляем саму импровизацию
        db.session.delete(improvisation)
        db.session.commit()

        return jsonify({
            'success': True,
            'message': 'Импровизация успешно удалена',
            'new_count': user.improvisation_count,
            'user_id': user.id
        })

    except Exception as e:
        db.session.rollback()
        app.logger.error(f"Error deleting improvisation: {str(e)}", exc_info=True)
        return jsonify({
            'success': False,
            'message': f'Произошла ошибка при удалении: {str(e)}'
        }), 500
# Профиль пользователя
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def profile():
    form = ProfileForm()
    if form.validate_on_submit():
        current_user.username = form.username.data

        # Handle avatar upload
        if form.avatar.data:
            avatar = form.avatar.data
            filename = secure_filename(avatar.filename)
            avatar_path = os.path.join(app.config['AVATAR_UPLOAD_FOLDER'], filename)
            avatar.save(avatar_path)
            current_user.avatar = f'avatars/{filename}'

        # Handle password change if requested
        if form.current_password.data and form.new_password.data:
            if current_user.password == form.current_password.data:
                current_user.password = form.new_password.data
                flash('Пароль успешно изменен', 'success')
            else:
                flash('Неверный текущий пароль', 'danger')

        db.session.commit()
        flash('Профиль обновлен', 'success')
        return redirect(url_for('profile'))
    elif request.method == 'GET':
        form.username.data = current_user.username
    return render_template('profile.html', form=form)
@app.route('/admin/toggle_admin/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def toggle_admin(user_id):
    try:
        data = request.get_json()
        if not data or 'is_admin' not in data:
            return jsonify({'success': False, 'message': 'Invalid request'}), 400

        user = User.query.get_or_404(user_id)
        user.is_admin = data['is_admin']
        db.session.commit()

        return jsonify({
            'success': True,
            'message': f'Админские права {"выданы" if user.is_admin else "отозваны"}',
            'is_admin': user.is_admin
        })
    except Exception as e:
        return jsonify({'success': False, 'message': str(e)}), 500

@app.route('/admin/toggle_visibility_permission/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def toggle_visibility_permission(user_id):
    try:
        data = request.get_json()
        if not data or 'can_toggle_visibility' not in data:
            return jsonify({'success': False, 'message': 'Invalid request'}), 400

        user = User.query.get_or_404(user_id)
        user.can_toggle_visibility = data['can_toggle_visibility']
        db.session.commit()

        return jsonify({
            'success': True,
            'message': f'Право на изменение видимости {"разрешено" if user.can_toggle_visibility else "запрещено"}',
            'can_toggle_visibility': user.can_toggle_visibility
        })
    except Exception as e:
        return jsonify({'success': False, 'message': str(e)}), 500
# Запись импровизации
@app.route('/record_improvisation', methods=['GET', 'POST'])
@login_required
def record_improvisation():
    form = ImprovisationForm()

    if request.method == 'POST' and form.validate_on_submit():
        try:
            # Проверка временного ограничения (1 запись в день)
            if current_user.last_improvisation_date and current_user.last_improvisation_date.date() == datetime.utcnow().date():
                flash('Вы уже записывали импровизацию сегодня. Можно только одну в день!', 'warning')
                return redirect(url_for('record_improvisation'))

            # Проверка аудиофайла
            audio_file = request.files.get('audio')
            if not audio_file or audio_file.filename == '':
                flash('Пожалуйста, прикрепите аудиофайл', 'danger')
                return redirect(url_for('record_improvisation'))

            # Генерация имени файла
            filename = secure_filename(f"{current_user.id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.wav")
            filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
            os.makedirs(os.path.dirname(filepath), exist_ok=True)
            audio_file.save(filepath)
            # Создание записи об импровизации (теперь с duration)
            improvisation = Improvisation(
                title=form.title.data,
                audio_path=filename,
                user_id=current_user.id,
            )
            db.session.add(improvisation)
            db.session.flush()  # Чтобы получить ID импровизации

            # Обновление статистики пользователя
            today = datetime.utcnow().date()
            yesterday = today - timedelta(days=1)

            # Логика обновления серии
            if not current_user.last_activity_date:
                current_streak = 1
            elif current_user.last_activity_date == yesterday:
                current_streak = current_user.current_streak + 1
                if current_user.streak_frozen:
                    current_user.streak_frozen = False
                    flash('❄️ Серия разморожена!', 'success')
            elif current_user.last_activity_date == today:
                current_streak = current_user.current_streak
            else:
                days_missed = (today - current_user.last_activity_date).days - 1
                if days_missed == 1:
                    current_streak = current_user.current_streak
                    current_user.streak_frozen = True
                    flash('Вы пропустили вчера! Серия заморожена.', 'warning')
                else:
                    if current_user.current_streak > 0:
                        flash(f'Серия из {current_user.current_streak} дней сброшена.', 'danger')
                    current_streak = 1

            # Создание записи активности
            activity = ActivityRecord(
                user_id=current_user.id,
                date=today,
                streak_length=current_streak,
                improvisation_id=improvisation.id
            )

            # Обновление данных пользователя
            current_user.improvisation_count += 1
            current_user.last_improvisation_date = datetime.utcnow()
            current_user.last_activity_date = today
            current_user.current_streak = current_streak
            current_user.longest_streak = max(current_user.longest_streak or 0, current_streak)

            db.session.add(activity)
            db.session.commit()

            check_achievements(current_user)

            flash(f'🎉 Импровизация записана!  Серия: {current_streak} дней', 'success')
            return redirect(url_for('user_profile', user_id=current_user.id))

        except Exception as e:
            db.session.rollback()
            app.logger.error(f"Error recording improvisation: {str(e)}", exc_info=True)
            flash('Произошла ошибка при сохранении записи', 'danger')
            return redirect(url_for('record_improvisation'))

    return render_template('record_improvisation.html',
                         form=form,
                         current_streak=current_user.current_streak,
                         streak_frozen=current_user.streak_frozen)

# Лидербордss
@app.route('/leaderboard')
def leaderboard():
    leaders = User.query.order_by(User.improvisation_count.desc()).limit(10).all()
    user = User.query.get_or_404
    # Находим максимальное количество импровизаций среди лидеров
    max_improvisations = 60

    return render_template('leaderboard.html',
                         leaders=leaders,
                         max_improvisations=max_improvisations)
# Страница пользователя
@app.route('/user/<int:user_id>')
def user_profile(user_id):
    user = User.query.get_or_404(user_id)

    # Все возможные достижения с их базовыми именами
    all_achievements = {
        'solfege_rabbit': {
            'base_name': 'Кролик-сольфеджист',  # Базовое имя для сравнения
            'display_name': 'Кролик-сольфеджист',  # Имя для отображения
            'icon': 'rabbit.png',
            'category': 'Пасхальные'
        },
        'streak_3': {
            'base_name': '🥉 Бронзовая серия',
            'display_name': '🥉 Бронзовая серия',
            'icon': 'bronze.png',
            'category': 'Последовательность'
        },
        'streak_5': {
            'base_name': '🥈 Серебряная серия',
            'display_name': '🥈 Серебряная серия',
            'icon': 'silver.png',
            'category': 'Последовательность'
        },
        'streak_10': {
            'base_name': '🥇 Золотая серия',
            'display_name': '🥇 Золотая серия',
            'icon': 'gold.png',
            'category': 'Последовательность'
        },
        'kulich_master': {
            'base_name': 'Кулич',  # Базовое имя для поиска в БД (часть после user_id_)
            'display_name': 'Мастер Кулич',  # Имя для отображения
            'icon': 'kulich_master.png',
            'category': 'Пасхальные'
        },
        'first_recording': {
            'base_name': 'Первый шаг',
            'display_name': 'Первый шаг',
            'icon': 'first.png',
            'category': 'Прогресс'
        }
    }

    # Получаем достижения пользователя
    user_achievements = Achievement.query.filter_by(user_id=user.id).all()

    # Создаем список достижений для отображения
    achieved = []
    for ach in user_achievements:
        # Извлекаем базовое имя (часть после user_id_)
        achievement_name = ach.name.split('_')[-1] if '_' in ach.name else ach.name

        # Находим соответствующее достижение в all_achievements
        for ach_id, ach_data in all_achievements.items():
            if ach_data['base_name'] == achievement_name:
                achieved.append({
                    'id': ach_id,
                    'name': ach_data['display_name'],
                    'icon': ach_data['icon'],
                    'date': ach.date_achieved.strftime('%d.%m.%Y'),
                    'description': ach.description,
                    'category': ach_data['category']
                })
                break

    # Фильтрация импровизаций
    if current_user.is_authenticated and (current_user.id == user.id or current_user.is_admin):
        improvisations = user.improvisations
    else:
        improvisations = [imp for imp in user.improvisations if imp.is_public]

    return render_template('user_profile.html',
                         user=user,
                         improvisations=improvisations,
                         public_count=len([imp for imp in user.improvisations if imp.is_public]),
                         achievements=achieved[:5],  # Показываем первые 5 достижений
                         achievement_count=len(achieved),
                         total_achievements=len(all_achievements))

# Переключение видимости импровизации
@app.route('/toggle_visibility/<int:improvisation_id>')
@login_required
def toggle_visibility(improvisation_id):
    improvisation = Improvisation.query.get_or_404(improvisation_id)

    # Проверяем права
    if current_user.id != improvisation.user_id and not current_user.is_admin:
        flash('Нет прав для изменения видимости', 'danger')
        return redirect(url_for('home'))

    # Проверяем, разрешено ли пользователю менять видимость
    user = improvisation.user
    if not current_user.is_admin and not user.can_toggle_visibility:
        flash('Изменение видимости запрещено администратором', 'warning')
        return redirect(url_for('user_profile', user_id=current_user.id))

    # Меняем видимость
    improvisation.is_public = not improvisation.is_public
    db.session.commit()

    flash(f'Импровизация теперь {"публичная" if improvisation.is_public else "приватная"}', 'success')
    return redirect(url_for('user_profile', user_id=current_user.id))

# Публичные импровизации
@app.route('/public_improvisations')
def public_improvisations():
    improvisations = Improvisation.query.filter_by(is_public=True).all()
    return render_template('public_improvisations.html', improvisations=improvisations)

# Отправка сообщения
@app.route('/send_message', methods=['GET', 'POST'])
@app.route('/send_message/<int:recipient_id>', methods=['GET', 'POST'])
@login_required
def send_message(recipient_id=None):
    form = MessageForm()
    recipient = User.query.get(recipient_id) if recipient_id else None
    all_users = User.query.filter(User.id != current_user.id).all()

    if form.validate_on_submit():
        recipient_id = request.form.get('recipient_id')
        if not recipient_id:
            flash('Выберите получателя', 'danger')
            return redirect(url_for('send_message'))

        message = Message(
            sender_id=current_user.id,
            recipient_id=recipient_id,
            content=form.content.data,
            improvisation_id=form.improvisation_id.data
        )
        db.session.add(message)
        db.session.commit()
        flash('Сообщение отправлено!', 'success')
        return redirect(url_for('messages'))

    return render_template('send_message.html',
                         form=form,
                         recipient=recipient,
                         all_users=all_users)

# Просмотр сообщений

@app.route('/messages')
@login_required
def messages():
    # Получаем все сообщения для текущего пользователя
    received_messages = Message.query.filter_by(recipient_id=current_user.id).order_by(Message.timestamp.desc()).all()
    sent_messages = Message.query.filter_by(sender_id=current_user.id).order_by(Message.timestamp.desc()).all()
    return render_template('messages.html', received_messages=received_messages, sent_messages=sent_messages)

# Создание базы данных при первом запуске
with app.app_context():
    db.create_all()

# Запуск приложения
if __name__ == '__main__':
    app.run(host="0.0.0.0", debug=True)
По записи импровизаций. Работать должно так: можно записать одну импровизацию в течение суток (с 00:00:00 до 23:59:59).

Но бывают ситуации, когда вы болеете, или уезжаете, или возникают ещё какие-нибудь обстоятельства. На такие случаи у меня должна быть возможность добавить отдельным пользователям возможность записать какое-то количество импровизаций вне установленных изначально суток. Это возможно?
меняй дб, делай как надо