# Version2406

import logging
import re
import aiohttp
import os
from dotenv import load_dotenv
from telegram import Update, ReplyKeyboardMarkup
from telegram.ext import (
    Application,
    CommandHandler,
    MessageHandler,
    filters,
    ConversationHandler,
    CallbackContext,
)
from telegram.ext import ApplicationBuilder, Defaults
import pytz

# например, для Риги
tz = pytz.timezone("Europe/Moscow")

defaults = Defaults(tzinfo=tz)

from typing import Dict, Optional, List
from logging.handlers import RotatingFileHandler
import traceback
from tenacity import retry, stop_after_attempt, wait_exponential

# Настройка логирования
LOG_FILE = os.path.join(os.path.dirname(__file__), 'logs', 'bot.log')
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s - %(message)s'
)

file_handler = RotatingFileHandler(
    LOG_FILE,
    maxBytes=5*1024*1024,
    backupCount=5,
    encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)

console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)

logger.addHandler(file_handler)
logger.addHandler(console_handler)

# Загрузка переменных окружения
load_dotenv()
AMOCRM_DOMAIN = 'https://moscowrcrealty.amocrm.ru'
AMOCRM_API_KEY = os.getenv('AMOCRM_API_KEY')
TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN')

if not AMOCRM_API_KEY or not TELEGRAM_TOKEN:
    logger.critical("AMOCRM_API_KEY или TELEGRAM_TOKEN не найдены в .env")
    raise ValueError("AMOCRM_API_KEY или TELEGRAM_TOKEN не найдены в .env")

# Этапы диалога
PHONE, NAME, NOTE, SOURCE = range(4)

# Новый этап сделки
NEW_DEAL_PIPELINE_ID = 7131054
NEW_DEAL_STAGE_ID = 59648410

def normalize_phone(phone: str) -> str:
    """Нормализует номер телефона: оставляет только цифры."""
    logger.debug(f"Вход: phone={phone}")
    digits = ''.join(c for c in phone if c.isdigit())
    if len(digits) != 11:
        logger.warning(f"Номер телефона '{phone}' не содержит 11 цифр: {digits}")
        return digits
    if digits.startswith("8"):
        digits = "7" + digits[1:]
    logger.debug(f"Выход: normalized_phone={digits}")
    return digits

def last_10(phone: str) -> str:
    """Возвращает последние 10 цифр."""
    logger.debug(f"Вход: phone={phone}")
    result = phone[-10:] if len(phone) >= 10 else phone
    logger.debug(f"Выход: last_10={result}")
    return result

async def get_source_keyboard() -> ReplyKeyboardMarkup:
    """Возвращает клавиатуру для выбора источника сделки."""
    logger.debug("Создание клавиатуры для выбора источника")
    keyboard = ReplyKeyboardMarkup(
        [
            ["закупка6", "закупка7", "закупка8"],
            ["закупка9", "закупка10", "Тёплый круг"],
            ["СЮ", "СВ"],
            ["Отмена"]
        ],
        one_time_keyboard=True,
        resize_keyboard=True
    )
    logger.debug("Клавиатура создана")
    return keyboard

@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=2))
async def find_contact_by_phone(phone: str) -> Optional[Dict]:
    """Ищет контакт в amoCRM по номеру телефона."""
    logger.debug(f"Вход: phone={phone}")
    norm_phone = normalize_phone(phone)
    logger.info(f"Поиск контакта по номеру: {norm_phone}")
    queries = [norm_phone]
    if norm_phone.startswith("7"):
        queries.append("8" + norm_phone[1:])
    l10 = last_10(norm_phone)
    if l10 not in queries:
        queries.append(l10)
    headers = {'Authorization': f'Bearer {AMOCRM_API_KEY}'}
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
        for query in queries:
            url = f'{AMOCRM_DOMAIN}/api/v4/contacts?query={query}&limit=250'
            logger.debug(f"Запрос к amoCRM: method=GET, url={url}")
            try:
                async with session.get(url, headers=headers) as response:
                    logger.debug(f"Ответ от amoCRM: status={response.status}")
                    if response.status == 204:
                        logger.info(f"Запрос {query}: ответ 204, контакт не найден")
                        continue
                    if response.status == 200:
                        data = await response.json()
                        contacts = data.get('_embedded', {}).get('contacts', [])
                        logger.info(f"Запрос {query}: найдено контактов: {len(contacts)}")
                        for contact in contacts:
                            for field in contact.get('custom_fields_values', []):
                                field_code = field.get('field_code', '') if field is not None else ''
                                if isinstance(field_code, str) and field_code.upper() == 'PHONE':
                                    for value in field.get('values', []):
                                        stored_phone = normalize_phone(value.get('value', ''))
                                        if stored_phone == norm_phone or last_10(stored_phone) == l10:
                                            logger.info(f"Найден контакт: id={contact['id']}, phone={stored_phone}")
                                            logger.debug(f"Выход: contact={contact}")
                                            return contact
                    else:
                        error_text = await response.text()
                        logger.error(f"Ошибка поиска контакта: query={query}, status={response.status}, response={error_text}")
            except aiohttp.ClientError as e:
                logger.error(f"Сетевая ошибка при поиске контакта: query={query}, error={str(e)}", exc_info=True)
                raise
        logger.info("Контакт не найден ни по одному запросу")
        logger.debug("Выход: contact=None")
        return None

@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=2))
async def find_deal_by_contact(contact_id: int) -> Optional[Dict]:
    logger.info(f"Поиск сделок для контакта ID {contact_id}")
    url = f'{AMOCRM_DOMAIN}/api/v4/contacts/{contact_id}?with=leads'
    headers = {'Authorization': f'Bearer {AMOCRM_API_KEY}'}
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
        async with session.get(url, headers=headers) as response:
            if response.status == 200:
                data = await response.json()
                leads = data.get('_embedded', {}).get('leads', [])
                logger.info(f"Найдено сделок: {len(leads)}, IDs: {[lead['id'] for lead in leads]}")
                if leads:
                    chosen_deal = max(leads, key=lambda d: int(d['id']))
                    logger.info(f"Выбрана сделка: id={chosen_deal['id']}")
                    return chosen_deal
                return None
            else:
                logger.error(f"Ошибка: status={response.status}, response={await response.text()}")
                return None
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=2))
async def update_deal_stage(deal_id: int, new_stage: int) -> bool:
    """Обновляет стадию сделки."""
    logger.debug(f"Вход: deal_id={deal_id}, new_stage={new_stage}")
    logger.info(f"Обновление стадии сделки ID {deal_id} на {new_stage}")
    url = f'{AMOCRM_DOMAIN}/api/v4/leads/{deal_id}'
    headers = {'Authorization': f'Bearer {AMOCRM_API_KEY}', 'Content-Type': 'application/json'}
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
        logger.debug(f"Запрос к amoCRM: method=GET, url={url}")
        try:
            async with session.get(url, headers=headers) as response:
                logger.debug(f"Ответ от amoCRM: status={response.status}")
                if response.status == 200:
                    lead = await response.json()
                    current_data = lead.get('custom_fields_values', []) or []
                    data = {
                        "custom_fields_values": current_data,
                        "status_id": new_stage
                    }
                    logger.debug(f"Запрос к amoCRM: method=PATCH, url={url}, data={data}")
                    async with session.patch(url, json=data, headers=headers) as response:
                        logger.debug(f"Ответ от amoCRM: status={response.status}")
                        if response.status in [200, 204]:
                            logger.info("Стадия сделки успешно обновлена")
                            logger.debug("Выход: success=True")
                            return True
                        else:
                            error_text = await response.text()
                            logger.error(f"Ошибка обновления сделки: status={response.status}, response={error_text}")
                            logger.debug("Выход: success=False")
                            return False
                else:
                    error_text = await response.text()
                    logger.error(f"Ошибка получения сделки: status={response.status}, response={error_text}")
                    logger.debug("Выход: success=False")
                    return False
        except aiohttp.ClientError as e:
            logger.error(f"Сетевая ошибка при обновлении сделки: error={str(e)}", exc_info=True)
            raise

@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=2))
async def create_contact_in_amocrm(name: str, phone: str) -> Optional[int]:
    """Создаёт контакт в amoCRM."""
    logger.debug(f"Вход: name={name}, phone={phone}")
    norm_phone = normalize_phone(phone)
    logger.info(f"Создание контакта: name={name}, phone={norm_phone}")
    url = f'{AMOCRM_DOMAIN}/api/v4/contacts'
    headers = {'Authorization': f'Bearer {AMOCRM_API_KEY}', 'Content-Type': 'application/json'}
    data = [{
        "name": name,
        "custom_fields_values": [
            {"field_code": "PHONE", "values": [{"value": norm_phone}]}
        ]
    }]
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
        logger.debug(f"Запрос к amoCRM: method=POST, url={url}, data={data}")
        try:
            async with session.post(url, json=data, headers=headers) as response:
                logger.debug(f"Ответ от amoCRM: status={response.status}")
                response_text = await response.text()
                if response.status in [200, 201]:
                    contact_id = (await response.json())['_embedded']['contacts'][0]['id']
                    logger.info(f"Контакт создан: id={contact_id}")
                    logger.debug(f"Выход: contact_id={contact_id}")
                    return contact_id
                else:
                    logger.error(f"Ошибка создания контакта: status={response.status}, response={response_text}")
                    logger.debug("Выход: contact_id=None")
                    return None
        except aiohttp.ClientError as e:
            logger.error(f"Сетевая ошибка при создании контакта: error={str(e)}", exc_info=True)
            raise

@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=2))
async def create_deal_in_amocrm(deal_name: str, contact_id: int) -> Optional[int]:
    """Создаёт сделку в amoCRM."""
    logger.debug(f"Вход: deal_name={deal_name}, contact_id={contact_id}")
    logger.info(f"Создание сделки для контакта ID {contact_id}: name={deal_name}")
    url = f'{AMOCRM_DOMAIN}/api/v4/leads'
    headers = {'Authorization': f'Bearer {AMOCRM_API_KEY}', 'Content-Type': 'application/json'}
    data = [{
        "name": f"Сделка: {deal_name}",
        "pipeline_id": NEW_DEAL_PIPELINE_ID,
        "status_id": NEW_DEAL_STAGE_ID,
        "_embedded": {"contacts": [{"id": contact_id}]}
    }]
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
        logger.debug(f"Запрос к amoCRM: method=POST, url={url}, data={data}")
        try:
            async with session.post(url, json=data, headers=headers) as response:
                logger.debug(f"Ответ от amoCRM: status={response.status}")
                response_text = await response.text()
                if response.status in [200, 201]:
                    deal_id = (await response.json())['_embedded']['leads'][0]['id']
                    logger.info(f"Сделка создана: id={deal_id}")
                    logger.debug(f"Выход: deal_id={deal_id}")
                    return deal_id
                else:
                    logger.error(f"Ошибка создания сделки: status={response.status}, response={response_text}")
                    logger.debug("Выход: deal_id=None")
                    return None
        except aiohttp.ClientError as e:
            logger.error(f"Сетевая ошибка при создании сделки: error={str(e)}", exc_info=True)
            raise

@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=2))
async def add_note_to_deal(deal_id: int, note: str) -> bool:
    """Добавляет примечание к сделке."""
    logger.debug(f"Вход: deal_id={deal_id}, note={note}")
    logger.info(f"Добавление примечания к сделке ID {deal_id}")
    url = f'{AMOCRM_DOMAIN}/api/v4/leads/{deal_id}/notes'
    headers = {'Authorization': f'Bearer {AMOCRM_API_KEY}', 'Content-Type': 'application/json'}
    data = [{"note_type": "common", "params": {"text": note}}]
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
        logger.debug(f"Запрос к amoCRM: method=POST, url={url}, data={data}")
        try:
            async with session.post(url, json=data, headers=headers) as response:
                logger.debug(f"Ответ от amoCRM: status={response.status}")
                response_text = await response.text()
                if response.status in [200, 201]:
                    logger.info(f"Примечание добавлено к сделке ID {deal_id}")
                    logger.debug("Выход: success=True")
                    return True
                else:
                    logger.error(f"Ошибка добавления примечания: status={response.status}, response={response_text}")
                    logger.debug("Выход: success=False")
                    return False
        except aiohttp.ClientError as e:
            logger.error(f"Сетевая ошибка при добавлении примечания: error={str(e)}", exc_info=True)
            raise

@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=2))
async def add_tag_to_deal(deal_id: int, tag_name: str) -> bool:
    """Добавляет тег к сделке."""
    logger.debug(f"Вход: deal_id={deal_id}, tag_name={tag_name}")
    logger.info(f"Добавление тега '{tag_name}' к сделке ID {deal_id}")
    url = f'{AMOCRM_DOMAIN}/api/v4/leads/{deal_id}'
    headers = {'Authorization': f'Bearer {AMOCRM_API_KEY}', 'Content-Type': 'application/json'}
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
        logger.debug(f"Запрос к amoCRM: method=GET, url={url}")
        try:
            async with session.get(url, headers=headers) as response:
                logger.debug(f"Ответ от amoCRM: status={response.status}")
                if response.status != 200:
                    error_text = await response.text()
                    logger.error(f"Ошибка получения сделки: status={response.status}, response={error_text}")
                    logger.debug("Выход: success=False")
                    return False
                lead = await response.json()
                current_tags = lead.get('_embedded', {}).get('tags', [])
                current_tag_names = [tag.get('name') for tag in current_tags]
                if tag_name not in current_tag_names:
                    current_tags.append({"name": tag_name})
                    data = {"_embedded": {"tags": current_tags}}
                    logger.debug(f"Запрос к amoCRM: method=PATCH, url={url}, data={data}")
                    async with session.patch(url, json=data, headers=headers) as response:
                        logger.debug(f"Ответ от amoCRM: status={response.status}")
                        if response.status in [200, 204]:
                            logger.info(f"Тег '{tag_name}' добавлен к сделке ID {deal_id}")
                            logger.debug("Выход: success=True")
                            return True
                        else:
                            error_text = await response.text()
                            logger.error(f"Ошибка добавления тега: status={response.status}, response={error_text}")
                            logger.debug("Выход: success=False")
                            return False
                else:
                    logger.info(f"Тег '{tag_name}' уже существует в сделке ID {deal_id}")
                    logger.debug("Выход: success=True")
                    return True
        except aiohttp.ClientError as e:
            logger.error(f"Сетевая ошибка при добавлении тега: error={str(e)}", exc_info=True)
            raise

async def back_to_phone(update: Update, context: CallbackContext) -> int:
    logger.debug("Вход")
    logger.info("Возврат к вводу телефона")
    await update.message.reply_text(
        "📱 Введите ваш телефон:",
        reply_markup=ReplyKeyboardMarkup([["Отмена"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=PHONE")
    return PHONE

async def back_to_name(update: Update, context: CallbackContext) -> int:
    logger.debug("Вход")
    logger.info("Возврат к вводу имени")
    await update.message.reply_text(
        "👤 Введите ваше имя:",
        reply_markup=ReplyKeyboardMarkup([["Отмена", "Назад"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=NAME")
    return NAME

async def back_to_note(update: Update, context: CallbackContext) -> int:
    logger.debug("Вход")
    logger.info("Возврат к вводу примечания")
    await update.message.reply_text(
        "📝 Введите примечание для сделки:",
        reply_markup=ReplyKeyboardMarkup([["Отмена", "Назад"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=NOTE")
    return NOTE

async def cancel(update: Update, context: CallbackContext) -> int:
    logger.debug("Вход")
    logger.info("Пользователь нажал 'Отмена'")
    await update.message.reply_text(
        "🚫 Операция отменена.\n\n"
        "Пришли данные в формате:\n"
        "📝 Примечание Имя Номер телефона\n"
        "или выбери *Ввести вручную*.",
        reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=ConversationHandler.END")
    return ConversationHandler.END

async def start_command(update: Update, context: CallbackContext) -> int:
    logger.debug("Вход")
    logger.info("Получена команда /start")
    await update.message.reply_text(
        "👋 Привет! Я бот для создания сделок в amoCRM.\n\n"
        "Пришли данные в формате:\n"
        "📝 Примечание Имя Номер телефона\n"
        "или выбери *Ввести вручную*.",
        reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=ConversationHandler.END")
    return ConversationHandler.END

async def manual_entry(update: Update, context: CallbackContext) -> int:
    logger.debug("Вход")
    logger.info("Пользователь выбрал ручной ввод")
    await update.message.reply_text(
        "📱 Введите ваш телефон:",
        reply_markup=ReplyKeyboardMarkup([["Отмена"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=PHONE")
    return PHONE

async def phone_received(update: Update, context: CallbackContext) -> int:
    logger.debug(f"Вход: message={update.message.text}")
    raw_phone = update.message.text.strip()
    logger.info(f"Пользователь ввёл телефон: {raw_phone}")
    if raw_phone.lower() == "отмена":
        logger.debug("Вызов cancel")
        return await cancel(update, context)
    if raw_phone.lower() == "ввести вручную":
        logger.info("Повторный запрос ввода телефона")
        await update.message.reply_text(
            "📱 Введите ваш телефон:",
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=PHONE")
        return PHONE
    norm_phone = normalize_phone(raw_phone)
    logger.info(f"Нормализованный телефон: {norm_phone}")
    if not re.match(r"^\d{11}$", norm_phone):
        logger.warning("Неверный формат телефона")
        await update.message.reply_text(
            "❌ Неверный формат телефона. Введите 11-значный номер:",
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=PHONE")
        return PHONE
    context.user_data['phone'] = norm_phone
    await update.message.reply_text(
        "👤 Введите ваше имя:",
        reply_markup=ReplyKeyboardMarkup([["Отмена", "Назад"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=NAME")
    return NAME

async def name_received(update: Update, context: CallbackContext) -> int:
    logger.debug(f"Вход: message={update.message.text}")
    name = update.message.text.strip()
    logger.info(f"Пользователь ввёл имя: {name}")
    if name.lower() == "отмена":
        logger.sdebug("Вызов cancel")
        return await cancel(update, context)
    if name.lower() == "назад":
        logger.debug("Вызов back_to_phone")
        return await back_to_phone(update, context)
    if not name:
        logger.warning("Пустое имя")
        await update.message.reply_text(
            "❌ Имя не может быть пустым. Введите ваше имя:",
            reply_markup=ReplyKeyboardMarkup([["Отмена", "Назад"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=NAME")
        return NAME
    context.user_data['name'] = name
    await update.message.reply_text(
        "📝 Введите примечание для сделки:",
        reply_markup=ReplyKeyboardMarkup([["Отмена", "Назад"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=NOTE")
    return NOTE

async def process_contact_and_deal(
    update: Update,
    context: CallbackContext,
    phone: str,
    name: str,
    note: str
) -> int:
    logger.debug(f"Вход: phone={phone}, name={name}, note={note}")
    logger.info(f"Обработка контакта и сделки: phone={phone}, name={name}")
    try:
        existing_contact = await find_contact_by_phone(phone)
        deal_id = None
        deal_link = None

        if existing_contact:
            contact_id = int(existing_contact['id'])
            logger.info(f"Контакт существует: id={contact_id}")
            existing_deal = await find_deal_by_contact(contact_id)
            if existing_deal:
                deal_id = int(existing_deal['id'])
                logger.info(f"Найдена сделка: id={deal_id}")
                updated = await update_deal_stage(deal_id, NEW_DEAL_STAGE_ID)
                if updated:
                    await add_note_to_deal(deal_id, f"Повторная заявка: {note}")
                    deal_link = f"{AMOCRM_DOMAIN}/leads/detail/{deal_id}"
                    message = (
                        f"🔄 Контакт уже существует, этап сделки обновлён.\n"
                        f"🔗 Ссылка на сделку: {deal_link}\n\n"
                        f"Выбери источник сделки:"
                    )
                else:
                    logger.error("Не удалось обновить этап сделки")
                    await update.message.reply_text(
                        "❌ Контакт уже существует, но не удалось обновить этап сделки.",
                        reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
                        parse_mode="Markdown"
                    )
                    logger.debug("Выход: state=ConversationHandler.END")
                    return ConversationHandler.END
            else:
                deal_id = await create_deal_in_amocrm(name, contact_id)
                if deal_id:
                    await add_note_to_deal(deal_id, note)
                    deal_link = f"{AMOCRM_DOMAIN}/leads/detail/{deal_id}"
                    message = (
                        f"✅ Контакт уже существует, создана новая сделка.\n"
                        f"🔗 Ссылка на сделку: {deal_link}\n\n"
                        f"Выбери источник сделки:"
                    )
                else:
                    logger.error("Не удалось создать сделку")
                    await update.message.reply_text(
                        "❌ Контакт уже существует, но не удалось создать сделку.",
                        reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
                        parse_mode="Markdown"
                    )
                    logger.debug("Выход: state=ConversationHandler.END")
                    return ConversationHandler.END
        else:
            contact_id = await create_contact_in_amocrm(name, phone)
            if not contact_id:
                logger.error("Не удалось создать контакт")
                await update.message.reply_text(
                    "❌ Не удалось создать контакт.",
                    reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
                    parse_mode="Markdown"
                )
                logger.debug("Выход: state=ConversationHandler.END")
                return ConversationHandler.END

            deal_id = await create_deal_in_amocrm(name, contact_id)
            if not deal_id:
                logger.error("Не удалось создать сделку")
                await update.message.reply_text(
                    "❌ Не удалось создать сделку.",
                    reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
                    parse_mode="Markdown"
                )
                logger.debug("Выход: state=ConversationHandler.END")
                return ConversationHandler.END

            await add_note_to_deal(deal_id, note)
            deal_link = f"{AMOCRM_DOMAIN}/leads/detail/{deal_id}"
            message = (
                f"🎉 Контакт и сделка успешно созданы!\n"
                f"🔗 Ссылка на сделку: {deal_link}\n\n"
                f"Выбери источник сделки:"
            )

        context.user_data['deal_id'] = deal_id
        await update.message.reply_text(
            message,
            reply_markup=await get_source_keyboard(),
            parse_mode="Markdown"
        )
        logger.info(f"Запрос источника сделки: deal_id={deal_id}")
        logger.debug("Выход: state=SOURCE")
        return SOURCE
    except Exception as e:
        logger.error(f"Ошибка в process_contact_and_deal: {str(e)}", exc_info=True)
        await update.message.reply_text(
            "❌ Произошла ошибка при обработке данных.",
            reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=ConversationHandler.END")
        return ConversationHandler.END

async def note_received(update: Update, context: CallbackContext) -> int:
    logger.debug(f"Вход: message={update.message.text}")
    note = update.message.text.strip()
    logger.info(f"Пользователь ввёл примечание: {note}")
    if note.lower() == "отмена":
        logger.debug("Вызов cancel")
        return await cancel(update, context)
    if note.lower() == "назад":
        logger.debug("Вызов back_to_name")
        return await back_to_name(update, context)
    if not note:
        logger.warning("Пустое примечание")
        await update.message.reply_text(
            "❌ Примечание не может быть пустым. Введите примечание:",
            reply_markup=ReplyKeyboardMarkup([["Отмена", "Назад"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=NOTE")
        return NOTE

    context.user_data['note'] = note
    phone = context.user_data.get('phone')
    name = context.user_data.get('name')
    logger.debug(f"Данные: phone={phone}, name={name}, note={note}")

    return await process_contact_and_deal(update, context, phone, name, note)

async def source_received(update: Update, context: CallbackContext) -> int:
    source = update.message.text.strip()
    logger.info(f"Пользователь выбрал источник: {source}")
    if source.lower() == "отмена":
        return await cancel(update, context)
    valid_sources = ["закупка6", "закупка7", "закупка8", "закупка9", "закупка10", "Тёплый круг", "СЮ", "СВ"]
    if source not in valid_sources:
        await update.message.reply_text(
            "❌ Неверный источник. Выбери один из предложенных вариантов:",
            reply_markup=await get_source_keyboard(),
            parse_mode="Markdown"
        )
        return SOURCE

    deal_id = context.user_data.get('deal_id')
    phone = context.user_data.get('phone')
    if not deal_id or not phone:
        logger.error(f"Отсутствуют данные: deal_id={deal_id}, phone={phone}")
        await update.message.reply_text(
            "❌ Ошибка: данные сделки или телефона потеряны. Начните заново.",
            reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        return ConversationHandler.END

    await add_note_to_deal(deal_id, f"Источник сделки: {source}")
    tag_added = await add_tag_to_deal(deal_id, source)
    if not tag_added:
        logger.error(f"Не удалось добавить тег: source={source}, deal_id={deal_id}")
    await update.message.reply_text(
        f"✅ Источник *{source}* сохранён.\n\n"
        "Можем создать ещё одну сделку.",
        reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    return ConversationHandler.END
    logger.debug(f"Вход: message={update.message.text}")
    source = update.message.text.strip()
    logger.info(f"Пользователь выбрал источник: {source}")
    if source.lower() == "отмена":
        logger.debug("Вызов cancel")
        return await cancel(update, context)
    valid_sources = ["закупка6", "закупка7", "закупка8", "закупка9", "закупка10", "Тёплый круг", "СЮ", "СВ"]
    if source not in valid_sources:
        logger.warning(f"Неверный источник: {source}")
        await update.message.reply_text(
            "❌ Неверный источник. Выбери один из предложенных вариантов:",
            reply_markup=await get_source_keyboard(),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=SOURCE")
        return SOURCE

    deal_id = context.user_data.get('deal_id')
    if deal_id:
        await add_note_to_deal(deal_id, f"Источник сделки: {source}")
        tag_added = await add_tag_to_deal(deal_id, source)
        if not tag_added:
            logger.error(f"Не удалось добавить тег: source={source}, deal_id={deal_id}")
        logger.info(f"Источник добавлен: source={source}, deal_id={deal_id}")
    else:
        logger.error("ID сделки не найден в user_data")

    await update.message.reply_text(
        f"✅ Источник *{source}* сохранён.\n\n"
        "Можем создать ещё одну сделку. Пример:\n"
        "📝 Заявка Иван +7(123)456-7890\n"
        "или выбери *Ввести вручную*.",
        reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
        parse_mode="Markdown"
    )
    logger.debug("Выход: state=ConversationHandler.END")
    return ConversationHandler.END

async def handle_full_input(update: Update, context: CallbackContext) -> int:
    logger.debug(f"Вход: message={update.message.text}")
    text = update.message.text.strip()
    logger.info(f"Получено сообщение: {text}")

    if text.lower() == "ввести вручную":
        logger.debug("Вызов manual_entry")
        return await manual_entry(update, context)
    if text.lower() == "отмена":
        logger.debug("Вызов cancel")
        return await cancel(update, context)

    digits = ''.join(c for c in text if c.isdigit())
    if len(digits) < 11:
        logger.warning("Недостаточно цифр для номера телефона")
        await update.message.reply_text(
            "❌ Неверный формат данных. Номер телефона должен содержать 11 цифр.\n"
            "Пример: Заявка Иван +7(123)456-7890",
            reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=ConversationHandler.END")
        return ConversationHandler.END

    phone = digits[-11:]
    phone_match = re.search(r'[\+\d\s\(\)-]*\d', text)
    if not phone_match:
        logger.warning("Не удалось найти номер телефона")
        await update.message.reply_text(
            "❌ Неверный формат номера телефона.\n"
            "Пример: Заявка Иван +7(123)456-7890",
            reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=ConversationHandler.END")
        return ConversationHandler.END

    phone_start = phone_match.start()
    before_phone = text[:phone_start].strip()
    if not before_phone:
        logger.warning("Текст перед номером телефона пустой")
        await update.message.reply_text(
            "❌ Не указано имя. Укажите имя перед номером телефона.\n"
            "Пример: Заявка Иван +7(123)456-7890",
            reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=ConversationHandler.END")
        return ConversationHandler.END

    words_before = re.findall(r'\b\w+\b', before_phone)
    if not words_before:
        logger.warning("Не удалось найти имя")
        await update.message.reply_text(
            "❌ Не указано имя. Укажите имя перед номером телефона.\n"
            "Пример: Заявка Иван +7(123)456-7890",
            reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=ConversationHandler.END")
        return ConversationHandler.END

    name = words_before[-1]
    note = " ".join(words_before[:-1]) or "Без примечания"
    logger.info(f"Извлечено: phone={phone}, name={name}, note={note}")

    normalized_phone = normalize_phone(phone)
    if not re.match(r"^\d{11}$", normalized_phone):
        logger.error(f"Неверный формат телефона: {normalized_phone}")
        await update.message.reply_text(
            "❌ Неверный формат номера телефона: ожидается 11 цифр.\n"
            "Пример: Заявка Иван +7(123)456-7890",
            reply_markup=ReplyKeyboardMarkup([["Ввести вручную"]], one_time_keyboard=True, resize_keyboard=True),
            parse_mode="Markdown"
        )
        logger.debug("Выход: state=ConversationHandler.END")
        return ConversationHandler.END

    context.user_data['phone'] = normalized_phone
    context.user_data['name'] = name
    context.user_data['note'] = note

    return await process_contact_and_deal(update, context, normalized_phone, name, note)

def main() -> None:
    logger.debug("Starting main function")
    logger.info("Инициализация бота")
try:
    # Установка кодировки UTF-8
    os.environ["LANG"] = "ru_RU.UTF-8"
    app = Application.builder().token(TELEGRAM_TOKEN).build()
    # Добавление обработчика ошибок
    async def error_handler(update: Update, context: CallbackContext) -> None:
        """Логирует ошибки и отправляет сообщение пользователю."""
        logger.error(f"Произошла ошибка: {context.error}", exc_info=True)
        if update and update.message:
            await update.message.reply_text(
                "❌ Произошла ошибка. Пожалуйста, попробуйте позже.",
                parse_mode="Markdown"
            )

    app.add_error_handler(error_handler)

    conv_handler = ConversationHandler(
        entry_points=[
            MessageHandler(filters.Text(["Ввести вручную"]), manual_entry),
            MessageHandler(filters.TEXT & ~filters.COMMAND, handle_full_input),
        ],
        states={
            PHONE: [MessageHandler(filters.TEXT & ~filters.COMMAND, phone_received)],
            NAME: [
                MessageHandler(filters.Regex("(?i)^Отмена$"), cancel),
                MessageHandler(filters.Regex("(?i)^Назад$"), back_to_phone),
                MessageHandler(filters.TEXT & ~filters.COMMAND, name_received),
            ],
            NOTE: [
                MessageHandler(filters.Regex("(?i)^Отмена$"), cancel),
                MessageHandler(filters.Regex("(?i)^Назад$"), back_to_name),
                MessageHandler(filters.TEXT & ~filters.COMMAND, note_received),
            ],
            SOURCE: [
                MessageHandler(filters.Regex("(?i)^Отмена$"), cancel),
                MessageHandler(filters.TEXT & ~filters.COMMAND, source_received),
            ],
        },
        fallbacks=[MessageHandler(filters.Regex("(?i)^Отмена$"), cancel)],
    )

    app.add_handler(CommandHandler("start", start_command))
    app.add_handler(conv_handler)

    logger.info("Бот запущен")
    
    app.run_polling()
except Exception as e:
    logger.critical(f"Ошибка при запуске бота: {str(e)}", exc_info=True)
    raise

if __name__ == "__main__":
    main()