import datetime as dt
import logging
import os
import sys
import time
from logging.handlers import RotatingFileHandler

import requests
import telegram
import telegram.ext as te
from dotenv import load_dotenv
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove

from image_handler import geo_data, qr_data
from kia_api_handler import (grs_location, item_location, kia_names,
                             last_measurement, location, r_kia_id)

load_dotenv()

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

HOST = os.environ['HOST_BOT']

URL_PIEZOMETERS = f'http://{HOST}/api/piezometers/'
URL_MARKS = f'http://{HOST}/api/marks/'

URL_MEASUREMENTS = f'http://{HOST}/api/measurement/'

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

format_log = logging.Formatter('%(asctime)s, %(levelname)s, %(message)s')

logger = logging.getLogger(__name__)

filename_log = os.path.join(BASE_DIR, 'tg-bot.log')

f_handler = RotatingFileHandler(filename_log,
                                maxBytes=512000,
                                backupCount=5)
f_handler.setFormatter(format_log)
logger.addHandler(f_handler)

s_handler = logging.StreamHandler()
s_handler.setFormatter(format_log)
logger.addHandler(s_handler)

try:
    TELEGRAM_TOKEN = os.environ['TELEGRAM_TOKEN']
    USER_ID = os.environ['USER_ID']
    bot = telegram.Bot(token=TELEGRAM_TOKEN)
    updater = te.Updater(token=TELEGRAM_TOKEN)
except Exception as e:
    logger.error(e, exc_info=True)
    sys.exit(1)

REPLY_KEYBOARD = [['Расположение КиА', 'Проверить настройки'],
                  ['Измерения', '/stop']]

info_markup = ReplyKeyboardMarkup(REPLY_KEYBOARD,
                                  resize_keyboard=True,
                                  one_time_keyboard=False)

back_markup = ReplyKeyboardMarkup([['⬅ Вернуться назад']],
                                  resize_keyboard=True,
                                  one_time_keyboard=False)

yesno_markup = ReplyKeyboardMarkup([['Да', 'Нет']],
                                   resize_keyboard=True,
                                   one_time_keyboard=False)

INFO, PHOTO, CHECK, TO_DB, MAP, CHECK_GEO, CHECK_VALUE = range(7)

qr_kia_name = ''
measurement_status = ''
result = 0

radius = 0.0002  # Переменная с радиусом поиска. Примерно ~20 метров.


def start(update, context):
    """Выводит стартовое сообщение, передаёт управление
    обработчику первого диалога.
    """
    start_text = ('Привет! 😊 \n Я - бот для работы с контрольно-'
                  'измерительной аппаратурой. Для получения '
                  'информации о местоположении КИА - нажмите '
                  '*"Расположение КиА"*. Для того, чтобы внести '
                  'данные измерений - *"Измерения"*. '
                  'Для проверки настроек для работы с ботом - '
                  '*"Проверить настройки"*. Если Вы хотите '
                  'завершить диалог в любой момент времени '
                  'нажмите кнопку, или введите команду */stop*.')

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=start_text, parse_mode='Markdown',
                             reply_markup=info_markup)
    username = update.effective_user['username']
    logger.info(f'Sent start message to {username}')
    return INFO


def stop(update, context):
    """Останавливает все диалоги."""
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text='Хорошего дня! 🙂 ',
                             reply_markup=ReplyKeyboardRemove())
    username = update.effective_user['username']
    logger.info(f'Sent good bye message to {username}')
    return te.ConversationHandler.END


def first_response(update, context):
    """Перенаправляет запросы первого диалога."""
    choice = update.message.text
    if choice.lower() == 'расположение киа':
        kia_location(update, context)
        return MAP
    elif choice.lower() == 'проверить настройки':
        check_geo(update, context)
        return CHECK_GEO
    elif choice.lower() == 'измерения':
        measurement(update, context)
        return PHOTO
    else:
        text = 'Не знаю такой команды 🤯 '
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text)
        username = update.effective_user['username']
        logger.info(f'{username} entered unknown command')
    return INFO


def to_info(update, context):
    """Возвращает к старту по команде "назад",
    сообщает в чат при неизвестной команде.
    """
    choice = update.message.text
    if choice.lower() == '⬅ вернуться назад':
        start(update, context)
    else:
        text = 'Не знаю такой команды 🤯 '
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text)
        username = update.effective_user['username']
        logger.info(f'{username} entered unknown command')
    return INFO


def to_photo_or_database(update, context):
    """Возвращает к отправке фотографий по команде
    "нет" если пользователь не хочет вводить данные измерений,
    или передаёт управление на функцию валидации
    измерений по команде "да".
    """
    choice = update.message.text
    if choice.lower() == 'нет':
        measurement(update, context)
        return PHOTO
    if choice.lower() == 'да':
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text='Введите цифрой: ',
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())
        return TO_DB


def to_photo_or_post_to_db(update, context):
    """Возвращает к отправке фотографий по команде
    "нет" если пользователь не хочет вводить данные измерений,
    или отправляет данные в БД.
    """
    choice = update.message.text
    if choice.lower() == 'нет':
        measurement(update, context)
        return PHOTO
    if choice.lower() == 'да':
        return post_to_db(update, context)


def measurement_validator(update, context):
    """Валидизирует введенные данные, выводит в чат измерения,
    которые ввёл пользователь, вызывает функцию отправки
    в базу данных если данные валидны.
    """
    global qr_kia_name
    global measurement_status
    global result
    choice = update.message.text

    username = update.effective_user['username']

    if ',' in choice:
        choice = choice.replace(',', '.')
    try:
        result = round(float(choice), 2)

    except ValueError as e:
        logger.info(f'{username} sent wrong measurement value {e}')
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text='Попробуйте ещё раз. Требуется число ',
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())
        return TO_DB

    last_value = last_measurement(url=URL_MEASUREMENTS)

    if last_value != 0 and result != 0 and (
            (result >= 1.2 * last_value) or (result <= 0.8 * last_value)):

        percent = round(abs((result * 100 / last_value) - 100), 2)

        logger.info(f'{username} entered value different '
                    f'from previous by {percent} %')
        text = (f'Вы ввели данные, расходящиеся с предыдущими на '
                f'*{percent}%*. Данные предыдущих измерений - '
                f'*{last_value}*. Всё равно хотите внести измерения '
                f'в базу?')
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text,
                                 parse_mode='Markdown',
                                 reply_markup=yesno_markup)
        return CHECK_VALUE
    else:
        return post_to_db(update, context)


def post_to_db(update, context):
    """Отправляет данные в базу данных."""
    global qr_kia_name
    global measurement_status
    global result

    now = dt.datetime.now()

    username = update.effective_user['username']

    kia_id = r_kia_id(url=URL_PIEZOMETERS, name=qr_kia_name)

    if update.effective_user['first_name'] is None:
        first_name = '--'
    else:
        first_name = update.effective_user['first_name']
    if update.effective_user['last_name'] is None:
        last_name = '--'
    else:
        last_name = update.effective_user['last_name']

    author = f'{first_name} {last_name}'

    data = {
        'kia': kia_id,
        'date': now,
        'value': result,
        'status': measurement_status,
        'author': author
    }

    logger.info(f'Data created: {data} by {username}')
    r = requests.post(URL_MEASUREMENTS, data=data)
    logger.info(f'Code: {r.status_code}')

    if r.status_code == 201:
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=f'Вы ввели *{result}*. Данные сохранены'
                                      f' для выбранной КиА ',
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())
    else:
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=f'Вы ввели *{result}*. Данные не '
                                      f'подтверждены для выбранной КиА. '
                                      f'Проверьте, не ошиблись ли вы при '
                                      f'вводе данных? 🧐 Попробуйте '
                                      f'ещё.',
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())
        return TO_DB

    qr_kia_name = ''
    measurement_status = ''
    result = 0

    time.sleep(2)  # Пользователь должен успеть прочитать сообщение

    measurement(update, context)
    return PHOTO


def wrong_file_handler(update, context, to_return):
    """Обрабатывает событие: пользователь отправил в чат фото не
    в виде вложенного файла.
     """
    username = update.effective_user['username']
    text = 'Отправьте, пожалуйста, фото в виде файла'
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=text)
    logger.info(f'{username} send a photo instead of attachment')
    return to_return


def photo(update, context):
    """Проверяет отправлено ли фото в виде вложения, отправляет
    в чат распознанные данные (QR-код, геокоординаты, дату
    и время), валидирует данные. Если данные валидны -
    передаёт управление на to_photo (подтверждение желания
    пользователя отправить данные измерений).
    """
    global measurement_status

    username = update.effective_user['username']

    file_id = file_get(update, context)
    qr_item = 'qr.jpg'
    file_id.download(qr_item)

    qr = qr_validator(update, context, qr_item, 'Пьезометр')

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=f"*Данные QR-кода:* \n"
                                  f"{qr['text']}",
                             parse_mode='Markdown')

    geo = geo_validator(qr_item)

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=f"*Данные съемки:*\n"
                                  f"{geo['text']}",
                             parse_mode='Markdown',
                             reply_markup=back_markup)
    os.remove('qr.jpg')
    os.remove('resized.jpg')

    if geo['is_loc_photo_valid']:
        check = 'рядом с КиА'
        logger.info(f'{username} made a photo near from KiA')
        username = update.effective_user['username']
    else:
        check = 'далеко от КиА'
        measurement_status = 'не подтверждено'
        logger.info(f'{username} made a photo long ago from KiA')

    if qr['is_qr_valid'] and geo['is_geo_valid']:
        logger.info(f'Photo sent by {username} is ok')
        text = (f"*Съемка выполнена:* {check} \n"
                f"Вы хотите внести данные измерений {qr_kia_name} ?")
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text, parse_mode='Markdown',
                                 reply_markup=yesno_markup)
        return CHECK

    return PHOTO


def measurement(update, context):
    """Даёт в чат информацию о действиях с фотографией."""
    text = (
        '*Сфотографируйте и отправьте в чат QR-код, размещенный на КиА.* \n'
        'Не забудьте включить проставление геометок на фотографии. \n'
        '\nПроставление геометок включается на iPhone следующим образом: \n'
        ' 1. Переходим в _Настройки_ – _Конфиденциальность_ – '
        '_Службы геолокации_. \n'
        ' 2. Активируем главный переключатель. \n'
        ' 3. Далее в разделе Камера выбираем: '
        '_при использовании приложения_ и '
        '_точная геопозиция_\n'
        ' \nБольше информации по ссылке: \n'
        ' [Как включить геоданные](https://clck.ru/XG39G)'
    )

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=text, parse_mode='Markdown',
                             reply_markup=back_markup)


def to_map_or_back(update, context):
    """
    Выводит на экран расположение КиА по имени,
    обрабатывает ошибку ввода в случае неточного
    совпадения имени КиА с именем из БД.
    :param update: служебный параметр
    :param context: служебный параметр
    :return: MAP
    """
    choice = update.message.text
    username = update.effective_user['username']
    if choice.lower() == '⬅ вернуться назад':
        start(update, context)
        return INFO
    else:
        api_location = item_location(url=URL_PIEZOMETERS, name=choice)
        if api_location == {}:
            api_location = item_location(url=URL_MARKS, name=choice)
        try:
            context.bot.send_location(chat_id=update.effective_chat.id,
                                      latitude=api_location['x'],
                                      longitude=api_location['y'],
                                      horizontal_accuracy=0,
                                      reply_markup=back_markup)
            text = 'Введите ещё одно название или нажмите *"назад"*'
            context.bot.send_message(chat_id=update.effective_chat.id,
                                     text=text, parse_mode='Markdown',
                                     reply_markup=back_markup)
            logger.info(f'{username} got KiA location')
        except KeyError as e:
            logger.info(f'{username} made a mistake in KiA name ', e)
            text = ('Вы ввели неправильное название, либо '
                    'не существующую в системе КиА 😲 '
                    'Попробуйте ещё.')

            context.bot.send_message(chat_id=update.effective_chat.id,
                                     text=text, parse_mode='Markdown',
                                     reply_markup=back_markup)

        return MAP


def kia_location(update, context):
    """Возвращает расположение КиА по команде:
    'Расположение КиА' в первом диалоге."""
    try:
        text = location(url=URL_PIEZOMETERS)
    except Exception as e:
        text = f'Ошибка подключения к серверу {e}'
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text)
        logger.error(e, exc_info=True)
        return INFO

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=f'*Данные по '
                                  f'пьезометрам:* \n{text}',
                             parse_mode='Markdown')
    text = location(url=URL_MARKS)
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=f'*Данные по '
                                  f'маркам:* \n{text}',
                             parse_mode='Markdown')
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text='Введите наименование '
                                  'КИА в любом регистре для '
                                  'получения геопозиции: ',
                             reply_markup=back_markup,
                             parse_mode='Markdown')


def calibration(update, context):
    """Принимает фотографию и отдайт данные из
    её тегов виде координат и геоточки. Предлагает
    пользвателю включить геоданные при их отсуствии,
    либо включить точные геоданные при их визуальном
     несовпадении.
     """
    file_id = file_get(update, context)
    photo_item = 'photo.jpg'
    file_id.download(photo_item)

    username = update.effective_user['username']

    geo = geo_data(filename=photo_item)
    if not geo:
        text = ('У вас выключено геопозиционирование '
                'для фотографий. Включите, и попробуйте '
                'ещё раз')
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text,
                                 parse_mode='Markdown',
                                 reply_markup=back_markup)
        logger.info(f'{username} has wrong photo settings')
    else:
        x = geo['Latitude']
        y = geo['Longitude']
        time = geo['DateTime']

        text = (f'*Данные съемки:* \n'
                f'Координата Х: {x}\n'
                f'Координата У: {y}\n'
                f'Время: {time}')

        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text,
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())

        context.bot.send_location(chat_id=update.effective_chat.id,
                                  latitude=x,
                                  longitude=y,
                                  horizontal_accuracy=0,
                                  reply_markup=ReplyKeyboardRemove())

        logger.info(f'{username} got his photo geo-tags')

        text = ('Если  фотография была сделана Вами более чем '
                'на 20 метров дальше от присланных координат - '
                'необходимо настроить точное распознавание в телефоне')

        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text,
                                 parse_mode='Markdown',
                                 reply_markup=back_markup)
    os.remove(photo_item)

    return CHECK_GEO


def check_geo(update, context):
    """Возвращает сообщение по команде: 'Калибровка''
    в первом диалоге."""
    text = ('Для того чтобы успешно работало распознавание '
            'фотографий, необходимо, чтобы в телефоне:\n'
            '✅ Было включено *геопозиционирование*\n'
            '✅ Была включена настройка *"передавать'
            ' точные данные"*.\n '
            'Чтобы это проверить, сделайте фотографию и '
            'отправьте её в чат в виде файла')
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=text, parse_mode='Markdown',
                             reply_markup=back_markup)


def qr_validator(update, context, qr_item, kia_type: str):
    """
    Проверяет:
    1. Распознался ли QR-код
    2. Есть ли в нём kia_type: str
    3. Есть ли указанная КиА в БД бэкенда
    :param context: служебная информация
    :param update: служебная информация
    :param qr_item: имя изображения
    :param kia_type: тип КиА
    :return: {'text': текст для отправки в чат
             'is_qr_valid': true - если валидация пройдена
             false - если какая-либо проверка не пройдена}
    """

    global qr_kia_name

    username = update.effective_user['username']

    is_qr_valid = False
    q = qr_data(qr_item)

    if len(q) == 0:
        text = ('Не удалось распознать QR-код 😞 '
                'Попробуйте сделать ещё одну фотографию')
        logger.info(f'QR provided by {username} is not '
                    f'recognized {len(q)}')
    else:

        try:
            kia_names(URL_PIEZOMETERS)
        except Exception as e:
            text = f'Ошибка подключения к серверу {e}'
            context.bot.send_message(chat_id=update.effective_chat.id,
                                     text=text)
            logger.error(e, exc_info=True)
            os.remove('qr.jpg')
            os.remove('resized.jpg')

            time.sleep(2)  # Чтобы пользователь прочитал информацию

            start(update, context)

        if f'{kia_type}: ' in q:
            qr_kia_name = q[(q.find(": ") + 2): q.find(", ")]
            if qr_kia_name in kia_names(URL_PIEZOMETERS):
                text = q
                is_qr_valid = True
            else:
                text = (f'Пьезометр {qr_kia_name} не зарегистрирован '
                        f'в базе 😞')
                logger.info(f'{username} send qr with wrong KiA')
        else:
            text = 'QR-код не содержит информации по пьезометрам 😞'
            logger.info(f'{username} send qr with no KiA')

    logger.info(f'QR provided by {username} is recognized')
    return {'text': text, 'is_qr_valid': is_qr_valid}


def geo_validator(qr_item):
    """
    Проверяет:
    1. Распознались ли геотеги
    2. Рядом ли сделано фото
    :param qr_item: имя изображения
    :return: {'text': текст для отправки в чат
              'is_geo_valid': true - если валидация пройдена
              false, если проверка не пройдена
              'is_loc_photo_valid': true - если валидация
              пройдена false, если проверка не пройдена}
    """
    is_geo_valid = False
    is_loc_photo_valid = False
    g = geo_data(qr_item)

    if len(g) == 0:
        text = ('Не удалось прочитать геотеги из файла 😞 '
                'Пожалуйста, включите геопозиционирование фотографий '
                'в Вашем телефоне.')
        logger.debug('GEO not recognized')
    else:
        text = (f"Фотография сделана: \n{g['DateTime']}\n"
                f"Координата Х: {g['Latitude']}\n"
                f"Координата У: {g['Longitude']}")
        is_geo_valid = True
        is_loc_photo_valid = photo_location_validator(
            g['Latitude'], g['Longitude'],
            qr_kia_name, radius=radius
        )
        logger.debug('GEO is recognized')
    return {'text': text,
            'is_geo_valid': is_geo_valid,
            'is_loc_photo_valid': is_loc_photo_valid}


def photo_location_validator(x, y, name, radius):
    """
    Проверяет рядом ли с КиА делалась фотография.
    :param x: широта из фото
    :param y: долгота из фото
    :param name: наименование КиА
    :param radius: радиус требуемый
    :return: True/False
    """
    location = grs_location(URL_PIEZOMETERS, name)
    xc = location['x']
    yc = location['y']
    if (x - xc) ** 2 + (y - yc) ** 2 <= radius ** 2:
        return True
    else:
        return False


def file_get(update, context):
    """Обрабатывает приём файлов."""
    try:
        update.message.photo[-1].get_file()
        wrong_file_handler(update, context, to_return=CHECK_GEO)
        return
    except IndexError:
        file_id = context.bot.getFile(update.message.document.file_id)
    logger.debug('File received')
    text = '📩 Файл получен. Обрабатываю....'
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=text, reply_markup=ReplyKeyboardRemove())
    return file_id


def main():
    """Обработчик диалогов."""
    logger.info('Bot started')

    conv_handler = te.ConversationHandler(
        entry_points=[te.CommandHandler('start', start)],
        states={
            INFO: [te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                     first_response, pass_user_data=True)],
            PHOTO: [
                te.MessageHandler(te.Filters.photo, photo),
                te.MessageHandler(te.Filters.attachment, photo),
                te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                  to_info, pass_user_data=True)
            ],
            CHECK: [te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                      to_photo_or_database,
                                      pass_user_data=True)],
            TO_DB: [te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                      measurement_validator,
                                      pass_user_data=True)],
            MAP: [te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                    to_map_or_back, pass_user_data=True)],
            CHECK_GEO: [
                te.MessageHandler(te.Filters.photo & (~ te.Filters.text),
                                  calibration),
                te.MessageHandler(te.Filters.attachment & (~ te.Filters.text),
                                  calibration),
                te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                  to_info, pass_user_data=True)
            ],
            CHECK_VALUE: [te.MessageHandler(
                te.Filters.text & (~ te.Filters.command),
                to_photo_or_post_to_db, pass_user_data=True)],
        },
        fallbacks=[te.CommandHandler('stop', stop)]
    )

    dispatcher = updater.dispatcher

    dispatcher.add_handler(conv_handler)

    updater.start_polling()
    updater.idle()


if __name__ == '__main__':
    main()

import datetime as dt
import logging
import os
import sys
import time
from logging.handlers import RotatingFileHandler

import requests
import telegram
import telegram.ext as te
from dotenv import load_dotenv
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove

from image_handler import geo_data, qr_data
from kia_api_handler import (grs_location, item_location, kia_names,
                             last_measurement, location, r_kia_id)

load_dotenv()

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

HOST = os.environ['HOST_BOT']

URL_PIEZOMETERS = f'http://{HOST}/api/piezometers/'
URL_MARKS = f'http://{HOST}/api/marks/'

URL_MEASUREMENTS = f'http://{HOST}/api/measurement/'

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

format_log = logging.Formatter('%(asctime)s, %(levelname)s, %(message)s')

logger = logging.getLogger(__name__)

filename_log = os.path.join(BASE_DIR, 'tg-bot.log')

f_handler = RotatingFileHandler(filename_log,
                                maxBytes=512000,
                                backupCount=5)
f_handler.setFormatter(format_log)
logger.addHandler(f_handler)

s_handler = logging.StreamHandler()
s_handler.setFormatter(format_log)
logger.addHandler(s_handler)

try:
    TELEGRAM_TOKEN = os.environ['TELEGRAM_TOKEN']
    USER_ID = os.environ['USER_ID']
    bot = telegram.Bot(token=TELEGRAM_TOKEN)
    updater = te.Updater(token=TELEGRAM_TOKEN)
except Exception as e:
    logger.error(e, exc_info=True)
    sys.exit(1)

REPLY_KEYBOARD = [['Расположение КиА', 'Проверить настройки'],
                  ['Измерения', '/stop']]

info_markup = ReplyKeyboardMarkup(REPLY_KEYBOARD,
                                  resize_keyboard=True,
                                  one_time_keyboard=False)

back_markup = ReplyKeyboardMarkup([['⬅ Вернуться назад']],
                                  resize_keyboard=True,
                                  one_time_keyboard=False)

yesno_markup = ReplyKeyboardMarkup([['Да', 'Нет']],
                                   resize_keyboard=True,
                                   one_time_keyboard=False)

INFO, PHOTO, CHECK, TO_DB, MAP, CHECK_GEO, CHECK_VALUE = range(7)

qr_kia_name = ''
measurement_status = ''
result = 0

radius = 0.0002  # Переменная с радиусом поиска. Примерно ~20 метров.


def start(update, context):
    """Выводит стартовое сообщение, передаёт управление
    обработчику первого диалога.
    """
    start_text = ('Привет! 😊 \n Я - бот для работы с контрольно-'
                  'измерительной аппаратурой. Для получения '
                  'информации о местоположении КИА - нажмите '
                  '*"Расположение КиА"*. Для того, чтобы внести '
                  'данные измерений - *"Измерения"*. '
                  'Для проверки настроек для работы с ботом - '
                  '*"Проверить настройки"*. Если Вы хотите '
                  'завершить диалог в любой момент времени '
                  'нажмите кнопку, или введите команду */stop*.')

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=start_text, parse_mode='Markdown',
                             reply_markup=info_markup)
    username = update.effective_user['username']
    logger.info(f'Sent start message to {username}')
    return INFO


def stop(update, context):
    """Останавливает все диалоги."""
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text='Хорошего дня! 🙂 ',
                             reply_markup=ReplyKeyboardRemove())
    username = update.effective_user['username']
    logger.info(f'Sent good bye message to {username}')
    return te.ConversationHandler.END


def first_response(update, context):
    """Перенаправляет запросы первого диалога."""
    choice = update.message.text
    if choice.lower() == 'расположение киа':
        kia_location(update, context)
        return MAP
    elif choice.lower() == 'проверить настройки':
        check_geo(update, context)
        return CHECK_GEO
    elif choice.lower() == 'измерения':
        measurement(update, context)
        return PHOTO
    else:
        text = 'Не знаю такой команды 🤯 '
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text)
        username = update.effective_user['username']
        logger.info(f'{username} entered unknown command')
    return INFO


def to_info(update, context):
    """Возвращает к старту по команде "назад",
    сообщает в чат при неизвестной команде.
    """
    choice = update.message.text
    if choice.lower() == '⬅ вернуться назад':
        start(update, context)
    else:
        text = 'Не знаю такой команды 🤯 '
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text)
        username = update.effective_user['username']
        logger.info(f'{username} entered unknown command')
    return INFO


def to_photo_or_database(update, context):
    """Возвращает к отправке фотографий по команде
    "нет" если пользователь не хочет вводить данные измерений,
    или передаёт управление на функцию валидации
    измерений по команде "да".
    """
    choice = update.message.text
    if choice.lower() == 'нет':
        measurement(update, context)
        return PHOTO
    if choice.lower() == 'да':
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text='Введите цифрой: ',
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())
        return TO_DB


def to_photo_or_post_to_db(update, context):
    """Возвращает к отправке фотографий по команде
    "нет" если пользователь не хочет вводить данные измерений,
    или отправляет данные в БД.
    """
    choice = update.message.text
    if choice.lower() == 'нет':
        measurement(update, context)
        return PHOTO
    if choice.lower() == 'да':
        return post_to_db(update, context)


def measurement_validator(update, context):
    """Валидизирует введенные данные, выводит в чат измерения,
    которые ввёл пользователь, вызывает функцию отправки
    в базу данных если данные валидны.
    """
    global qr_kia_name
    global measurement_status
    global result
    choice = update.message.text

    username = update.effective_user['username']

    if ',' in choice:
        choice = choice.replace(',', '.')
    try:
        result = round(float(choice), 2)

    except ValueError as e:
        logger.info(f'{username} sent wrong measurement value {e}')
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text='Попробуйте ещё раз. Требуется число ',
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())
        return TO_DB

    last_value = last_measurement(url=URL_MEASUREMENTS)

    if last_value != 0 and result != 0 and (
            (result >= 1.2 * last_value) or (result <= 0.8 * last_value)):

        percent = round(abs((result * 100 / last_value) - 100), 2)

        logger.info(f'{username} entered value different '
                    f'from previous by {percent} %')
        text = (f'Вы ввели данные, расходящиеся с предыдущими на '
                f'*{percent}%*. Данные предыдущих измерений - '
                f'*{last_value}*. Всё равно хотите внести измерения '
                f'в базу?')
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text,
                                 parse_mode='Markdown',
                                 reply_markup=yesno_markup)
        return CHECK_VALUE
    else:
        return post_to_db(update, context)


def post_to_db(update, context):
    """Отправляет данные в базу данных."""
    global qr_kia_name
    global measurement_status
    global result

    now = dt.datetime.now()

    username = update.effective_user['username']

    kia_id = r_kia_id(url=URL_PIEZOMETERS, name=qr_kia_name)

    if update.effective_user['first_name'] is None:
        first_name = '--'
    else:
        first_name = update.effective_user['first_name']
    if update.effective_user['last_name'] is None:
        last_name = '--'
    else:
        last_name = update.effective_user['last_name']

    author = f'{first_name} {last_name}'

    data = {
        'kia': kia_id,
        'date': now,
        'value': result,
        'status': measurement_status,
        'author': author
    }

    logger.info(f'Data created: {data} by {username}')
    r = requests.post(URL_MEASUREMENTS, data=data)
    logger.info(f'Code: {r.status_code}')

    if r.status_code == 201:
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=f'Вы ввели *{result}*. Данные сохранены'
                                      f' для выбранной КиА ',
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())
    else:
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=f'Вы ввели *{result}*. Данные не '
                                      f'подтверждены для выбранной КиА. '
                                      f'Проверьте, не ошиблись ли вы при '
                                      f'вводе данных? 🧐 Попробуйте '
                                      f'ещё.',
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())
        return TO_DB

    qr_kia_name = ''
    measurement_status = ''
    result = 0

    time.sleep(2)  # Пользователь должен успеть прочитать сообщение

    measurement(update, context)
    return PHOTO


def wrong_file_handler(update, context, to_return):
    """Обрабатывает событие: пользователь отправил в чат фото не
    в виде вложенного файла.
     """
    username = update.effective_user['username']
    text = 'Отправьте, пожалуйста, фото в виде файла'
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=text)
    logger.info(f'{username} send a photo instead of attachment')
    return to_return


def photo(update, context):
    """Проверяет отправлено ли фото в виде вложения, отправляет
    в чат распознанные данные (QR-код, геокоординаты, дату
    и время), валидирует данные. Если данные валидны -
    передаёт управление на to_photo (подтверждение желания
    пользователя отправить данные измерений).
    """
    global measurement_status

    username = update.effective_user['username']

    file_id = file_get(update, context)
    qr_item = 'qr.jpg'
    file_id.download(qr_item)

    qr = qr_validator(update, context, qr_item, 'Пьезометр')

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=f"*Данные QR-кода:* \n"
                                  f"{qr['text']}",
                             parse_mode='Markdown')

    geo = geo_validator(qr_item)

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=f"*Данные съемки:*\n"
                                  f"{geo['text']}",
                             parse_mode='Markdown',
                             reply_markup=back_markup)
    os.remove('qr.jpg')
    os.remove('resized.jpg')

    if geo['is_loc_photo_valid']:
        check = 'рядом с КиА'
        logger.info(f'{username} made a photo near from KiA')
        username = update.effective_user['username']
    else:
        check = 'далеко от КиА'
        measurement_status = 'не подтверждено'
        logger.info(f'{username} made a photo long ago from KiA')

    if qr['is_qr_valid'] and geo['is_geo_valid']:
        logger.info(f'Photo sent by {username} is ok')
        text = (f"*Съемка выполнена:* {check} \n"
                f"Вы хотите внести данные измерений {qr_kia_name} ?")
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text, parse_mode='Markdown',
                                 reply_markup=yesno_markup)
        return CHECK

    return PHOTO


def measurement(update, context):
    """Даёт в чат информацию о действиях с фотографией."""
    text = (
        '*Сфотографируйте и отправьте в чат QR-код, размещенный на КиА.* \n'
        'Не забудьте включить проставление геометок на фотографии. \n'
        '\nПроставление геометок включается на iPhone следующим образом: \n'
        ' 1. Переходим в _Настройки_ – _Конфиденциальность_ – '
        '_Службы геолокации_. \n'
        ' 2. Активируем главный переключатель. \n'
        ' 3. Далее в разделе Камера выбираем: '
        '_при использовании приложения_ и '
        '_точная геопозиция_\n'
        ' \nБольше информации по ссылке: \n'
        ' [Как включить геоданные](https://clck.ru/XG39G)'
    )

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=text, parse_mode='Markdown',
                             reply_markup=back_markup)


def to_map_or_back(update, context):
    """
    Выводит на экран расположение КиА по имени,
    обрабатывает ошибку ввода в случае неточного
    совпадения имени КиА с именем из БД.
    :param update: служебный параметр
    :param context: служебный параметр
    :return: MAP
    """
    choice = update.message.text
    username = update.effective_user['username']
    if choice.lower() == '⬅ вернуться назад':
        start(update, context)
        return INFO
    else:
        api_location = item_location(url=URL_PIEZOMETERS, name=choice)
        if api_location == {}:
            api_location = item_location(url=URL_MARKS, name=choice)
        try:
            context.bot.send_location(chat_id=update.effective_chat.id,
                                      latitude=api_location['x'],
                                      longitude=api_location['y'],
                                      horizontal_accuracy=0,
                                      reply_markup=back_markup)
            text = 'Введите ещё одно название или нажмите *"назад"*'
            context.bot.send_message(chat_id=update.effective_chat.id,
                                     text=text, parse_mode='Markdown',
                                     reply_markup=back_markup)
            logger.info(f'{username} got KiA location')
        except KeyError as e:
            logger.info(f'{username} made a mistake in KiA name ', e)
            text = ('Вы ввели неправильное название, либо '
                    'не существующую в системе КиА 😲 '
                    'Попробуйте ещё.')

            context.bot.send_message(chat_id=update.effective_chat.id,
                                     text=text, parse_mode='Markdown',
                                     reply_markup=back_markup)

        return MAP


def kia_location(update, context):
    """Возвращает расположение КиА по команде:
    'Расположение КиА' в первом диалоге."""
    try:
        text = location(url=URL_PIEZOMETERS)
    except Exception as e:
        text = f'Ошибка подключения к серверу {e}'
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text)
        logger.error(e, exc_info=True)
        return INFO

    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=f'*Данные по '
                                  f'пьезометрам:* \n{text}',
                             parse_mode='Markdown')
    text = location(url=URL_MARKS)
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=f'*Данные по '
                                  f'маркам:* \n{text}',
                             parse_mode='Markdown')
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text='Введите наименование '
                                  'КИА в любом регистре для '
                                  'получения геопозиции: ',
                             reply_markup=back_markup,
                             parse_mode='Markdown')


def calibration(update, context):
    """Принимает фотографию и отдайт данные из
    её тегов виде координат и геоточки. Предлагает
    пользвателю включить геоданные при их отсуствии,
    либо включить точные геоданные при их визуальном
     несовпадении.
     """
    file_id = file_get(update, context)
    photo_item = 'photo.jpg'
    file_id.download(photo_item)

    username = update.effective_user['username']

    geo = geo_data(filename=photo_item)
    if not geo:
        text = ('У вас выключено геопозиционирование '
                'для фотографий. Включите, и попробуйте '
                'ещё раз')
        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text,
                                 parse_mode='Markdown',
                                 reply_markup=back_markup)
        logger.info(f'{username} has wrong photo settings')
    else:
        x = geo['Latitude']
        y = geo['Longitude']
        time = geo['DateTime']

        text = (f'*Данные съемки:* \n'
                f'Координата Х: {x}\n'
                f'Координата У: {y}\n'
                f'Время: {time}')

        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text,
                                 parse_mode='Markdown',
                                 reply_markup=ReplyKeyboardRemove())

        context.bot.send_location(chat_id=update.effective_chat.id,
                                  latitude=x,
                                  longitude=y,
                                  horizontal_accuracy=0,
                                  reply_markup=ReplyKeyboardRemove())

        logger.info(f'{username} got his photo geo-tags')

        text = ('Если  фотография была сделана Вами более чем '
                'на 20 метров дальше от присланных координат - '
                'необходимо настроить точное распознавание в телефоне')

        context.bot.send_message(chat_id=update.effective_chat.id,
                                 text=text,
                                 parse_mode='Markdown',
                                 reply_markup=back_markup)
    os.remove(photo_item)

    return CHECK_GEO


def check_geo(update, context):
    """Возвращает сообщение по команде: 'Калибровка''
    в первом диалоге."""
    text = ('Для того чтобы успешно работало распознавание '
            'фотографий, необходимо, чтобы в телефоне:\n'
            '✅ Было включено *геопозиционирование*\n'
            '✅ Была включена настройка *"передавать'
            ' точные данные"*.\n '
            'Чтобы это проверить, сделайте фотографию и '
            'отправьте её в чат в виде файла')
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=text, parse_mode='Markdown',
                             reply_markup=back_markup)


def qr_validator(update, context, qr_item, kia_type: str):
    """
    Проверяет:
    1. Распознался ли QR-код
    2. Есть ли в нём kia_type: str
    3. Есть ли указанная КиА в БД бэкенда
    :param context: служебная информация
    :param update: служебная информация
    :param qr_item: имя изображения
    :param kia_type: тип КиА
    :return: {'text': текст для отправки в чат
             'is_qr_valid': true - если валидация пройдена
             false - если какая-либо проверка не пройдена}
    """

    global qr_kia_name

    username = update.effective_user['username']

    is_qr_valid = False
    q = qr_data(qr_item)

    if len(q) == 0:
        text = ('Не удалось распознать QR-код 😞 '
                'Попробуйте сделать ещё одну фотографию')
        logger.info(f'QR provided by {username} is not '
                    f'recognized {len(q)}')
    else:

        try:
            kia_names(URL_PIEZOMETERS)
        except Exception as e:
            text = f'Ошибка подключения к серверу {e}'
            context.bot.send_message(chat_id=update.effective_chat.id,
                                     text=text)
            logger.error(e, exc_info=True)
            os.remove('qr.jpg')
            os.remove('resized.jpg')

            time.sleep(2)  # Чтобы пользователь прочитал информацию

            start(update, context)

        if f'{kia_type}: ' in q:
            qr_kia_name = q[(q.find(": ") + 2): q.find(", ")]
            if qr_kia_name in kia_names(URL_PIEZOMETERS):
                text = q
                is_qr_valid = True
            else:
                text = (f'Пьезометр {qr_kia_name} не зарегистрирован '
                        f'в базе 😞')
                logger.info(f'{username} send qr with wrong KiA')
        else:
            text = 'QR-код не содержит информации по пьезометрам 😞'
            logger.info(f'{username} send qr with no KiA')

    logger.info(f'QR provided by {username} is recognized')
    return {'text': text, 'is_qr_valid': is_qr_valid}


def geo_validator(qr_item):
    """
    Проверяет:
    1. Распознались ли геотеги
    2. Рядом ли сделано фото
    :param qr_item: имя изображения
    :return: {'text': текст для отправки в чат
              'is_geo_valid': true - если валидация пройдена
              false, если проверка не пройдена
              'is_loc_photo_valid': true - если валидация
              пройдена false, если проверка не пройдена}
    """
    is_geo_valid = False
    is_loc_photo_valid = False
    g = geo_data(qr_item)

    if len(g) == 0:
        text = ('Не удалось прочитать геотеги из файла 😞 '
                'Пожалуйста, включите геопозиционирование фотографий '
                'в Вашем телефоне.')
        logger.debug('GEO not recognized')
    else:
        text = (f"Фотография сделана: \n{g['DateTime']}\n"
                f"Координата Х: {g['Latitude']}\n"
                f"Координата У: {g['Longitude']}")
        is_geo_valid = True
        is_loc_photo_valid = photo_location_validator(
            g['Latitude'], g['Longitude'],
            qr_kia_name, radius=radius
        )
        logger.debug('GEO is recognized')
    return {'text': text,
            'is_geo_valid': is_geo_valid,
            'is_loc_photo_valid': is_loc_photo_valid}


def photo_location_validator(x, y, name, radius):
    """
    Проверяет рядом ли с КиА делалась фотография.
    :param x: широта из фото
    :param y: долгота из фото
    :param name: наименование КиА
    :param radius: радиус требуемый
    :return: True/False
    """
    location = grs_location(URL_PIEZOMETERS, name)
    xc = location['x']
    yc = location['y']
    if (x - xc) ** 2 + (y - yc) ** 2 <= radius ** 2:
        return True
    else:
        return False


def file_get(update, context):
    """Обрабатывает приём файлов."""
    try:
        update.message.photo[-1].get_file()
        wrong_file_handler(update, context, to_return=CHECK_GEO)
        return
    except IndexError:
        file_id = context.bot.getFile(update.message.document.file_id)
    logger.debug('File received')
    text = '📩 Файл получен. Обрабатываю....'
    context.bot.send_message(chat_id=update.effective_chat.id,
                             text=text, reply_markup=ReplyKeyboardRemove())
    return file_id


def main():
    """Обработчик диалогов."""
    logger.info('Bot started')

    conv_handler = te.ConversationHandler(
        entry_points=[te.CommandHandler('start', start)],
        states={
            INFO: [te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                     first_response, pass_user_data=True)],
            PHOTO: [
                te.MessageHandler(te.Filters.photo, photo),
                te.MessageHandler(te.Filters.attachment, photo),
                te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                  to_info, pass_user_data=True)
            ],
            CHECK: [te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                      to_photo_or_database,
                                      pass_user_data=True)],
            TO_DB: [te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                      measurement_validator,
                                      pass_user_data=True)],
            MAP: [te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                    to_map_or_back, pass_user_data=True)],
            CHECK_GEO: [
                te.MessageHandler(te.Filters.photo & (~ te.Filters.text),
                                  calibration),
                te.MessageHandler(te.Filters.attachment & (~ te.Filters.text),
                                  calibration),
                te.MessageHandler(te.Filters.text & (~ te.Filters.command),
                                  to_info, pass_user_data=True)
            ],
            CHECK_VALUE: [te.MessageHandler(
                te.Filters.text & (~ te.Filters.command),
                to_photo_or_post_to_db, pass_user_data=True)],
        },
        fallbacks=[te.CommandHandler('stop', stop)]
    )

    dispatcher = updater.dispatcher

    dispatcher.add_handler(conv_handler)

    updater.start_polling()
    updater.idle()


if __name__ == '__main__':
    main()

# Переменная с радиусом поиска. Примерно ~20 метров. Выводит стартовое сообщение, передаёт управление     обработчику первого диалога. Останавливает все диалоги. Перенаправляет запросы первого диалога. Возвращает к старту по команде "назад",     сообщает в чат при неизвестной команде. Возвращает к отправке фотографий по команде     "нет" если пользователь не хочет вводить данные измерений,     или передаёт управление на функцию валидации     измерений по команде "да". Возвращает к отправке фотографий по команде     "нет" если пользователь не хочет вводить данные измерений,     или отправляет данные в БД. Валидизирует введенные данные, выводит в чат измерения,     которые ввёл пользователь, вызывает функцию отправки     в базу данных если данные валидны. Отправляет данные в базу данных. # Пользователь должен успеть прочитать сообщение Обрабатывает событие: пользователь отправил в чат фото не     в виде вложенного файла. Проверяет отправлено ли фото в виде вложения, отправляет     в чат распознанные данные (QR-код, геокоординаты, дату     и время), валидирует данные. Если данные валидны -     передаёт управление на to_photo (подтверждение желания     пользователя отправить данные измерений). Даёт в чат информацию о действиях с фотографией. Выводит на экран расположение КиА по имени,     обрабатывает ошибку ввода в случае неточного     совпадения имени КиА с именем из БД.     :param update: служебный параметр     :param context: служебный параметр     :return: MAP Возвращает расположение КиА по команде:     'Расположение КиА' в первом диалоге. Принимает фотографию и отдайт данные из     её тегов виде координат и геоточки. Предлагает     пользвателю включить геоданные при их отсуствии,     либо включить точные геоданные при их визуальном      несовпадении. Возвращает сообщение по команде: 'Калибровка''     в первом диалоге. Проверяет:     1. Распознался ли QR-код     2. Есть ли в нём kia_type: str     3. Есть ли указанная КиА в БД бэкенда     :param context: служебная информация     :param update: служебная информация     :param qr_item: имя изображения     :param kia_type: тип КиА     :return: {'text': текст для отправки в чат              'is_qr_valid': true - если валидация пройдена              false - если какая-либо проверка не пройдена} # Чтобы пользователь прочитал информацию Проверяет:     1. Распознались ли геотеги     2. Рядом ли сделано фото     :param qr_item: имя изображения     :return: {'text': текст для отправки в чат               'is_geo_valid': true - если валидация пройдена               false, если проверка не пройдена               'is_loc_photo_valid': true - если валидация               пройдена false, если проверка не пройдена} Проверяет рядом ли с КиА делалась фотография.     :param x: широта из фото     :param y: долгота из фото     :param name: наименование КиА     :param radius: радиус требуемый     :return: True/False Обрабатывает приём файлов. Обработчик диалогов.