import os
import logging
from dotenv import load_dotenv
import requests
from telegram import ReplyKeyboardMarkup, Update, ReplyKeyboardRemove
from telegram.ext import (
    Application,
    CommandHandler,
    MessageHandler,
    filters,
    ContextTypes,
    ConversationHandler,
)
from datetime import datetime, timedelta, time
import pytz
import random

# Настройка логгирования
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO
)
logger = logging.getLogger(__name__)

# Загружаем переменные из .env
load_dotenv()
BOT_TOKEN = os.getenv('BOT_TOKEN')
WEATHER_API_KEY = os.getenv('WEATHER_API_KEY')

# Константы
DEMBEL_DATE = datetime(2025, 12, 6)  # Дата дембеля
MOSCOW_TZ = pytz.timezone('Europe/Moscow')  # Часовой пояс Москвы

# Мотивирующие фразы
MOTIVATIONAL_PHRASES = [
    "💪 Ты сильнее, чем думаешь! Каждый день - шаг к цели!",
    "🔥 Осталось совсем немного! Ты справишься!",
    "🚀 Сегодняшний день приближает тебя к заветной дате!",
    "🦾 Терпение и труд всё перетрут! Держись!",
    "🌟 Ты уже прошел большую часть пути! Осталось чуть-чуть!",
    "🏆 Каждый день - это маленькая победа!",
    "💯 Ты молодец! Скоро долгожданный день!",
    "🌈 Не сдавайся! Ты ближе, чем кажется!",
    "🌟 Каждый день - это возможность стать лучше!",
    "🌟 Ты можешь всё, что захочешь!",
]

# Состояния для ConversationHandler
SELECTING_ACTION, TYPING_CITY, NOTES, TYPING_NOTE, SET_REMINDER, TRACK_PROGRESS, MOOD, BOOK_PROGRESS = range(8)

# Клавиатура с городами и кнопками
reply_keyboard = [
    ["Ростов-на-Дону 🌊", "Москва 🏖️"],
    ["Поиск 🔍", "Заметки 📝"],
    ["Напоминания ⏰", "Прогресс 📊"],
    ["Настроение 😊", "Книги 📚"],
    ["Назад ⬅️"],
]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True, resize_keyboard=True)

# Функции бота
def get_days_until_dembel() -> int:
    """Возвращает количество дней до дембеля"""
    now = datetime.now(MOSCOW_TZ)  # Текущая дата с учетом временной зоны
    dembel_date_with_tz = MOSCOW_TZ.localize(DEMBEL_DATE)  # Дата дембеля с временной зоной
    delta = dembel_date_with_tz - now  # Вычитаем текущую дату из даты дембеля
    return delta.days

def get_motivational_phrase() -> str:
    """Возвращает случайную мотивирующую фразу"""
    return random.choice(MOTIVATIONAL_PHRASES)

async def send_daily_notification(context: ContextTypes.DEFAULT_TYPE) -> None:
    """Отправляет ежедневное уведомление"""
    try:
        chat_id = context.job.chat_id
        days_left = get_days_until_dembel()
        phrase = get_motivational_phrase()

        message = (
            f"⏳ До дембеля осталось {days_left} дней!\n"
            f"🗓 Дата: {DEMBEL_DATE.strftime('%d.%m.%Y')}\n\n"
            f"{phrase}"
        )

        await context.bot.send_message(chat_id=chat_id, text=message)
        logger.info(f"Отправлено ежедневное уведомление пользователю {chat_id}")

    except Exception as e:
        logger.error(f"Ошибка при отправке уведомления: {e}")

def get_weather_emoji(description: str) -> str:
    """Возвращает соответствующий эмодзи для погодных условий"""
    description = description.lower()
    if 'ясно' in description:
        return '☀️'
    elif 'облачно' in description or 'пасмурно' in description:
        return '☁️'
    elif 'дождь' in description:
        return '🌧️'
    elif 'снег' in description:
        return '❄️'
    elif 'гроза' in description:
        return '⛈️'
    elif 'морось' in description:
        return '🌦️'
    elif 'туман' in description or 'дымка' in description:
        return '🌫️'
    else:
        return '🌈'

def get_clothing_recommendation(temp: float, weather_desc: str) -> str:
    """Возвращает рекомендации по одежде"""
    weather_desc = weather_desc.lower()
    if temp > 25:
        clothes = (
            "👕 Легкая одежда (шорты/юбка, футболка)\n"
            "🧢 Головной убор от солнца\n"
            "🕶️ Солнцезащитные очки"
        )
    elif 18 <= temp <= 25:
        clothes = (
            "👕 Летняя одежда (рубашка/блузка)\n"
            "👖 Джинсы или легкие брюки"
        )
    elif 10 <= temp < 18:
        clothes = (
            "🧥 Легкая куртка или ветровка\n"
            "👖 Джинсы или брюки\n"
            "👟 Закрытая обувь"
        )
    elif 0 <= temp < 10:
        clothes = (
            "🧥 Теплая куртка\n"
            "🧣 Шарф при ветре\n"
            "🧤 Перчатки, если холодно"
        )
    else:
        clothes = (
            "🧥 Зимняя куртка\n"
            "🧣 Шарф\n"
            "🧤 Перчатки\n"
            "🎩 Теплая шапка"
        )

    if 'дождь' in weather_desc:
        clothes += "\n☔ Зонт или дождевик"
    elif 'снег' in weather_desc:
        clothes += "\n🧦 Теплые носки и зимняя обувь"
    elif 'ясно' in weather_desc:
        clothes += "\n🧴 Не забудьте солнцезащитный крем"

    return clothes

async def get_tomorrow_forecast(city: str) -> str:
    """Получает прогноз погоды на завтра"""
    try:
        url = f"http://api.openweathermap.org/data/2.5/forecast?q={city}&appid={WEATHER_API_KEY}&units=metric&lang=ru&cnt=16"
        response = requests.get(url, timeout=5)
        data = response.json()

        if data.get("cod") != "200":
            return ""

        tomorrow = (datetime.now() + timedelta(days=1)).date()
        temps = []
        descriptions = []

        for item in data['list']:
            forecast_date = datetime.fromtimestamp(item['dt']).date()
            if forecast_date == tomorrow:
                temps.append(item['main']['temp'])
                descriptions.append(item['weather'][0]['description'])

        if not temps:
            return ""

        avg_temp = round(sum(temps) / len(temps), 1)
        main_desc = max(set(descriptions), key=descriptions.count)
        emoji = get_weather_emoji(main_desc)

        return f"Завтра: {avg_temp}°C, {main_desc} {emoji}"

    except Exception as e:
        logger.error(f"Ошибка получения прогноза на завтра: {e}")
        return ""

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Начало работы с ботом"""
    try:
        user = update.effective_user
        chat_id = update.effective_chat.id

        # Удаляем старое задание, если существует
        if 'job' in context.chat_data:
            old_job = context.chat_data['job']
            old_job.schedule_removal()

        # Создаем новое задание
        time_utc = time(hour=10, minute=40)  # Было 6:45 по МСК (UTC+3)
        new_job = context.job_queue.run_daily(
            send_daily_notification,
            time=time_utc,
            days=(0, 1, 2, 3, 4, 5, 6),
            chat_id=chat_id,
            name=str(chat_id)
        )

        # Сохраняем задание в контексте
        context.chat_data['job'] = new_job

        days_left = get_days_until_dembel()
        phrase = get_motivational_phrase()

        await update.message.reply_text(
            f"Привет, {user.first_name}! 👋\n"
            f"⏳ До дембеля осталось {days_left} дней!\n"
            f"🗓 Дата: {DEMBEL_DATE.strftime('%d.%m.%Y')}\n\n"
            f"{phrase}\n\n"
            "Я бот, который покажет тебе текущую погоду и подскажет, что надеть.\n"
            "Выбери город из списка или найди другой:",
            reply_markup=markup
        )
        return SELECTING_ACTION

    except Exception as e:
        logger.error(f"Ошибка в start: {str(e)}")
        await update.message.reply_text("😔 Произошла ошибка. Попробуйте /start")
        return ConversationHandler.END

async def get_weather(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Получает погоду для выбранного города"""
    try:
        city = update.message.text

        if city == "Поиск 🔍":
            await update.message.reply_text(
                "🔍 Введите название города (например: Москва, Сочи):",
                reply_markup=ReplyKeyboardRemove(),
            )
            return TYPING_CITY

        if "Ростов" in city:
            city_api = "Rostov-on-Don"
            city_display = "Ростове-на-Дону 🌊"
        elif "Москва" in city:
            city_api = "Moscow"
            city_display = "Москве 🏖️"
        else:
            city_api = city
            city_display = city

        await update.message.reply_chat_action(action="typing")

        # Текущая погода
        current_url = f"http://api.openweathermap.org/data/2.5/weather?q={city_api}&appid={WEATHER_API_KEY}&units=metric&lang=ru"
        current_response = requests.get(current_url, timeout=10)
        current_data = current_response.json()

        if current_data.get("cod") != 200:
            error = current_data.get("message", "неизвестная ошибка")
            await update.message.reply_text(
                f"⚠️ Ошибка: {error}\nПопробуйте другой город.",
                reply_markup=markup
            )
            return SELECTING_ACTION

        # Парсим данные
        weather_desc = current_data["weather"][0]["description"]
        temp = current_data["main"]["temp"]
        feels_like = current_data["main"]["feels_like"]
        humidity = current_data["main"]["humidity"]
        wind = current_data["wind"]["speed"]
        emoji = get_weather_emoji(weather_desc)

        # Получаем рекомендации по одежде
        clothes = get_clothing_recommendation(temp, weather_desc)

        # Получаем прогноз на завтра
        tomorrow_forecast = await get_tomorrow_forecast(city_api)

        # Формируем сообщение
        message = (
            f"{emoji} Погода в {city_display}:\n\n"
            f"• Сейчас: {temp}°C (ощущается как {feels_like}°C)\n"
            f"• Состояние: {weather_desc.capitalize()} {emoji}\n"
            f"• Влажность: {humidity}% 💧\n"
            f"• Ветер: {wind} м/с 💨\n\n"
        )

        if tomorrow_forecast:
            message += f"{tomorrow_forecast}\n\n"

        message += (
            f"Что надеть:\n{clothes}\n\n"
            f"Хотите узнать погоду в другом месте? Выберите город:"
        )

        await update.message.reply_text(message, reply_markup=markup)
        return SELECTING_ACTION

    except requests.exceptions.Timeout:
        await update.message.reply_text(
            "⌛ Сервер погоды не отвечает. Попробуйте позже.",
            reply_markup=markup
        )
        return SELECTING_ACTION
    except Exception as e:
        logger.error(f"Ошибка в get_weather: {e}")
        await update.message.reply_text(
            "😔 Произошла ошибка. Попробуйте /start",
            reply_markup=markup
        )
        return SELECTING_ACTION
        async def search_city(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
        """Обрабатывает введенный пользователем город"""
        try:
        city = update.message.text
        return await get_weather(update, context)
    except Exception as e:
        logger.error(f"Ошибка в search_city: {e}")
        await update.message.reply_text(
            "😔 Ошибка обработки города. Попробуйте /start",
            reply_markup=markup
        )
        return SELECTING_ACTION

async def notes(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Обрабатывает команду "Заметки" и предлагает пользователю написать заметку"""
    await update.message.reply_text(
        "📝 Введите вашу заметку:",
        reply_markup=ReplyKeyboardRemove()
    )
    return TYPING_NOTE

async def save_note(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Сохраняет заметку пользователя"""
    user_id = update.effective_user.id
    note = update.message.text

    if 'notes' not in context.user_data:
        context.user_data['notes'] = []

    context.user_data['notes'].append(note)

    await update.message.reply_text(
        "✅ Заметка сохранена!\n\n"
        "Чтобы посмотреть все заметки, нажмите /notes.\n"
        "Чтобы вернуться к погоде, нажмите /start.",
        reply_markup=markup
    )
    return SELECTING_ACTION

async def show_notes(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Показывает все заметки пользователя"""
    user_id = update.effective_user.id

    if 'notes' not in context.user_data or not context.user_data['notes']:
        await update.message.reply_text(
            "📭 У вас пока нет заметок.\n"
            "Чтобы добавить заметку, нажмите /notes.",
            reply_markup=markup
        )
    else:
        notes_list = "\n\n".join(context.user_data['notes'])
        await update.message.reply_text(
            f"📝 Ваши заметки:\n\n{notes_list}\n\n"
            "Чтобы добавить новую заметку, нажмите /notes.\n"
            "Чтобы вернуться к погоде, нажмите /start.",
            reply_markup=markup
        )
    return SELECTING_ACTION

async def set_reminder(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Обрабатывает команду "Напоминания" и предлагает пользователю установить напоминание"""
    await update.message.reply_text(
        "⏰ Введите текст напоминания и время в формате 'Текст напоминания | ДД.ММ.ГГГГ ЧЧ:ММ' (например: 'Забрать посылку | 25.10.2023 18:00'):",
        reply_markup=ReplyKeyboardRemove()
    )
    return SET_REMINDER

async def save_reminder(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Сохраняет напоминание пользователя"""
    user_id = update.effective_user.id
    reminder_data = update.message.text.split(" | ")

    if len(reminder_data) != 2:
        await update.message.reply_text(
            "❌ Неверный формат напоминания. Попробуйте снова.",
            reply_markup=markup
        )
        return SET_REMINDER

    reminder_text, reminder_time_str = reminder_data

    try:
        reminder_time = datetime.strptime(reminder_time_str, "%d.%m.%Y %H:%M")
    except ValueError:
        await update.message.reply_text(
            "❌ Неверный формат времени. Попробуйте снова.",
            reply_markup=markup
        )
        return SET_REMINDER

    if 'reminders' not in context.user_data:
        context.user_data['reminders'] = []

    context.user_data['reminders'].append({
        'text': reminder_text,
        'time': reminder_time
    })

    await update.message.reply_text(
        "✅ Напоминание сохранено!\n\n"
        "Чтобы посмотреть все напоминания, нажмите /reminders.\n"
        "Чтобы вернуться к погоде, нажмите /start.",
        reply_markup=markup
    )
    return SELECTING_ACTION

async def show_reminders(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Показывает все напоминания пользователя"""
    user_id = update.effective_user.id

    if 'reminders' not in context.user_data or not context.user_data['reminders']:
        await update.message.reply_text(
            "📭 У вас пока нет напоминаний.\n"
            "Чтобы добавить напоминание, нажмите /reminders.",
            reply_markup=markup
        )
    else:
        reminders_list = "\n\n".join(
            [f"{reminder['text']} | {reminder['time'].strftime('%d.%m.%Y %H:%M')}" for reminder in context.user_data['reminders']]
        )
        await update.message.reply_text(
            f"⏰ Ваши напоминания:\n\n{reminders_list}\n\n"
            "Чтобы добавить новое напоминание, нажмите /reminders.\n"
            "Чтобы вернуться к погоде, нажмите /start.",
            reply_markup=markup
        )
    return SELECTING_ACTION

async def track_progress(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Обрабатывает команду "Прогресс" и предлагает пользователю отметить выполненное задание"""
    await update.message.reply_text(
        "📊 Введите задание, которое вы выполнили (например: 'Посетил спортзал'):",
        reply_markup=ReplyKeyboardRemove()
    )
    return TRACK_PROGRESS

async def save_progress(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Сохраняет прогресс пользователя"""
    user_id = update.effective_user.id
    task = update.message.text

    if 'progress' not in context.user_data:
        context.user_data['progress'] = []

    context.user_data['progress'].append({
        'task': task,
        'date': datetime.now().strftime("%d.%m.%Y %H:%M")
    })

    await update.message.reply_text(
        "✅ Прогресс сохранен!\n\n"
        "Чтобы посмотреть ваш прогресс, нажмите /progress.\n"
        "Чтобы вернуться к погоде, нажмите /start.",
        reply_markup=markup
    )
    return SELECTING_ACTION

async def show_progress(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Показывает прогресс пользователя"""
    user_id = update.effective_user.id

    if 'progress' not in context.user_data or not context.user_data['progress']:
        await update.message.reply_text(
            "📭 У вас пока нет отмеченного прогресса.\n"
            "Чтобы отметить выполненное задание, нажмите /progress.",
            reply_markup=markup
        )
    else:
        progress_list = "\n\n".join(
            [f"{progress['task']} | {progress['date']}" for progress in context.user_data['progress']]
        )
        await update.message.reply_text(
            f"📊 Ваш прогресс:\n\n{progress_list}\n\n"
            "Чтобы отметить новое задание, нажмите /progress.\n"
            "Чтобы вернуться к погоде, нажмите /start.",
            reply_markup=markup
        )
    return SELECTING_ACTION

async def mood(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Обрабатывает команду "Настроение" и предлагает пользователю отметить свое настроение"""
    await update.message.reply_text(
        "😊 Как ваше настроение сегодня? Введите описание (например: 'Отличное', 'Грустно'):",
        reply_markup=ReplyKeyboardRemove()
    )
    return MOOD

async def save_mood(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Сохраняет настроение пользователя"""
    user_id = update.effective_user.id
    mood_text = update.message.text

    if 'moods' not in context.user_data:
        context.user_data['moods'] = []

    context.user_data['moods'].append({
        'mood': mood_text,
        'date': datetime.now().strftime("%d.%m.%Y %H:%M")
    })

    await update.message.reply_text(
        "✅ Настроение сохранено!\n\n"
        "Чтобы посмотреть ваше настроение, нажмите /mood.\n"
        "Чтобы вернуться к погоде, нажмите /start.",
        reply_markup=markup
    )
    return SELECTING_ACTION

async def show_mood(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Показывает настроение пользователя"""
    user_id = update.effective_user.id

    if 'moods' not in context.user_data or not context.user_data['moods']:
        await update.message.reply_text(
            "📭 У вас пока нет отмеченного настроения.\n"
            "Чтобы отметить настроение, нажмите /mood.",
            reply_markup=markup
        )
    else:
        mood_list = "\n\n".join(
            [f"{mood['mood']} | {mood['date']}" for mood in context.user_data['moods']]
        )
        await update.message.reply_text(
            f"😊 Ваше настроение:\n\n{mood_list}\n\n"
            "Чтобы отметить новое настроение, нажмите /mood.\n"
            "Чтобы вернуться к погоде, нажмите /start.",
            reply_markup=markup
        )
    return SELECTING_ACTION

async def book_progress(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Обрабатывает команду "Книги" и предлагает пользователю отметить прогресс в чтении"""
    await update.message.reply_text(
        "📚 Введите название книги и количество прочитанных страниц (например: 'Война и мир | 120'):",
        reply_markup=ReplyKeyboardRemove()
    )
    return BOOK_PROGRESS

async def save_book_progress(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Сохраняет прогресс в чтении книги"""
    user_id = update.effective_user.id
    book_data = update.message.text.split(" | ")

    if len(book_data) != 2:
        await update.message.reply_text(
            "❌ Неверный формат. Попробуйте снова.",
            reply_markup=markup
        )
        return BOOK_PROGRESS

    book_title, pages_read = book_data

    try:
        pages_read = int(pages_read)
    except ValueError:
        await update.message.reply_text(
            "❌ Неверный формат количества страниц. Попробуйте снова.",
            reply_markup=markup
        )
        return BOOK_PROGRESS

    if 'books' not in context.user_data:
        context.user_data['books'] = []

    context.user_data['books'].append({
        'title': book_title,
        'pages': pages_read,
        'date': datetime.now().strftime("%d.%m.%Y %H:%M")
    })

    await update.message.reply_text(
        "✅ Прогресс в чтении сохранен!\n\n"
        "Чтобы посмотреть ваш прогресс в чтении, нажмите /books.\n"
        "Чтобы вернуться к погоде, нажмите /start.",
        reply_markup=markup
    )
    return SELECTING_ACTION

async def show_book_progress(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Показывает прогресс в чтении книг"""
    user_id = update.effective_user.id

    if 'books' not in context.user_data or not context.user_data['books']:
        await update.message.reply_text(
            "📭 У вас пока нет отмеченного прогресса в чтении.\n"
            "Чтобы отметить прогресс, нажмите /books.",
            reply_markup=markup
        )
    else:
        book_list = "\n\n".join(
            [f"{book['title']} | {book['pages']} страниц | {book['date']}" for book in context.user_data['books']]
        )
        await update.message.reply_text(
            f"📚 Ваш прогресс в чтении:\n\n{book_list}\n\n"
            "Чтобы отметить новый прогресс, нажмите /books.\n"
            "Чтобы вернуться к погоде, нажмите /start.",
            reply_markup=markup
        )
    return SELECTING_ACTION

async def back(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Возвращает пользователя на главный экран"""
    await update.message.reply_text(
        "Вы вернулись на главный экран.\n"
        "Чтобы продолжить, выберите действие:",
        reply_markup=markup
    )
    return SELECTING_ACTION

async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Завершает работу с ботом"""
    await update.message.reply_text(
        "До свидания! 👋 Нажмите /start для нового запроса.",
        reply_markup=ReplyKeyboardRemove()
    )
    return ConversationHandler.END

async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Обрабатывает ошибки"""
    logger.error(f"Ошибка: {context.error}")
    if update:
        await update.message.reply_text(
            "😔 Произошла ошибка. Попробуйте /start",
            reply_markup=markup
        )

def main() -> None:
    """Запуск бота"""
    try:
        application = Application.builder().token(BOT_TOKEN).build()  # Закрываем скобку

        conv_handler = ConversationHandler(
            entry_points=[CommandHandler("start", start)],
            states={
                SELECTING_ACTION: [
                    MessageHandler(filters.Regex("^(Ростов-на-Дону 🌊|Москва 🏖️)$"), get_weather),
                    MessageHandler(filters.Regex("^Поиск 🔍$"), get_weather),
                    MessageHandler(filters.Regex("^Заметки 📝$"), notes),
                    MessageHandler(filters.Regex("^Напоминания ⏰$"), set_reminder),
                    MessageHandler(filters.Regex("^Прогресс 📊$"), track_progress),
                    MessageHandler(filters.Regex("^Настроение 😊$"), mood),
                    MessageHandler(filters.Regex("^Книги 📚$"), book_progress),
                    MessageHandler(filters.Regex("^Назад ⬅️$"), back),
                    CommandHandler("notes", show_notes),
                    CommandHandler("reminders", show_reminders),
                    CommandHandler("progress", show_progress),
                    CommandHandler("mood", show_mood),
                    CommandHandler("books", show_book_progress),
                ],
                TYPING_CITY: [
                    MessageHandler(filters.TEXT & ~filters.COMMAND, search_city),
                ],
                TYPING_NOTE: [
                    MessageHandler(filters.TEXT & ~filters.COMMAND, save_note),
                ],
                SET_REMINDER: [
                    MessageHandler(filters.TEXT & ~filters.COMMAND, save_reminder),
                ],
                TRACK_PROGRESS: [
                    MessageHandler(filters.TEXT & ~filters.COMMAND, save_progress),
                ],
                MOOD: [
                    MessageHandler(filters.TEXT & ~filters.COMMAND, save_mood),
                ],
                BOOK_PROGRESS: [
                    MessageHandler(filters.TEXT & ~filters.COMMAND, save_book_progress),
                ],
            },
            fallbacks=[CommandHandler("cancel", cancel)],
        )

        application.add_handler(conv_handler)
        application.add_error_handler(error_handler)

        logger.info("🤖 Бот запущен")
        application.run_polling()

    except Exception as e:
        logger.critical(f"❌ Ошибка запуска: {e}")
    finally:
        logger.info("🛑 Бот остановлен")

if __name__ == "__main__":
    main()

Ошибка в терминале:

  File "C:\Users\Rota-4\Desktop\WeatherBot\weather_bot.py", line 316
    """Обрабатывает введенный пользователем город"""
    ^
IndentationError: expected an indented block after function definition on line 315
PS C:\Users\Rota-4\Desktop\WeatherBot>