from binascii import hexlify, b2a_base64
from hashlib import pbkdf2_hmac
import hmac
from json import JSONDecodeError
from logging import getLogger
from socket import socket, AF_INET, SOCK_STREAM
from threading import Lock, Thread
from time import sleep, time

from PyQt5.QtCore import QObject, pyqtSignal

from common.errors import ServerError
from common.variables import ATTEMPTS, ACTION, PRESENCE, TIME, USER, \
    ACCOUNT_NAME, RESPONSE, ERROR, MESSAGE, SENDER, DESTINATION, \
    MESSAGE_TEXT, GET_CONTACTS, LIST_INFO, USERS_REQUEST, ADD_CONTACT, \
    REMOVE_CONTACT, EXIT, PUBLIC_KEY, DATA, RESPONSE_511, PUBLIC_KEY_REQUEST
from common.utils import send_message, get_message

# Инициализация клиентского логера
logger = getLogger('client')
socket_lock = Lock()


class ClientTransport(Thread, QObject):
    """
    Класс реализующий транспортную подсистему клиентского модуля.
    Отвечает за взаимодействие с сервером.
    """
    # Сигналы: новое сообщение и потеря соединения
    new_message = pyqtSignal(dict)
    message_205 = pyqtSignal()
    connection_lost = pyqtSignal()

    def __init__(self, port, ip_address, db, username, password, keys):
        Thread.__init__(self)
        QObject.__init__(self)

        self.db = db
        self.username = username
        self.password = password
        self.transport = None
        self.keys = keys
        self.connection_init(port, ip_address)

        try:
            self.user_list_update()
            self.contacts_list_update()
        except OSError as err:
            if err.errno:
                logger.critical('Потеряно соединение с сервером')
                raise ServerError('Потеряно соединение с сервером!')
            logger.error('Timeout при обновлении списков пользователей')
        except JSONDecodeError:
            logger.critical('Потеряно соединение с сервером')
            raise ServerError('Потеряно соединение с сервером!')
        self.running = True

    def connection_init(self, port, ip):
        """Метод отвечающий за устанновку соединения с сервером."""
        self.transport = socket(AF_INET, SOCK_STREAM)
        self.transport.settimeout(5)

        connected = False

        for attempt in range(ATTEMPTS):
            logger.info(f'Попытка подключения №{attempt + 1}')
            try:
                self.transport.connect((ip, port))
            except (OSError, ConnectionRefusedError):
                pass
            else:
                connected = True
                logger.debug("Соединение установлено")
                break
            sleep(1)

        if not connected:
            logger.critical('Не удалось установить соединение с сервером')
            raise ServerError('Не удалось установить соединение с сервером')
        logger.debug('Диалог запуска авторизации')

        # Запускаем процедуру авторизации
        # Получаем хэш пароля
        password_bytes = self.password.encode('utf-8')
        salt = self.username.lower().encode('utf-8')
        password_hash = pbkdf2_hmac('sha512', password_bytes, salt, 10000)
        password_hash_string = hexlify(password_hash)

        logger.debug(f'Хэш пароля готов: {password_hash_string}')

        # Получаем публичный ключ и декодируем его из байтов
        pubkey = self.keys.publickey().export_key().decode('ascii')

        # Авторизируемся на сервере
        with socket_lock:
            presence = {ACTION: PRESENCE,
                        TIME: time(),
                        USER: {
                            ACCOUNT_NAME: self.username,
                            PUBLIC_KEY: pubkey}}
            logger.debug(f'Сообщение присутствия = {presence}')

            try:
                send_message(self.transport, presence)
                ans = get_message(self.transport)
                logger.debug(f'Ответ сервера = {ans}.')

                if RESPONSE in ans:
                    if ans[RESPONSE] == 400:
                        raise ServerError(ans[ERROR])
                    elif ans[RESPONSE] == 511:
                        ans_data = ans[DATA]
                        hash_ = hmac.new(password_hash_string,
                                        ans_data.encode('utf-8'), 'MD5')
                        digest = hash_.digest()
                        ans_ = RESPONSE_511
                        ans_[DATA] = b2a_base64(digest).decode('ascii')
                        send_message(self.transport, ans_)
                        self.process_server_ans(get_message(self.transport))
            except (OSError, JSONDecodeError) as err:
                logger.debug('Ошибка связи', exc_info=err)
                raise ServerError('Сбой соединения в процессе авторизации')

    def process_server_ans(self, message):
        """Метод обработчик поступающих сообщений с сервера."""
        logger.debug(f'Разбор сообщения от сервера: {message}')

        if RESPONSE in message:
            if message[RESPONSE] == 200:
                return
            elif message[RESPONSE] == 400:
                raise ServerError(f'{message[ERROR]}')
            elif message[RESPONSE] == 205:
                self.user_list_update()
                self.contacts_list_update()
                self.message_205.emit()
            else:
                logger.debug(f'Принят неизвестный код подтверждения '
                             f'{message[RESPONSE]}')

        elif ACTION in message and message[ACTION] == MESSAGE \
                and SENDER in message and DESTINATION in message \
                and MESSAGE_TEXT in message \
                and message[DESTINATION] == self.username:
            logger.debug(f'Получено сообщение от пользователя '
                         f'{message[SENDER]}: {message[MESSAGE_TEXT]}')
            self.new_message.emit(message)

    def contacts_list_update(self):
        """Метод обновляющий с сервера список контактов."""
        logger.debug(f'Запрос контактов для пользователя {self.name}')
        req = {
            ACTION: GET_CONTACTS,
            TIME: time(),
            USER: self.username
        }
        logger.debug(f'Сформирован запрос {req}')

        with socket_lock:
            send_message(self.transport, req)
            ans = get_message(self.transport)
        logger.debug(f'Получен ответ {ans}')
        if RESPONSE in ans and ans[RESPONSE] == 202:
            for contact in ans[LIST_INFO]:
                self.db.add_contact(contact)
        else:
            logger.error('Не удалось обновить список контактов')

    def user_list_update(self):
        """Метод обновляющий с сервера список пользователей."""
        logger.debug(f'Запрос списка известных пользователей {self.username}')
        req = {
            ACTION: USERS_REQUEST,
            TIME: time(),
            ACCOUNT_NAME: self.username
        }

        with socket_lock:
            send_message(self.transport, req)
            ans = get_message(self.transport)
        if RESPONSE in ans and ans[RESPONSE] == 202:
            self.db.add_users(ans[LIST_INFO])
        else:
            logger.error('Не удалось обновить список известных пользователей')

    def key_request(self, user):
        """Метод запрашивающий с сервера публичный ключ пользователя."""
        logger.debug(f'Запрос публичного ключа для {user}')
        req = {
            ACTION: PUBLIC_KEY_REQUEST,
            TIME: time(),
            ACCOUNT_NAME: user
        }
        with socket_lock:
            send_message(self.transport, req)
            ans = get_message(self.transport)
        if RESPONSE in ans and ans[RESPONSE] == 511:
            return ans[DATA]
        logger.error(f'Не удалось получить ключ собеседника {user}.')

    # Сообщает на сервер о добавлении нового контакта
    def add_contact(self, contact):
        """Метод отправляющий на сервер сведения о добавлении контакта."""
        logger.debug(f'Создание контакта {contact}')
        req = {
            ACTION: ADD_CONTACT,
            TIME: time(),
            USER: self.username,
            ACCOUNT_NAME: contact
        }

        with socket_lock:
            send_message(self.transport, req)
            self.process_server_ans(get_message(self.transport))

    def remove_contact(self, contact):
        """Метод отправляющий на сервер сведения о удалении контакта."""
        logger.debug(f'Удаление контакта {contact}')
        req = {
            ACTION: REMOVE_CONTACT,
            TIME: time(),
            USER: self.username,
            ACCOUNT_NAME: contact
        }

        with socket_lock:
            send_message(self.transport, req)
            self.process_server_ans(get_message(self.transport))

    def transport_shutdown(self):
        """Метод уведомляющий сервер о завершении работы клиента."""
        self.running = False
        message = {
            ACTION: EXIT,
            TIME: time(),
            ACCOUNT_NAME: self.username
        }

        with socket_lock:
            try:
                send_message(self.transport, message)
            except OSError:
                pass
        logger.debug('Транспорт завершает работу')
        sleep(0.5)

    def send_message(self, to, message):
        """Метод отправляющий на сервер сообщения для пользователя."""
        message_dict = {
            ACTION: MESSAGE,
            SENDER: self.username,
            DESTINATION: to,
            TIME: time(),
            MESSAGE_TEXT: message
        }
        logger.debug(f'Сформирован словарь сообщения: {message_dict}')

        # Дожидаемся освобождения сокета для отправки сообщения
        with socket_lock:
            send_message(self.transport, message_dict)
            self.process_server_ans(get_message(self.transport))
            logger.info(f'Отправлено сообщение для польователя {to}')

    def run(self):
        """Метод содержащий основной цикл работы транспортного потока."""
        logger.debug('Запущен процесс - приёмник сообщений от сервера')

        while self.running:
            sleep(1)
            message = None

            with socket_lock:
                try:
                    self.transport.settimeout(0.5)
                    message = get_message(self.transport)
                except OSError as err:
                    if err.errno:
                        logger.critical('Потеряно соединение с сервером')
                        self.running = False
                        self.connection_lost.emit()
                except (ConnectionError,
                        ConnectionAbortedError,
                        ConnectionResetError,
                        JSONDecodeError,
                        TypeError):
                    logger.debug('Потеряно соединение с сервером')
                    self.running = False
                    self.connection_lost.emit()
                finally:
                    self.transport.settimeout(5)

            # Если сообщение получено, то вызываем функцию обработчик
            if message:
                logger.debug(f'Принято сообщение с сервера: {message}')
                self.process_server_ans(message)
