привет! вот код моего телеграм бота. ознакомься. в следующем сообщении напишу проблему и попрошу решить, а пока просто ознакомься с кодом и никаких проблем не ищи
import logging
import requests
import json
import os
import re
import html
import sys
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from telegram import (
    Update,
    InlineKeyboardMarkup,
    InlineKeyboardButton,
    InputFile,
    ReplyKeyboardMarkup,
    ReplyKeyboardRemove
)
from telegram.constants import ParseMode
from telegram.ext import (
    ApplicationBuilder,
    CommandHandler,
    MessageHandler,
    ContextTypes,
    filters,
    CallbackQueryHandler,
    Defaults,
)
from telegram.error import TelegramError, BadRequest
import openai
import asyncio
import aiohttp 
from PIL import Image
import io
from io import BytesIO  
# Функция для загрузки конфигурации
def load_config():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(script_dir, 'config.json')

    if not os.path.exists(config_path):
        raise FileNotFoundError(f"файл config.json не найден по пути: {config_path}")

    with open(config_path, 'r', encoding='utf-8') as f:
        return json.load(f)

# Функция для сохранения контекста чата в файл
def save_chat_context(user_id, model_id, chat_slot, context):
    chats_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chats')
    if not os.path.exists(chats_dir):
        os.makedirs(chats_dir)

    filename = f"{user_id}_{model_id}_chat{chat_slot}.json"
    file_path = os.path.join(chats_dir, filename)

    try:
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(context, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"ошибка при сохранении контекста чата: {e}")

# Функция для загрузки контекста чата из файла
def load_chat_context(user_id, model_id, chat_slot, system_instruction):
    chats_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chats')
    filename = f"{user_id}_{model_id}_chat{chat_slot}.json"
    file_path = os.path.join(chats_dir, filename)

    if not os.path.exists(file_path):
        return [{"role": "system", "content": system_instruction}]

    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception as e:
        logger.error(f"ошибка при загрузке контекста чата: {e}")
        return [{"role": "system", "content": system_instruction}]

# Функция для загрузки и сохранения использования
def load_usage():
    usage_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'usage.json')
    if not os.path.exists(usage_path):
        return {}
    try:
        with open(usage_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception as e:
        logger.error(f"ошибка при загрузке usage.json: {e}")
        return {}

def save_usage():
    usage_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'usage.json')
    try:
        with open(usage_path, 'w', encoding='utf-8') as f:
            json.dump(usage_store, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"ошибка при сохранении usage.json: {e}")

def check_and_increment_limit(user_id, model_id):
    model_info = model_id_to_info.get(model_id, {})
    provider = model_info.get('provider', 'cablyai')

    # Общий лимит для CablyAI
    if provider == 'cablyai':
        global_usage = usage_store.get('cablyai_total_tokens', 0)
        if global_usage + 1 > config['limits']['cablyai']['daily_token_limit']:
            return False
        usage_store['cablyai_total_tokens'] = global_usage + 1  # Увеличиваем на 1 токен за запрос
        save_usage()
        return True

    # Helix лимиты
    elif provider == 'helix':
        helix_usage = usage_store.get('helix_requests', 0)
        if helix_usage >= config['limits']['helix']['daily_requests']:
            return False
        usage_store['helix_requests'] = helix_usage + 1
        save_usage()
        return True

    # MeowAI лимиты
    elif provider == 'meowai':
        meowai_usage = usage_store.get('meowai_requests', 0)
        if meowai_usage >= config['limits']['meowai']['total_requests']:
            return False
        usage_store['meowai_requests'] = meowai_usage + 1
        save_usage()
        return True

    # OpenAI и другие провайдеры (лимиты остаются без изменений)
    else:
        return True

# Функция для преобразования Markdown в HTML
def markdown_to_html(text):
    """
    Простой конвертер Markdown в HTML.
    Обрабатывает основные элементы Markdown.
    """
    # Экранирование HTML-символов
    text = html.escape(text)

    # Замена **жирный** на <b>жирный</b>
    text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', text)

    # Замена *курсив* на <i>курсив</i>
    text = re.sub(r'\*(.*?)\*', r'<i>\1</i>', text)

    # Замена __подчёркивание__ на <u>подчёркивание</u>
    text = re.sub(r'__(.*?)__', r'<u>\1</u>', text)

    # Замена ~~зачёркивание~~ на 
зачёркивание

    text = re.sub(r'~~(.*?)~~', r'
\1
', text)

    # Обработка заголовков
    text = re.sub(r'###### (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'##### (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'#### (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'### (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'## (.*?)\n', r'<b>\1</b>\n', text)
    text = re.sub(r'# (.*?)\n', r'<b>\1</b>\n', text)

    return text

# Функция для сохранения ответа в .md файл
def save_md_file(content, user_id):
    """
    Сохраняет содержимое в .md файл с уникальным именем.
    Возвращает путь к файлу.
    """
    if not os.path.exists('responses'):
        os.makedirs('responses')

    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    filename = f"responses/{user_id}_{timestamp}.md"

    try:
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(content)
        return filename
    except Exception as e:
        logger.error(f"Ошибка при сохранении файла .md: {e}")
        return None

# Функция для проверки, поддерживает ли модель обработку изображений
def is_image_supported(model_id):
    supported_models = [
        'openai-chatgpt-4o-latest',
        'openai-gpt-4-turbo',
        'openai-gpt-4o-mini',
        'chatgpt-4o-latest',
        'gpt-4-turbo',
        'gpt-4o-mini',
        "FLUX.1.1 [pro]",
        "flux.1.1-pro-ultra",
        "sd3.5",
        "stable-diffusion-3.5-turbo"
    ]
    return model_id in supported_models

# Функция для получения корректного идентификатора модели для API
def get_api_model_id(model_id, provider):
    if provider == 'openai' and model_id.startswith('openai-'):
        return model_id[len('openai-'):]
    elif provider == 'helix' and model_id.startswith('helix-'):
        return model_id[len('helix-'):]
    elif provider == 'meowai' and model_id.startswith('meowai-'):
        return model_id[len('meowai-'):]
    return model_id

# Функция для отправки запроса через CablyAI API
async def send_cablyai_request(model_id, messages=None, prompt=None, model_type='text'):
    provider_info = config['providers'].get('cablyai', {})
    api_url_text = provider_info.get('api_endpoint_text')
    api_url_image = provider_info.get('api_endpoint_image')
    api_key = provider_info.get('api_key')

    if not api_url_text or not api_key:
        logger.error("CablyAI API настройки отсутствуют в config.json.")
        return None

    headers = {
        'Authorization': f"Bearer {api_key}",
        'Content-Type': 'application/json'
    }

    try:
        if model_type in ['text', 'multimodal']:
            data = {
                "model": model_id,
                "messages": messages
            }
            url = api_url_text
        elif model_type == 'image':
            data = {
                "model": model_id,
                "prompt": prompt,
                "n": 1,
                "size": "1024x1024"  # Можно адаптировать под нужный размер
            }
            url = api_url_image
        else:
            logger.error(f"Неизвестный тип модели: {model_type}")
            return None

        async with aiohttp.ClientSession() as session:
            async with session.post(url, headers=headers, json=data) as response:
                if response.status == [200, 201]:
                    return await response.json()
                else:
                    error_text = await response.text()
                    logger.error(f"Ошибка запроса к CablyAI API: {response.status} - {error_text}")
                    return None
    except Exception as e:
        logger.error(f"Ошибка при запросе к CablyAI API: {e}")
        return None

# Асинхронная функция для перевода текста с русского на английский с помощью модели CablyAI
async def translate_to_english(original_text):
    translation_prompt = f"переведи следующий текст на английский язык и верни только переведенный текст:\n\n{original_text}"
    
    translation_messages = [
        {"role": "system", "content": "you are a helpful assistant that translates russian text to english."},
        {"role": "user", "content": translation_prompt}
    ]
    
    try:
        response = await send_cablyai_request("chatgpt-4o-latest", messages=translation_messages, model_type='multimodal')
        if response and "choices" in response and len(response["choices"]) > 0:
            translated_text = response["choices"][0].get('message', {}).get('content', '').strip()
            return translated_text
        else:
            logger.error("неверная структура ответа от модели при переводе.")
            return None
    except Exception as e:
        logger.error(f"ошибка при переводе текста: {e}")
        return None

# Функция для генерации изображений через CablyAI
async def generate_image_cablyai(model_id, prompt):
    """
    Генерирует изображение через CablyAI на основе предоставленного промпта.
    Возвращает URL сгенерированного изображения или None в случае ошибки.
    """
    response_data = await send_cablyai_request(model_id, prompt=prompt, model_type='image')
    if response_data and 'data' in response_data and len(response_data['data']) > 0:
        image_url = response_data['data'][0].get('url')
        return image_url
    else:
        logger.error("Не удалось сгенерировать изображение через CablyAI.")
        return None

# Функция для обработки изображений через соответствующий API
async def process_image_request(user_id, model_id, prompt, image_path=None):
    model_info = model_id_to_info.get(model_id, {})
    provider = model_info.get('provider', 'cablyai')
    
    if provider == 'cablyai':
        # Генерация изображения через CablyAI
        image_url = await generate_image_cablyai(model_id, prompt)
        return image_url
    elif provider in ['helix', 'meowai']:
        # Обработка через Helix или MeowAI
        provider_info = config['providers'].get(provider, {})
        api_url = provider_info.get('api_endpoint')
        api_key = provider_info.get('api_key')
        if not api_url or not api_key:
            logger.error(f"{provider.capitalize()} API настройки отсутствуют в config.json.")
            return None

        headers = {
            'Authorization': f"Bearer {api_key}",
            'Content-Type': 'application/json'
        }

        data = {
            "model": model_id,
            "messages": [{"role": "user", "content": prompt}]
        }

        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(api_url, headers=headers, json=data) as response:
                    if response.status == 200:
                        response_data = await response.json()
                        if "choices" in response_data and len(response_data["choices"]) > 0:
                            return response_data["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
                    else:
                        error_text = await response.text()
                        logger.error(f"Ошибка запроса к {provider.capitalize()} API: {response.status} - {error_text}")
                        return None
        except Exception as e:
            logger.error(f"Ошибка при запросе к {provider.capitalize()} API: {e}")
            return None
    elif provider == 'openai':
        # Обработка изображения через OpenAI
        try:
            if image_path:
                # Конвертация изображения в PNG и проверка размера
                with Image.open(image_path) as img:
                    img = img.convert("RGBA")
                    with io.BytesIO() as output:
                        img.save(output, format="PNG")
                        png_data = output.getvalue()
                
                if len(png_data) > 4 * 1024 * 1024:
                    # Если размер превышает 4MB, уменьшить качество или размер
                    img.thumbnail((1024, 1024))
                    with io.BytesIO() as output:
                        img.save(output, format="PNG", optimize=True)
                        png_data = output.getvalue()
                    if len(png_data) > 4 * 1024 * 1024:
                        logger.error("Размер изображения после конвертации превышает 4MB.")
                        return None

                # Сохранение конвертированного изображения
                with open(image_path, 'wb') as f:
                    f.write(png_data)
                
                # Использование OpenAI API для обработки изображения
                with open(image_path, 'rb') as image_file:
                    response = openai.Image.create_edit(
                        image=image_file,
                        prompt=prompt,
                        n=1,
                        size="1024x1024"
                    )
                # Извлечение URL сгенерированного изображения
                image_url = response['data'][0]['url']
                return image_url
            else:
                # Если изображение не предоставлено, обрабатываем только текст
                response = openai.ChatCompletion.create(
                    model=get_api_model_id(model_id, provider),
                    messages=[
                        {"role": "system", "content": "You are a helpful assistant."},
                        {"role": "user", "content": prompt}
                    ]
                )
                answer = response['choices'][0]['message']['content']
                return answer
        except openai.error.OpenAIError as e:
            logger.error(f"Ошибка OpenAI API при обработке изображения: {e}")
            return None
    elif provider == 'anthropic':
        # Пример обработки изображений для Anthropic моделей (запросите документацию провайдера)
        logger.warning(f"Обработка изображений для провайдера Anthropic модели {model_id} не реализована.")
        return "Обработка изображений для этой модели не поддерживается."
    else:
        logger.error(f"Неизвестный провайдер модели: {provider}")
        return None

# Функция для отправки запроса через OpenAI API
async def send_openai_request(model_id, messages=None, prompt=None, image_path=None):
    api_model_id = get_api_model_id(model_id, 'openai')
    try:
        if 'gpt-4' in api_model_id or 'chatgpt' in api_model_id or 'o1' in api_model_id:
            if image_path:
                # Обработка изображения через OpenAI
                try:
                    with open(image_path, 'rb') as image_file:
                        response = openai.Image.create_edit(
                            image=image_file,
                            prompt=messages[-1]['content'],  # Последнее сообщение пользователя как промпт
                            n=1,
                            size="1024x1024"
                        )
                    # Извлечение URL сгенерированного изображения
                    image_url = response['data'][0]['url']
                    return image_url
                except openai.error.OpenAIError as e:
                    logger.error(f"Ошибка OpenAI API при обработке изображения: {e}")
                    return None
            else:
                response = openai.ChatCompletion.create(
                    model=api_model_id,
                    messages=messages
                )
                return response
        else:
            # Обработка других моделей, если необходимо
            logger.error(f"Модель {model_id} не поддерживается OpenAI API.")
            return None
    except openai.error.OpenAIError as e:
        logger.error(f"Ошибка OpenAI API: {e}")
        return None

# Функция для добавления нового пользователя (админ-команда)
async def add_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """
    Админ-команда для добавления нового пользователя по Telegram ID.
    Использование: /adduser <telegram_id>
    """
    admin_id = 5005816138
    user_id = update.effective_user.id
    if user_id != admin_id:
        await update.message.reply_text("У вас нет прав для использования этой команды.")
        return

    if len(context.args) != 1:
        await update.message.reply_text("Использование: /adduser <telegram_id>")
        return

    try:
        new_user_id = int(context.args[0])
    except ValueError:
        await update.message.reply_text("Неверный формат Telegram ID. Он должен быть числом.")
        return

    if new_user_id in AUTHORIZED_USER_IDS:
        await update.message.reply_text("Этот пользователь уже авторизован.")
        return

    AUTHORIZED_USER_IDS.add(new_user_id)
    # Копирование лимитов от пользователя с ID 5075150336
    source_limits = config.get('user_limits', {}).get(str(5075150336), {})
    config['user_limits'][str(new_user_id)] = source_limits.copy()
    save_config()

    await update.message.reply_text(f"Пользователь {new_user_id} успешно добавлен.")
    try:
        await context.bot.send_message(
            chat_id=new_user_id,
            text="Вас добавили в список пользователей, имеющих доступ к боту."
        )
    except Exception as e:
        logger.error(f"Не удалось отправить сообщение новому пользователю {new_user_id}: {e}")

# Функция для сохранения обновленной конфигурации
def save_config():
    config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')
    try:
        with open(config_path, 'w', encoding='utf-8') as f:
            json.dump(config, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"Ошибка при сохранении config.json: {e}")

# Загрузка конфигурации
try:
    config = load_config()
except FileNotFoundError as e:
    print(e)
    exit(1)
except json.JSONDecodeError as e:
    print(f"Ошибка разбора config.json: {e}")
    exit(1)

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

# Настройка OpenAI API
openai_api_key = config.get('openai_api_key')
if not openai_api_key:
    logger.error("OpenAI API ключ не указан в config.json.")
    exit(1)
openai.api_key = openai_api_key

API_HEADERS = {
    'Content-Type': 'application/json'
}

# Инструкция для текстовых моделей
SYSTEM_INSTRUCTION_TEXT = config.get('system_instruction', "ты ведёшь себя как хороший ассистент, готовый ответить на любой вопрос.")

# Структуры для хранения контекста и активных чатов
context_store = {}
user_state = {}

# Загрузка использования
usage_store = load_usage()

# Конфигурация лимитов
if 'limits' not in config:
    config['limits'] = {
        "cablyai": {
            "daily_token_limit": 2000000,
            "image_daily_limit": 10000
        },
        "helix": {
            "daily_requests": 50
        },
        "meowai": {
            "total_requests": 960
        },
        # Добавьте лимиты для других провайдеров, если необходимо
    }

MAX_MESSAGE_LENGTH = 4096

# Преобразуем список авторизованных пользователей в множество для быстрого поиска
AUTHORIZED_USER_IDS = set(map(int, config.get('authorized_user_ids', [])))

# Хранение режима пользователя ('message' или 'file')
user_mode_store = {}
user_modes_file = 'user_modes.json'

# Функция для загрузки режимов пользователей из файла
def load_user_modes():
    if os.path.exists(user_modes_file):
        try:
            with open(user_modes_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception as e:
            logger.error(f"Ошибка при загрузке режимов пользователей: {e}")
            return {}
    else:
        return {}

# Функция для сохранения режимов пользователей в файл
def save_user_modes():
    try:
        with open(user_modes_file, 'w', encoding='utf-8') as f:
            json.dump(user_mode_store, f, ensure_ascii=False, indent=4)
    except Exception as e:
        logger.error(f"Ошибка при сохранении режимов пользователей: {e}")

# Загрузка режимов пользователей при запуске бота
user_mode_store = load_user_modes()

# Создание маппинга model_id -> model_info и model_id -> model_name для быстрого доступа
model_id_to_info = {}
model_id_to_name = {}
for model_name, model_info in config.get('models', {}).items():
    model_id = model_info.get('id')
    if model_id:
        model_id_to_info[model_id] = model_info
        model_id_to_name[model_id] = model_name.lower()
    else:
        logger.warning(f"Модель '{model_name}' не имеет 'id'.")

# Список ограниченных моделей
RESTRICTED_MODELS = ['o1-preview', 'o1-mini', 'openai-o1-preview', 'openai-o1-mini']
AUTHORIZED_MODEL_USERS = {5005816138, 1759650343, 7856892361, 488179251, 1110920711}

# Описания моделей
MODEL_DESCRIPTIONS = {
    "claude-3.5-sonnet-20241022": "передовая модель Anthropic. быстрая, в задачах с кодом в чём-то лучше даже OpenAI o1-mini и preview. актуальность знаний: до апреля 2024. контекстное окно: 200000 токенов. выплюнуть за раз может: 5000 символов ( символ не равно токен ).",
    "claude-3-opus": "бывшая передовая модель Anthropic. медленная, но очень хороша в генерации текстов и удивляет своими познаниями русского языка. актуальность знаний: до августа 2023. контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "claude-3.5-haiku": "почти передовая модель Anthropic. совсем новая, быстрая, лучше чем предшественник Claude 3 Haiku. её учили хорошо писать код! актуальность знаний: неизвестна. контекстное окно: 200000 токенов. выплюнуть за раз может: 4096 токенов.",
    "chatgpt-4o-latest": "новейшая версия модели GPT-4o за, вроде, сентябрь 2024, созданная OpenAI. быстрая, хороша для широкого спектра задач. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "gpt-4-turbo": "бывшая передовая модель OpenAI. иногда в чём-то превосходит даже GPT-4o-latest, но уступает в скорости и сильно дороже. актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 4096 токенов.",
    "gpt-4o-mini": "уменьшенная версия GPT-4o. очень быстрая! но по качеству ответов уступает. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена.",
    "o1-preview": "передовая модель OpenAI. использует цепочку рассуждений для ответов, что повышает их точность и размер, но генерирует ответ довольно долго относительно других моделей. в точных науках и коде показывает себя лучше любых моделей. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 65536 токенов.",
    "o1-mini": "уменьшенная версия модели o1 от OpenAI. тоже использует цепочку рассуждений, но думает намного быстрее. может уступать o1-preview в качестве ответов, но это компенсируется её на 80% более низкой ценой (для тех кто платит за неё) и быстротой ответов. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 32768 токенов.",
    "llama-3.1-405b-turbo": "текстовая модель. сопоставима по качеству ответов с остальными моделями, но может уступать, и выдавать всякие иероглифы посреди ответа. но почему бы и не добавить? актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: неизвестно. ",
    "FLUX.1.1 [pro]": "лучшая существующая модель генерации изображений. некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "flux.1.1-pro-ultra": "новая версия лучшей существующей модели генерации изображений! некоторые её изображения неотличимы от реальных! но она нестабильна. но ты всегда можешь отправить новый запрос! контекста не имеет, работая лишь с текущим заданным запросом.",
    "sd3.5": "модель для генерации изображений по текстовому описанию. контекста не имеет, работая лишь с текущим заданным запросом.",
    "stable-diffusion-3.5-turbo": "более новая и быстрая модель для генерации изображений по текстовому описанию. контекста не имеет, работая лишь с текущим заданным запросом.",
    "openai-chatgpt-4o-latest": "новейшая версия модели GPT-4o за, вроде, сентябрь 2024, созданная OpenAI. быстрая, хороша для широкого спектра задач. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI.",
    "openai-gpt-4-turbo": "бывшая передовая модель OpenAI. иногда в чём-то превосходит даже GPT-4o-latest, но уступает в скорости. актуальность знаний: до декабря 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 4096 токенов. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI.",
    "openai-gpt-4o-mini": "уменьшенная версия GPT-4o. очень быстрая! но по качеству ответов уступает. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 16384 токена. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI.",
    "openai-o1-preview": "передовая модель OpenAI. использует цепочку рассуждений для ответов, что повышает их точность и размер, но генерирует ответ довольно долго относительно других моделей. в точных науках и коде показывает себя лучше любых моделей. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 32768 токенов. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI.",
    "openai-o1-mini": "уменьшенная версия модели o1 от OpenAI. тоже использует цепочку рассуждений, но думает намного быстрее. может уступать o1-preview в качестве ответов, но это компенсируется её на 80% более низкой ценой (для тех кто платит за неё) и быстротой ответов. актуальность знаний: до октября 2023. контекстное окно: 128000 токенов. выплюнуть за раз может: 65536 токенов. отличия от подобной модели выше: поставляется с помощью ключа api OpenAI."
}

# Функция для разбивки длинного сообщения на части
def split_message(message, max_length):
    return [message[i:i + max_length] for i in range(0, len(message), max_length)]

# Функция для создания кнопок выбора провайдера (постоянная клавиатура)
def get_provider_buttons():
    keyboard = [
        ['CatAI', 'OpenAI'],
        ['Helix', 'MeowAI']
    ]
    reply_markup = ReplyKeyboardMarkup(
        keyboard, 
        resize_keyboard=True, 
        one_time_keyboard=False
    )
    return reply_markup

# Функция для создания кнопок выбора модели по провайдеру
def get_model_buttons_by_provider(provider):
    buttons = []
    for name, model_info in config.get('models', {}).items():
        model_provider = model_info.get('provider', 'cablyai')
        if provider.lower() == model_provider.lower():
            button = InlineKeyboardButton(html.escape(name), callback_data=f"model_select:{model_info['id']}")
            buttons.append([button])
    return InlineKeyboardMarkup(buttons)

# Функция для создания кнопок выбора слота чата
def get_chat_slot_buttons(model_id):
    keyboard = [
        [
            InlineKeyboardButton("чат 1", callback_data=f"chat_slot:1:{model_id}"),
            InlineKeyboardButton("чат 2", callback_data=f"chat_slot:2:{model_id}")
        ]
    ]
    return InlineKeyboardMarkup(keyboard)

# Функция для создания основных кнопок: 'сменить модель', 'выбрать чат', 'очистить контекст', 'продолжить'
def get_main_buttons(user_id, model_id=None):
    buttons = []
    if model_id:
        buttons.append([
            InlineKeyboardButton("сменить модель", callback_data='change_model'),
            InlineKeyboardButton("выбрать чат", callback_data='change_chat_slot'),
            InlineKeyboardButton("очистить контекст", callback_data='clear_context')
        ])
        # Добавляем кнопку "Продолжить" на отдельный ряд
        buttons.append([
            InlineKeyboardButton("продолжить", callback_data='continue')
        ])
    else:
        buttons.append([
            InlineKeyboardButton("сменить модель", callback_data='change_model')
        ])
    return InlineKeyboardMarkup(buttons)

# Обработчик команды /models
async def models_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Отправляет пользователю список доступных моделей с описаниями.
    Разбивает вывод на части по 10 моделей для соответствия лимитам Telegram.
    """
    user_id = update.effective_user.id
    models = config.get('models', {})
    if not models:
        await update.message.reply_text('Нет доступных моделей.', reply_markup=get_main_buttons(user_id=user_id))
        return

    descriptions = []
    for name, info in models.items():
        model_id = info.get('id')
        model_name = name.lower()
        model_type = info.get('type', 'неизвестный тип').lower()
        description = "Описание недоступно."  # MODEL_DESCRIPTIONS.get(model_id, "Описание недоступно.")
        descriptions.append(f"- <b>{html.escape(model_name)}</b>: {html.escape(description)}")

    # Разбить на чанки по 10 моделей
    chunk_size = 10
    for i in range(0, len(descriptions), chunk_size):
        chunk = descriptions[i:i+chunk_size]
        message = "<b>Описание доступных моделей:</b>\n\n" + "\n".join(chunk)
        await update.message.reply_text(message, parse_mode=ParseMode.HTML)

# Обработчик команды /instructions
async def instructions(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Отправляет пользователю инструкции по использованию бота.
    """
    menu_text = (
        "<b>инструкция по использованию бота:</b>\n\n"
        "<b>/start</b> - запустить бота и выбрать модель для взаимодействия.\n"
        "<b>/setmode [message|file]</b> - выбрать режим получения ответов (message - текстовые сообщения, file - .md файлы, они нужны для того чтобы получить полный ответ модели без ограничения длины сообщения телеграма).\n"
        "<b>/mode</b> - узнать текущий выбранный режим.\n"
        "<b>/instructions</b> - получить инструкции по использованию бота.\n"
        "<b>/models</b> - получить описание доступных моделей.\n"
        "<b>/limits</b> - посмотреть ваши текущие лимиты и оставшиеся запросы на сегодня.\n\n"
        "также доступно использование кнопок ниже:\n"
        "<b>сменить модель</b> - выбрать другую модель для общения.\n"
        "<b>продолжить</b> - продолжить ответ модели.\n"
        "‼️<b>важно:</b> токен не равно символ. все модели воспринимают токен как единицу измерения по разному, будь то слово, кусок слова или символ. если про модель указано что она отвечает определённым количеством символов - так и есть.\n"
        "‼️<b>лимиты бота:</b> если у тебя есть доступ к этому боту - лимитов никаких нет кроме как на o1 модели (если тебе к ним открыт доступ). админ бота не гарантирует, что бот сможет работать бесконечно, всё зависит от провайдера API ключей.\n"
        "‼️<b>контекст:</b> в боте доступны чаты - 2 на каждую модель. ты можешь очистить контекст определённого чата, если надо."
    )

    await update.message.reply_text(menu_text, parse_mode=ParseMode.HTML)

# Обработчик команды /setmode
async def setmode(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Позволяет пользователю выбрать режим получения ответов:
    - message: получение текстовых сообщений
    - file: получение ответов в .md файлах
    """
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать /setmode: {user_id}")
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    if len(context.args) != 1:
        await update.message.reply_text('Использование: /setmode [message|file]')
        return

    mode = context.args[0].lower()
    if mode not in ['message', 'file']:
        await update.message.reply_text('Неверный режим. К команде надо дописать "message" или "file".')
        return

    user_mode_store[str(user_id)] = mode  # Сохраняем как строку для сериализации в JSON
    save_user_modes()
    await update.message.reply_text(f'Режим получения ответов установлен на: <b>{mode}</b>', parse_mode=ParseMode.HTML)

# Обработчик команды /mode
async def mode_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Позволяет пользователю узнать текущий режим получения ответов.
    """
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать /mode: {user_id}")
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    mode = user_mode_store.get(str(user_id), 'message')  # По умолчанию 'message'
    await update.message.reply_text(f'Текущий режим получения ответов: <b>{mode}</b>', parse_mode=ParseMode.HTML)

# Обработчик команды /limits
async def limits_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Выводит пользователю информацию о его лимитах и оставшихся запросах на сегодня.
    """
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("У вас нет доступа к этому боту.")
        return

    user_id_str = str(user_id)
    user_limits = config.get('user_limits', {}).get(user_id_str, {})
    if not user_limits:
        await update.message.reply_text("У вас нет установленных лимитов.", reply_markup=get_main_buttons(user_id))
        return

    response_lines = ["<b>Ваши лимиты:</b>"]

    # CatAI лимиты
    catai_tokens = usage_store.get('cablyai_tokens', 0)
    catai_remaining = config['limits']['cablyai']['daily_token_limit'] - catai_tokens
    response_lines.append(f"\n<b>CatAI:</b>")
    response_lines.append(f"- Токены: {catai_tokens} / {config['limits']['cablyai']['daily_token_limit']} (Осталось: {catai_remaining})")
    response_lines.append(f"- Изображения: {usage_store.get('cablyai_images', 0)} / {config['limits']['cablyai']['image_daily_limit']} (Осталось: {config['limits']['cablyai']['image_daily_limit'] - usage_store.get('cablyai_images', 0)})")

    # Helix лимиты
    helix_requests = usage_store.get('helix_requests', 0)
    helix_remaining = config['limits']['helix']['daily_requests'] - helix_requests
    response_lines.append(f"\n<b>Helix:</b>")
    response_lines.append(f"- Запросы: {helix_requests} / {config['limits']['helix']['daily_requests']} (Осталось: {helix_remaining})")

    # MeowAI лимиты
    meowai_requests = usage_store.get('meowai_requests', 0)
    meowai_remaining = config['limits']['meowai']['total_requests'] - meowai_requests
    response_lines.append(f"\n<b>MeowAI:</b>")
    response_lines.append(f"- Запросы: {meowai_requests} / {config['limits']['meowai']['total_requests']} (Осталось: {meowai_remaining})")

    # OpenAI лимиты (оставшиеся без изменений)
    response_lines.append(f"\n<b>OpenAI:</b>")
    for model_id, limit in user_limits.items():
        if model_id.startswith('openai-'):
            usage = usage_store.get(user_id_str, {}).get(model_id, 0)
            if limit == "infinite":
                remaining = "∞"
            else:
                remaining = limit - usage
                if remaining < 0:
                    remaining = 0
            model_name = model_id_to_name.get(model_id, model_id)
            response_lines.append(f"- <b>{html.escape(model_name)}</b>: {usage} / {limit} запросов (Осталось: {remaining})")

    # Состояние CatAI токенов для блокировки
    if config['limits']['cablyai']['daily_token_limit'] - usage_store.get('cablyai_tokens', 0) < 100000:
        response_lines.append("\n⚠️ <b>Внимание:</b> Осталось менее 100,000 токенов для CatAI. Доступ к моделям, расходующим токены (multimodal), закрыт для всех пользователей кроме админа.")

    response_text = "\n".join(response_lines)
    response_text += f"\n\n<code>Лимиты сбросятся в {config.get('limit_reset_time', '00:00')} по московскому времени.</code>"

    await update.message.reply_text(response_text, parse_mode=ParseMode.HTML)

# Обработчик команды /start
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать /start: {user_id}")
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    # Инициализация структуры данных для пользователя
    if user_id not in context_store:
        context_store[user_id] = {}
    if user_id not in user_state:
        user_state[user_id] = {'current_model_id': None, 'current_chat_slot': None, 'current_provider': None}

    # Кнопки для выбора провайдера
    await update.message.reply_text(
        'Привет! Выбери провайдера для общения:',
        reply_markup=get_provider_buttons()
    )

# Обработчик команды /reload
async def reload_bot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Перезагружает бота, обновляя конфигурации.
    """
    user_id = update.effective_user.id
    admin_ids = {5005816138} # ID администраторов

    if user_id not in admin_ids:
        await update.message.reply_text('Ты не админ и тебе такое нельзя.')
        return

    await update.message.reply_text('Перезагружаю конфигурацию бота...')
    logger.info("Перезагрузка конфигурации бота по команде /reload")

    try:
        global config, model_id_to_info, model_id_to_name, AUTHORIZED_USER_IDS, RESTRICTED_MODELS, AUTHORIZED_MODEL_USERS, SYSTEM_INSTRUCTION_TEXT, user_limits
        config = load_config()

        # Обновление ключевых переменных
        AUTHORIZED_USER_IDS = set(map(int, config.get('authorized_user_ids', [])))

        model_id_to_info.clear()
        model_id_to_name.clear()
        for model_name, model_info in config.get('models', {}).items():
            model_id = model_info.get('id')
            if model_id:
                model_id_to_info[model_id] = model_info
                model_id_to_name[model_id] = model_name.lower()
            else:
                logger.warning(f"Модель '{model_name}' не имеет 'id'.")

        RESTRICTED_MODELS = ['o1-preview', 'o1-mini', 'openai-o1-preview', 'openai-o1-mini']
        AUTHORIZED_MODEL_USERS = {5005816138, 1759650343, 7856892361, 488179251}  # Обновите при необходимости

        SYSTEM_INSTRUCTION_TEXT = config.get('system_instruction', "ты ведёшь себя как хороший ассистент, готовый ответить на любой вопрос.")

        # Перезагрузка user_limits и usage_store
        usage_store.clear()
        usage_store.update(load_usage())

        # Загрузка режимов пользователей
        user_mode_store.clear()
        user_mode_store.update(load_user_modes())

        await update.message.reply_text('Конфигурация успешно перезагружена.')
        logger.info("Конфигурация бота успешно перезагружена.")
    except Exception as e:
        logger.error(f"Ошибка при перезагрузке конфигурации: {e}")
        await update.message.reply_text('Произошла ошибка при перезагрузке конфигурации.')

# Обработчик выбора провайдера через кнопки (ReplyKeyboard)
DISPLAY_TO_INTERNAL_PROVIDERS = {
    'catai': 'cablyai',
    'openai': 'openai',
    'helix': 'helix',
    'meowai': 'meowai'
}

async def handle_provider_selection(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("У вас нет доступа к этому боту.")
        return

    provider_display = update.message.text
    internal_provider = DISPLAY_TO_INTERNAL_PROVIDERS.get(provider_display.lower())

    if not internal_provider:
        await update.message.reply_text("Некорректный провайдер.")
        return

    # Устанавливаем текущий провайдер
    user_state[user_id]['current_provider'] = internal_provider
    user_state[user_id]['current_model_id'] = None
    user_state[user_id]['current_chat_slot'] = None

    # Предлагаем выбрать модель
    await update.message.reply_text(
        f'Выбран провайдер: <b>{html.escape(provider_display)}</b>\n\nВыберите модель:',
        parse_mode=ParseMode.HTML,
        reply_markup=get_model_buttons_by_provider(internal_provider)
    )

# Обработчик выбора модели через кнопки
async def select_model(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался выбрать модель: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    selected_model_data = query.data

    if selected_model_data.startswith("model_select:"):
        model_id = selected_model_data.split("model_select:")[1]
    else:
        logger.error(f"Неправильный формат данных callback: {selected_model_data}")
        await query.edit_message_text('Произошла ошибка при выборе модели.')
        return

    model_info = model_id_to_info.get(model_id)

    if not model_info:
        logger.error(f"Выбранная модель не найдена в конфигурации: {model_id}")
        await query.edit_message_text('Выбранная модель не найдена в конфигурации.')
        return

    # Проверка и инкремент лимита
    if not check_and_increment_limit(user_id, model_id):
        await query.edit_message_text('Вы достигли лимита запросов для этой модели.')
        return

    # Обновляем текущую модель пользователя
    user_state[user_id]['current_model_id'] = model_id

    # Предлагаем выбрать слот чата (1 или 2)
    await query.edit_message_text(
        f'Выбрана модель: <b>{html.escape(model_id_to_name.get(model_id, "Неизвестная модель"))}</b>\n\nВыберите слот чата:',
        parse_mode=ParseMode.HTML,
        reply_markup=get_chat_slot_buttons(model_id)
    )

async def download_image(url):
    """
    Скачивает изображение по указанному URL и возвращает его байты.
    """
    try:
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                if response.status == 200:
                    return await response.read()
                else:
                    logger.error(f"Не удалось скачать изображение. HTTP статус: {response.status}")
                    return None
    except Exception as e:
        logger.error(f"Ошибка при скачивании изображения: {e}")
        return None
    
# Обработчик выбора слота чата
async def select_chat_slot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    data = query.data  # Формат: "chat_slot:{slot}:{model_id}"
    try:
        _, slot_str, model_id = data.split(":")
        chat_slot = int(slot_str)
    except ValueError:
        logger.error(f"Некорректные данные callback для выбора чата: {data}")
        await query.edit_message_text('Произошла ошибка при выборе чата.')
        return

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался выбрать чат: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    # Обновляем текущий слот чата пользователя
    user_state[user_id]['current_chat_slot'] = chat_slot
    user_state[user_id]['current_model_id'] = model_id

    # Загружаем контекст чата
    model_info = model_id_to_info.get(model_id, {})
    provider = model_info.get('provider', 'cablyai')
    system_instruction = SYSTEM_INSTRUCTION_TEXT
    if provider == 'openai' and model_id not in ['openai-o1-preview', 'openai-o1-mini']:
        system_instruction = "You are a helpful assistant."

    context_store.setdefault(user_id, {}).setdefault(model_id, {})
    if chat_slot not in context_store[user_id][model_id]:
        context_store[user_id][model_id][chat_slot] = load_chat_context(user_id, model_id, chat_slot, system_instruction)
    else:
        # Если контекст уже загружен, ничего не делаем
        pass

    model_name = html.escape(model_id_to_name.get(model_id, "Неизвестная модель"))
    await query.edit_message_text(
        f'Выбран <b>Чат {chat_slot}</b> для модели <b>{model_name}</b>',
        parse_mode=ParseMode.HTML,
        reply_markup=get_main_buttons(user_id, model_id)
    )

# Обработчик кнопки "сменить модель"
async def change_model_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = query.from_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь пытался сменить модель: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    # Получаем текущую модель для логирования
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    logger.info(f"Пользователь {user_id} меняет модель. Текущая модель: {current_model_id}")

    # Обновляем состояние пользователя
    user_state[user_id]['current_model_id'] = None
    user_state[user_id]['current_chat_slot'] = None
    user_state[user_id]['current_provider'] = None

    # Очищаем текущий контекст
    if current_model_id and current_model_id in context_store[user_id]:
        context_store[user_id][current_model_id] = {}

    # Отправляем новый сообщение с выбором провайдера вместо редактирования старого
    try:
        await query.message.reply_text('Выбери провайдера для взаимодействия:', reply_markup=get_provider_buttons())
        await query.message.delete()
    except Exception as e:
        logger.error(f"Ошибка при отображении кнопок выбора провайдера: {e}")
        await query.edit_message_text('Произошла ошибка при смене модели. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))

# Обработчик кнопки "выбрать чат"
async def change_chat_slot_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался выбрать чат: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    # Получаем текущую выбранную модель пользователя
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    if not current_model_id:
        await query.edit_message_text('Модель не выбрана. Используй /start для выбора модели и чата.')
        return

    # Предлагаем выбрать слот чата для текущей модели
    await query.edit_message_text(
        f'Выбери слот для модели <b>{html.escape(model_id_to_name.get(current_model_id, "Неизвестная модель"))}</b>:',
        parse_mode=ParseMode.HTML,
        reply_markup=get_chat_slot_buttons(current_model_id)
    )

# Обработчик кнопки "очистить контекст"
async def clear_context_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    query = update.callback_query
    await query.answer()

    user_id = query.from_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался очистить контекст: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    # Получаем текущую выбранную модель и чат пользователя
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')
    if not current_model_id or not current_chat_slot:
        await query.edit_message_text('Модель или чат не выбраны. Используй /start для выбора модели и чата.')
        return

    # Проверка и инкремент лимита перед очисткой
    if not check_and_increment_limit(user_id, current_model_id):
        await query.edit_message_text('Вы достигли лимита запросов для этой модели.')
        return

    # Очищаем контекст
    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai')
    system_instruction = SYSTEM_INSTRUCTION_TEXT
    if provider == 'openai' and current_model_id not in ['openai-o1-preview', 'openai-o1-mini']:
        system_instruction = "You are a helpful assistant."

    context_store[user_id][current_model_id][current_chat_slot] = [{"role": "system", "content": system_instruction}]
    save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

    await query.edit_message_text(
        f'Контекст <b>Чат {current_chat_slot}</b> для модели <b>{html.escape(model_id_to_name.get(current_model_id, "Неизвестная модель"))}</b> очищен.',
        parse_mode=ParseMode.HTML,
        reply_markup=get_main_buttons(user_id, current_model_id)
    )

# Обработчик кнопки "Продолжить" через CallbackQueryHandler
async def continue_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """
    Обрабатывает нажатие кнопки "Продолжить" и отправляет дальнейший запрос модели.
    """
    query = update.callback_query
    await query.answer()

    user_id = query.from_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался использовать кнопку продолжить: {user_id}")
        await query.edit_message_text('Тебе сюда мяукать нельзя.')
        return

    # Получаем текущую модель и чат пользователя
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')

    if not current_model_id or not current_chat_slot:
        await query.edit_message_text('Модель или чат не выбраны. Используйте /start для выбора модели и чата.')
        return

    # Проверка и инкремент лимита перед продолжением
    if not check_and_increment_limit(user_id, current_model_id):
        await query.edit_message_text('Вы достигли лимита запросов для этой модели.')
        return

    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai')
    selected_model_type = model_info.get('type', 'text').lower()

    # Проверка доступа к модели
    if current_model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
        await query.edit_message_text('У тебя нет доступа к этой модели.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Получение текущего контекста
    context_messages = context_store.get(user_id, {}).get(current_model_id, {}).get(current_chat_slot, [])

    if not context_messages:
        await query.edit_message_text('Контекст чата пуст. Начните разговор с бота.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Добавляем специальное сообщение для продолжения
    context_messages.append({"role": "user", "content": "Продолжи"})

    # Подготовка данных для API с учетом контекста
    if provider == 'cablyai':
        response_data = await send_cablyai_request(current_model_id, messages=context_messages, model_type=selected_model_type)
        if response_data and response_data.get('choices'):
            answer = response_data["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
        else:
            logger.error("Неправильная структура ответа от CablyAI API.")
            await query.edit_message_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
            return
    elif provider in ['openai']:
        response = await send_openai_request(current_model_id, messages=context_messages)
        if response and isinstance(response, dict):
            if 'choices' in response and len(response['choices']) > 0:
                if isinstance(response['choices'][0]['message']['content'], str):
                    answer = response["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
                else:
                    # Если ответ содержит URL изображения
                    answer = response["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
            else:
                logger.error("Неправильная структура ответа от OpenAI API.")
                await query.edit_message_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        else:
            logger.error("Неправильная структура ответа от OpenAI API.")
            await query.edit_message_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
            return
    elif provider in ['helix', 'meowai']:
        # Используем соответствующую функцию для провайдера
        if provider == 'helix':
            response_data = await send_helix_request(current_model_id, context_messages)
        elif provider == 'meowai':
            response_data = await send_meowai_request(current_model_id, context_messages)
        
        if response_data and isinstance(response_data, str):
            answer = response_data
        else:
            logger.error(f"Неправильная структура ответа от {provider.capitalize()} API.")
            await query.edit_message_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
            return
    else:
        await query.edit_message_text('Неизвестный провайдер модели.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Добавление ответа ассистента в контекст
    context_store[user_id][current_model_id][current_chat_slot].append({"role": "assistant", "content": answer})
    save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

    try:
        if selected_model_type != 'image':
            # Преобразование Markdown в HTML
            answer_html = markdown_to_html(answer)
            messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
            for msg in messages:
                # Отправляем все основные кнопки, включая "Продолжить"
                await query.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
        # Не добавляем кнопку "Продолжить" для моделей типа 'image'
    except Exception as e:
        logger.error(f"Ошибка при отправке продолжения: {e}")
        await query.message.reply_text('Не удалось отправить продолжение ответа.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

# Функция для отправки запроса через Helix API
async def send_helix_request(model_id, messages):
    provider_info = config['providers'].get('helix', {})
    api_url = provider_info.get('api_endpoint')
    api_key = provider_info.get('api_key')

    if not api_url or not api_key:
        logger.error("Helix API настройки отсутствуют в config.json.")
        return None

    headers = {
        'Authorization': f"Bearer {api_key}",
        'Content-Type': 'application/json'
    }

    data = {
        "model": model_id,
        "messages": messages
    }

    try:
        async with aiohttp.ClientSession() as session:
            async with session.post(api_url, headers=headers, json=data) as response:
                if response.status == 200:
                    response_data = await response.json()
                    if "choices" in response_data and len(response_data["choices"]) > 0:
                        return response_data["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
                else:
                    error_text = await response.text()
                    logger.error(f"Ошибка запроса к Helix API: {response.status} - {error_text}")
                    return None
    except Exception as e:
        logger.error(f"Ошибка при запросе к Helix API: {e}")
        return None

# Функция для отправки запроса через MeowAI API
async def send_meowai_request(model_id, messages):
    provider_info = config['providers'].get('meowai', {})
    api_url = provider_info.get('api_endpoint')
    api_key = provider_info.get('api_key')

    if not api_url or not api_key:
        logger.error("MeowAI API настройки отсутствуют в config.json.")
        return None

    headers = {
        'Authorization': f"Bearer {api_key}",
        'Content-Type': 'application/json'
    }

    data = {
        "model": model_id,
        "messages": messages
    }

    try:
        async with aiohttp.ClientSession() as session:
            async with session.post(api_url, headers=headers, json=data) as response:
                if response.status == 200:
                    response_data = await response.json()
                    if "choices" in response_data and len(response_data["choices"]) > 0:
                        return response_data["choices"][0].get('message', {}).get('content', 'что-то пошло не так. мяу :(')
                else:
                    error_text = await response.text()
                    logger.error(f"Ошибка запроса к MeowAI API: {response.status} - {error_text}")
                    return None
    except Exception as e:
        logger.error(f"Ошибка при запросе к MeowAI API: {e}")
        return None

async def process_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_text = update.message.text
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        await update.message.reply_text("У вас нет доступа к этому боту.")
        return

    # Проверка, выбрал ли пользователь провайдера
    if 'current_provider' not in user_state.get(user_id, {}):
        # Если провайдер не выбран, ожидаем выбора
        await update.message.reply_text("Пожалуйста, выберите провайдера из предоставленных кнопок.")
        return

    # Получаем текущую модель и чат слота
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')

    if not current_model_id or not current_chat_slot:
        await update.message.reply_text('Модель или чат не выбраны. Используйте /start для выбора модели и чата.', reply_markup=get_main_buttons(user_id))
        return

    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai')
    selected_model_type = model_info.get('type', 'text').lower()

    # Проверка доступа к модели
    if current_model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
        await update.message.reply_text('Вам нельзя использовать эту модель.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Проверка и инкремент лимита
    if not check_and_increment_limit(user_id, current_model_id):
        await update.message.reply_text('Вы достигли лимита запросов для этой модели.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Инициализация контекста, если ещё не инициализирован
    if user_id not in context_store:
        context_store[user_id] = {}
    if current_model_id not in context_store[user_id]:
        context_store[user_id][current_model_id] = {}
    if current_chat_slot not in context_store[user_id][current_model_id]:
        system_instruction = SYSTEM_INSTRUCTION_TEXT
        if provider == 'openai' and current_model_id not in ['openai-o1-preview', 'openai-o1-mini']:
            system_instruction = "You are a helpful assistant."
        context_store[user_id][current_model_id][current_chat_slot] = load_chat_context(user_id, current_model_id, chat_slot=current_chat_slot, system_instruction=system_instruction)

    if selected_model_type == 'image':
        # Проверка языка сообщения, теперь всегда переводим через модель при наличии текста
        if user_text:
            await update.message.reply_text("Твой запрос содержит русский язык. Генерация картинки будет идти чуть дольше - другой модели нужно перевести текст на английский для более корректного понимания моделью твоего запроса. Если тебя не устроила полученная картинка - ты всегда можешь сделать ещё запрос.")
            translated_text = await translate_to_english(user_text)
            if translated_text:
                user_text = translated_text
            else:
                await update.message.reply_text("Не удалось перевести текст. Пожалуйста, попробуйте позже.", reply_markup=get_main_buttons(user_id, current_model_id))
                return

        # Генерация или обработка изображения через соответствующий API
        image_or_answer = await process_image_request(user_id, current_model_id, user_text)
        if image_or_answer:
            # Скачиваем изображение
            image_bytes = await download_image(image_or_answer)
            if image_bytes:
                image_file = InputFile(BytesIO(image_bytes), filename="image.png")
                try:
                    await update.message.reply_photo(photo=image_file, reply_markup=get_main_buttons(user_id, current_model_id))
                except TelegramError as e:
                    logger.error(f"Ошибка при отправке изображения: {e}")
                    await update.message.reply_text("Не удалось отправить изображение. Попробуйте позже.", reply_markup=get_main_buttons(user_id, current_model_id))
            else:
                await update.message.reply_text("Не удалось скачать изображение.", reply_markup=get_main_buttons(user_id, current_model_id))
        else:
            await update.message.reply_text("Не удалось сгенерировать/обработать изображение.", reply_markup=get_main_buttons(user_id, current_model_id))
    else:
        # Обработка текстовых сообщений для мультимодальных моделей
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "user", "content": user_text})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

        # Подготовка данных для API с учетом контекста
        messages = context_store[user_id][current_model_id][current_chat_slot]

        if provider == 'cablyai':
            response_data = await send_cablyai_request(current_model_id, messages=messages, model_type=selected_model_type)
            if response_data and response_data.get('choices'):
                answer = response_data["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
            else:
                logger.error("Неправильная структура ответа от CablyAI API.")
                await update.message.reply_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        elif provider == 'openai':
            response = await send_openai_request(current_model_id, messages=messages)
            if response and isinstance(response, dict):
                if 'choices' in response and len(response['choices']) > 0:
                    if isinstance(response['choices'][0]['message']['content'], str):
                        answer = response["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
                    else:
                        # Если ответ содержит URL изображения
                        answer = response["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
                else:
                    logger.error("Неправильная структура ответа от OpenAI API.")
                    await update.message.reply_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                    return
            else:
                logger.error("Неправильная структура ответа от OpenAI API.")
                await update.message.reply_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        elif provider in ['helix', 'meowai']:
            # Используем соответствующую функцию для провайдера
            if provider == 'helix':
                response_data = await send_helix_request(current_model_id, messages)
            elif provider == 'meowai':
                response_data = await send_meowai_request(current_model_id, messages)

            if response_data and isinstance(response_data, str):
                answer = response_data
            else:
                logger.error(f"Неправильная структура ответа от {provider.capitalize()} API.")
                await update.message.reply_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        else:
            await update.message.reply_text('Неизвестный провайдер модели.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Добавление ответа ассистента в контекст
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "assistant", "content": answer})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

        if user_text.lower().strip() != "продолжи":
            if user_mode_store.get(str(user_id), 'message') == 'message':
                # Преобразование Markdown в HTML
                answer_html = markdown_to_html(answer)
                messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
                for msg in messages:
                    # Отправляем все основные кнопки, включая "Продолжить"
                    await update.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
            elif user_mode_store.get(str(user_id), 'file') == 'file':
                # Сохранение ответа в .md файл
                file_path = save_md_file(answer, user_id)
                if file_path:
                    # Отправка файла пользователю
                    try:
                        with open(file_path, 'rb') as f:
                            await update.message.reply_document(
                                document=InputFile(f),
                                filename=os.path.basename(file_path),
                                caption="Твой ответ модели в формате markdown.",
                                reply_markup=get_main_buttons(user_id, current_model_id)
                            )
                        # Удаление файла после отправки
                        os.remove(file_path)
                    except Exception as e:
                        logger.error(f"Ошибка при отправке файла: {e}")
                        await update.message.reply_text('Не удалось отправить файл. Попробуйте позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                else:
                    await update.message.reply_text('Не удалось создать файл с ответом.', reply_markup=get_main_buttons(user_id, current_model_id))

async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь отправил изображение: {user_id}")
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    # Получаем текущую модель и чат слот
    current_model_id = user_state.get(user_id, {}).get('current_model_id')
    current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')

    if not current_model_id or not current_chat_slot:
        await update.message.reply_text('Модель или чат не выбраны. Используйте /start для выбора модели и чата.', reply_markup=get_main_buttons(user_id))
        return

    model_info = model_id_to_info.get(current_model_id, {})
    provider = model_info.get('provider', 'cablyai')
    selected_model_type = model_info.get('type', 'text').lower()

    # Проверка доступа к модели
    if current_model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
        await update.message.reply_text('Вам нельзя использовать эту модель.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Проверка и инкремент лимита
    if not check_and_increment_limit(user_id, current_model_id):
        await update.message.reply_text('Вы достигли лимита запросов для этой модели.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Проверка поддержки изображений
    if not is_image_supported(current_model_id):
        await update.message.reply_text('Выбранная модель не поддерживает обработку изображений.', reply_markup=get_main_buttons(user_id, current_model_id))
        return

    # Скачиваем изображение
    photo = update.message.photo[-1]  # Берем наибольшее разрешение изображения
    file = await photo.get_file()
    downloads_photos_dir = os.path.join('downloads', 'photos')
    if not os.path.exists(downloads_photos_dir):
        os.makedirs(downloads_photos_dir)
    file_path = os.path.join(downloads_photos_dir, f"{user_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.jpg")

    try:
        await file.download_to_drive(file_path)
    except Exception as e:
        logger.error(f"Ошибка при скачивании изображения: {e}")
        await update.message.reply_text('Не удалось скачать изображение. Попробуй позже.', reply_markup=get_main_buttons(user_id))
        return

    # Подготовка промпта из caption или пустой строки
    user_caption = update.message.caption or ""

    # Проверка языка промпта, теперь всегда переводим через модель при наличии текста
    if user_caption:
        await update.message.reply_text("Твой запрос содержит русский язык. Генерация картинки будет идти чуть дольше - другой модели нужно перевести текст на английский для более корректного понимания моделью твоего запроса. Если тебя не устроила полученная картинка - ты всегда можешь сделать ещё запрос.")
        translated_text = await translate_to_english(user_caption)
        if translated_text:
            user_caption = translated_text
        else:
            await update.message.reply_text("Не удалось перевести текст. Пожалуйста, попробуйте позже.", reply_markup=get_main_buttons(user_id, current_model_id))
            return

    # Генерация или обработка изображения через соответствующий API
    image_or_answer = await process_image_request(user_id, current_model_id, user_caption, image_path=file_path)
    if image_or_answer:
        # Если CablyAI возвращает URL изображения
        if image_or_answer.startswith('http'):
            # Скачиваем изображение
            image_bytes = await download_image(image_or_answer)
            if image_bytes:
                image_file = InputFile(BytesIO(image_bytes), filename="image.png")
                try:
                    await update.message.reply_photo(photo=image_file, reply_markup=get_main_buttons(user_id, current_model_id))
                except TelegramError as e:
                    logger.error(f"Ошибка при отправке изображения: {e}")
                    await update.message.reply_text("Не удалось отправить изображение. Попробуйте позже.", reply_markup=get_main_buttons(user_id, current_model_id))
            else:
                await update.message.reply_text("Не удалось скачать изображение.", reply_markup=get_main_buttons(user_id, current_model_id))
        else:
            # Если обработка вернула текст
            await update.message.reply_text(image_or_answer, reply_markup=get_main_buttons(user_id, current_model_id))
    else:
        await update.message.reply_text("Не удалось сгенерировать или обработать изображение.", reply_markup=get_main_buttons(user_id, current_model_id))

    # Удаление скачанного изображения
    if os.path.exists(file_path):
        os.remove(file_path)
        
# Обработчик документов (файлов)
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user_id = update.effective_user.id
    if user_id not in AUTHORIZED_USER_IDS:
        logger.info(f"Неавторизованный пользователь попытался отправить файл: {user_id}")
        await update.message.reply_text('Тебе сюда мяукать нельзя.')
        return

    user_mode = user_mode_store.get(str(user_id), 'message')

    document = update.message.document
    file = await document.get_file()
    filename = document.file_name.lower()  # Приводим к нижнему регистру для надёжной проверки

    # Проверка, является ли файл .py или .json
    if filename.endswith('.py') or filename.endswith('.json'):
        # Обработка как текстовый файл
        try:
            # Создаём директорию для скачивания, если её нет
            download_path = os.path.join('downloads', 'files')
            if not os.path.exists(download_path):
                os.makedirs(download_path)

            # Скачиваем файл
            file_path = os.path.join(download_path, document.file_name)
            await file.download_to_drive(file_path)

            # Читаем содержимое файла
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()

            # Удаляем файл после чтения
            os.remove(file_path)
        except Exception as e:
            logger.error(f"Ошибка при скачивании или чтении файла: {e}")
            await update.message.reply_text('Не удалось скачать или прочитать файл. Попробуйте позже.', reply_markup=get_main_buttons(user_id))
            return

        # Получаем текущую модель и чат слота
        current_model_id = user_state.get(user_id, {}).get('current_model_id')
        current_chat_slot = user_state.get(user_id, {}).get('current_chat_slot')

        if not current_model_id or not current_chat_slot:
            await update.message.reply_text('Модель или чат не выбраны. Используйте /start для выбора модели и чата.', reply_markup=get_main_buttons(user_id))
            return

        model_info = model_id_to_info.get(current_model_id, {})
        provider = model_info.get('provider', 'cablyai')

        # Проверка доступа к модели
        if current_model_id in RESTRICTED_MODELS and user_id not in AUTHORIZED_MODEL_USERS:
            await update.message.reply_text('Вам нельзя использовать эту модель.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Проверка, поддерживает ли модель текстовый ввод
        selected_model_type = model_info.get('type', 'text').lower()
        if selected_model_type not in ['text', 'multimodal']:
            await update.message.reply_text('Выбранная модель не поддерживает обработку текстовых файлов.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Проверка и инкремент лимита
        if not check_and_increment_limit(user_id, current_model_id):
            await update.message.reply_text('Вы достигли лимита запросов для этой модели.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Инициализация контекста, если ещё не инициализирован
        if user_id not in context_store:
            context_store[user_id] = {}
        if current_model_id not in context_store[user_id]:
            context_store[user_id][current_model_id] = {}
        if current_chat_slot not in context_store[user_id][current_model_id]:
            system_instruction = SYSTEM_INSTRUCTION_TEXT
            if provider == 'openai' and current_model_id not in ['openai-o1-preview', 'openai-o1-mini']:
                system_instruction = "You are a helpful assistant."
            context_store[user_id][current_model_id][current_chat_slot] = load_chat_context(user_id, current_model_id, chat_slot=current_chat_slot, system_instruction=system_instruction)

        # Добавление содержимого файла в контекст
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "user", "content": content})

        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

        # Подготовка данных для API с учетом контекста
        messages = context_store[user_id][current_model_id][current_chat_slot]

        if provider == 'cablyai':
            response_data = await send_cablyai_request(current_model_id, messages=messages, model_type=selected_model_type)
            if response_data and response_data.get('choices'):
                answer = response_data["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
            else:
                logger.error("Неправильная структура ответа от CablyAI API.")
                await update.message.reply_text('API вернул непредвиденный ответ. Попробуй позже.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        elif provider == 'openai':
            response = await send_openai_request(current_model_id, messages=messages)
            if response and isinstance(response, dict):
                if 'choices' in response and len(response['choices']) > 0:
                    if isinstance(response['choices'][0]['message']['content'], str):
                        answer = response["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
                    else:
                        # Если ответ содержит URL изображения
                        answer = response["choices"][0].get('message', {}).get('content', 'Что-то пошло не так. Мяу :(')
                else:
                    logger.error("Неправильная структура ответа от OpenAI API.")
                    await update.message.reply_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                    return
            else:
                logger.error("Неправильная структура ответа от OpenAI API.")
                await update.message.reply_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        elif provider in ['helix', 'meowai']:
            # Используем соответствующую функцию для провайдера
            if provider == 'helix':
                response_data = await send_helix_request(current_model_id, messages)
            elif provider == 'meowai':
                response_data = await send_meowai_request(current_model_id, messages)

            if response_data and isinstance(response_data, str):
                answer = response_data
            else:
                logger.error(f"Неправильная структура ответа от {provider.capitalize()} API.")
                await update.message.reply_text('API вернул непредвиденный ответ. попробуй потом.', reply_markup=get_main_buttons(user_id, current_model_id))
                return
        else:
            await update.message.reply_text('Неизвестный провайдер модели.', reply_markup=get_main_buttons(user_id, current_model_id))
            return

        # Добавление ответа ассистента в контекст
        context_store[user_id][current_model_id][current_chat_slot].append({"role": "assistant", "content": answer})
        save_chat_context(user_id, current_model_id, current_chat_slot, context_store[user_id][current_model_id][current_chat_slot])

        if user_mode == 'message':
            # Преобразование Markdown в HTML
            answer_html = markdown_to_html(answer)
            messages = split_message(answer_html, MAX_MESSAGE_LENGTH)
            for msg in messages:
                # Отправляем все основные кнопки, включая "Продолжить"
                await update.message.reply_text(msg, reply_markup=get_main_buttons(user_id, current_model_id))
        elif user_mode == 'file':
            # Сохранение ответа в .md файл
            file_path = save_md_file(answer, user_id)
            if file_path:
                # Отправка файла пользователю
                try:
                    with open(file_path, 'rb') as f:
                        await update.message.reply_document(
                            document=InputFile(f),
                            filename=os.path.basename(file_path),
                            caption="Твой ответ модели в формате markdown.",
                            reply_markup=get_main_buttons(user_id, current_model_id)
                        )
                    # Удаление файла после отправки
                    os.remove(file_path)
                except Exception as e:
                    logger.error(f"Ошибка при отправке файла: {e}")
                    await update.message.reply_text('Не удалось отправить файл. Попробуйте позже.', reply_markup=get_main_buttons(user_id, current_model_id))
            else:
                await update.message.reply_text('Не удалось создать файл с ответом.', reply_markup=get_main_buttons(user_id, current_model_id))

# Обработчик документов (файлов) уже реализован выше

# Обработчик ошибок
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Ловит все необработанные ошибки."""
    logger.error(msg="Exception while handling an update:", exc_info=context.error)
    if isinstance(update, Update) and update.effective_user:
        try:
            user_id = update.effective_user.id
            # Получаем текущую модель, если она есть
            current_model_id = user_state.get(user_id, {}).get('current_model_id')
            await update.effective_message.reply_text('Произошла ошибка. Пожалуйста, попробуйте позже.', reply_markup=get_main_buttons(user_id, current_model_id))
        except TelegramError as e:
            logger.error(f"Не удалось отправить сообщение об ошибке пользователю: {e}")

# Фоновая задача для сброса лимитов в 00:00 по московскому времени
async def reset_limits_daily(application):
    while True:
        now = datetime.now(ZoneInfo('Europe/Moscow'))
        next_reset = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
        sleep_duration = (next_reset - now).total_seconds()
        logger.info(f"Сброс лимитов будет через {sleep_duration} секунд.")
        await asyncio.sleep(sleep_duration)

        # Сброс лимитов
        usage_store['cablyai_tokens'] = 0
        usage_store['cablyai_images'] = 0
        usage_store['helix_requests'] = 0
        usage_store['meowai_requests'] = 0
        save_usage()
        logger.info("Лимиты пользователей сброшены.")

        # Отправка уведомлений всем пользователям о сбросе лимитов
        for uid in AUTHORIZED_USER_IDS:
            try:
                await application.bot.send_message(
                    chat_id=uid,
                    text="Дневные лимиты сброшены. Вы можете продолжать использовать бота."
                )
            except Exception as e:
                logger.error(f"Не удалось отправить сообщение пользователю {uid}: {e}")

# Основная функция запуска бота
def main():
    global application  # Чтобы фоновая задача могла использовать объект application

    TELEGRAM_TOKEN = config.get('telegram_token')
    if not TELEGRAM_TOKEN:
        logger.error("Токен бота не указан в config.json.")
        exit(1)

    # Создание директорий для чатов, если её нет
    chats_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chats')
    if not os.path.exists(chats_dir):
        os.makedirs(chats_dir)

    # Создание директорий для загрузок, если их нет
    downloads_photos_dir = os.path.join('downloads', 'photos')
    if not os.path.exists(downloads_photos_dir):
        os.makedirs(downloads_photos_dir)

    downloads_files_dir = os.path.join('downloads', 'files')
    if not os.path.exists(downloads_files_dir):
        os.makedirs(downloads_files_dir)

    # Создание директорий для ответов и загрузок, если их нет
    responses_dir = os.path.join('responses')
    if not os.path.exists(responses_dir):
        os.makedirs(responses_dir)

    # Создание экземпляра приложения с установкой ParseMode.HTML по умолчанию
    application = ApplicationBuilder().token(TELEGRAM_TOKEN).defaults(Defaults(parse_mode=ParseMode.HTML)).build()

    # Обработчики команд
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("setmode", setmode))
    application.add_handler(CommandHandler("mode", mode_command))
    application.add_handler(CommandHandler("models", models_command))
    application.add_handler(CommandHandler("reload", reload_bot))
    application.add_handler(CommandHandler("limits", limits_command))
    application.add_handler(CommandHandler("adduser", add_user))

    # Обработчики CallbackQuery
    application.add_handler(CallbackQueryHandler(select_model, pattern='^model_select:.+$|^restricted:.+$'))
    application.add_handler(CallbackQueryHandler(select_chat_slot, pattern='^chat_slot:\d+:.+$'))
    application.add_handler(CallbackQueryHandler(change_model_callback, pattern='^change_model$'))
    application.add_handler(CallbackQueryHandler(change_chat_slot_callback, pattern='^change_chat_slot$'))
    application.add_handler(CallbackQueryHandler(clear_context_callback, pattern='^clear_context$'))
    application.add_handler(CallbackQueryHandler(continue_callback, pattern='^continue$'))

    # Обработчики сообщений
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND & filters.Regex('^(CatAI|OpenAI|Helix|MeowAI)$'), handle_provider_selection))
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, process_message))
    application.add_handler(MessageHandler(filters.PHOTO, handle_photo))
    application.add_handler(MessageHandler(filters.Document.ALL, handle_document))

    # Обработчик ошибок
    application.add_error_handler(error_handler)

    # Запуск фоновой задачи для сброса лимитов
    application.job_queue.run_once(lambda context: asyncio.create_task(reset_limits_daily(application)), when=0)

    # Запуск бота
    application.run_polling()

if __name__ == '__main__':
    main()