import threading
import logging
import select
import socket
import json
import hmac
import binascii
import os

from common.descriptors import ServerPort, ServerAddress
from common.settings import MAX_CONNECTIONS, DESTINATION, SENDER, ACTION, \
    PRESENCE, MESSAGE, MESSAGE_TEXT, USER, TIME, ACCOUNT_NAME, ERROR, \
    RESPONSE_200, RESPONSE_400, EXIT, GET_CONTACTS, LIST_INFO, ADD_CONTACT, \
    RESPONSE_202, REMOVE_CONTACT, PUBLIC_KEY_REQUEST, RESPONSE_511, DATA, \
    USERS_REQUEST, RESPONSE, RESPONSE_205, PUBLIC_KEY
from common.utils import send_message, recv_message
from common.decos import login_required

# Инициализация логирования
SERVER_LOGGER = logging.getLogger('server')


class MessageProcessor(threading.Thread):
    '''
    Основной класс сервера. Принимает содинения, словари - пакеты
    от клиентов, обрабатывает поступающие сообщения.
    Работает в качестве отдельного потока.
    '''

    port = ServerPort()
    addr = ServerAddress()

    def __init__(self, listen_address, listen_port, database):
        # Параментры подключения:
        self.addr = listen_address
        self.port = listen_port

        # База данных сервера:
        self.database = database

        # Сокет, через который будет осуществляться работа:
        self.sock = None

        # Список подключённых клиентов:
        self.clients = []

        # Сокеты:
        self.listen_sockets = None
        self.error_sockets = None

        # Флаг продолжения работы:
        self.running = True

        # Словарь содержащий сопоставленные имена и соответствующие им сокеты:
        self.names = dict()
        # Конструктор родителя:
        super().__init__()

    def run(self):
        '''Метод основной цикл потока.'''
        # Инициализация Сокета:
        self.init_socket()

        # Основной цикл программы сервера:
        while self.running:
            # Ждём подключения, если таймаут вышел, ловим исключение:
            try:
                client, client_address = self.sock.accept()
            except OSError:
                pass
            else:
                SERVER_LOGGER.info(f'Установлено соединение с адресом: '
                                   f'{client_address}')
                client.settimeout(5)
                self.clients.append(client)

            recv_msg_lst = []
            send_msg_lst = []
            err_lst = []
            # Проверяем на наличие ждущих клиентов:
            try:
                if self.clients:
                    recv_msg_lst, self.listen_sockets, \
                    self.error_sockets = select.select(
                        self.clients, self.clients, [], 0)
            except OSError as err:
                SERVER_LOGGER.error(f'Ошибка работы с сокетами: {err.errno}')

            # Принимаем сообщения и если ошибка, исключаем клиента:
            if recv_msg_lst:
                for client_with_msg in recv_msg_lst:
                    try:
                        self.client_msg_handler(
                            recv_message(client_with_msg), client_with_msg)
                    except (OSError, json.JSONDecodeError, TypeError):
                        self.remove_client(client_with_msg)

    def remove_client(self, client):
        '''
        Метод обработчик клиента с которым прервана связь.
        Ищет клиента и удаляет его из списков и базы:
        '''
        SERVER_LOGGER.info(f'Клиент {client.getpeername()} '
                           f'отключился от сервера.')
        for name in self.names:
            if self.names[name] == client:
                self.database.user_login(name)
                del self.names[name]
                break
        self.clients.remove(client)
        client.close()

    def init_socket(self):
        '''Метод инициализатор сокета.'''
        # Готовим сокет:
        transport = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        transport.bind((self.addr, self.port))
        transport.settimeout(0.5)

        # Начинаем слушать сокет:
        self.sock = transport
        self.sock.listen(MAX_CONNECTIONS)

        SERVER_LOGGER.info(f'Запущен сервер с портом для подключений: '
                           f'{self.port}, '
                           f'адрес с которого принимаются подключения: '
                           f'{self.addr}. '
                           f'Если адрес не указан, соединения будут приниматься '
                           f'с любых адресов')
        print('Сервер запущен')

    def proccess_message(self, message):
        '''
        Метод отправки сообщения клиенту.
        '''
        if message[DESTINATION] in self.names \
                and self.names[message[DESTINATION]] in self.listen_sockets:
            try:
                send_message(self.names[message[DESTINATION]], message)
                SERVER_LOGGER.info(f'Отправлено сообщение пользователю '
                                   f'{message[DESTINATION]} от пользователя '
                                   f'{message[SENDER]}.')
            except OSError:
                self.remove_client(message[DESTINATION])
        elif message[DESTINATION] in self.names \
                and self.names[message[DESTINATION]] not in self.listen_sockets:
            SERVER_LOGGER.error(f'Связь с клиентом {message[DESTINATION]} '
                                f'была потеряна. Соединение закрыто, '
                                f'доставка невозможна.')
            self.remove_client(self.names[message[DESTINATION]])
        else:
            SERVER_LOGGER.error(f'Пользователь {message[DESTINATION]} '
                                f'не зарегистрирован на сервере, '
                                f'отправка сообщения невозможна.')

    @login_required
    def client_msg_handler(self, message, client):
        '''Метод отбработчик поступающих сообщений.'''
        SERVER_LOGGER.debug(f'Обработка сообщения от клиента: {message}')
        # Если сообщение о присутствии, принимаем и отвечаем
        if ACTION in message and message[ACTION] == PRESENCE \
                and TIME in message and USER in message:
            # Если сообщение о присутствии то вызываем функцию авторизации:
            self.autorize_user(message, client)

        # Если это сообщение, то добавляем его в очередь:
        elif ACTION in message and message[ACTION] == MESSAGE \
                and DESTINATION in message and TIME in message \
                and SENDER in message and MESSAGE_TEXT in message \
                and self.names[message[SENDER]] == client:
            if message[DESTINATION] in self.names:
                self.database.process_message(message[SENDER],
                                          message[DESTINATION])
                self.proccess_message(message)
                try:
                    send_message(client, RESPONSE_200)
                except OSError:
                    self.remove_client(client)
            else:
                response = RESPONSE_400
                response[ERROR] = 'Пользователь не зарегистрирован на сервере.'
                try:
                    send_message(client, response)
                except OSError:
                    pass
            return

        # Если клиент выходит:
        elif ACTION in message and message[ACTION] == EXIT \
                and ACCOUNT_NAME in message \
                and self.names[message[ACCOUNT_NAME]] == client:
            self.remove_client(client)

        # Если запрос контакт листа:
        elif ACTION in message and message[ACTION] == GET_CONTACTS \
                and USER in message and self.names[message[USER]] == client:
            response = RESPONSE_202
            response[LIST_INFO] = self.database.get_contacts(message[USER])
            try:
                send_message(client, response)
            except OSError:
                self.remove_client(client)

        # Если добаваление контакта:
        elif ACTION in message and message[ACTION] == ADD_CONTACT and \
                ACCOUNT_NAME in message and USER in message \
                and self.names[message[USER]] == client:
            self.database.add_contact(message[USER], message[ACCOUNT_NAME])
            try:
                send_message(client, RESPONSE_200)
            except OSError:
                self.remove_client(client)

        # Если удаление контакта:
        elif ACTION in message and message[ACTION] == REMOVE_CONTACT \
            and ACCOUNT_NAME in message and USER in message \
                and self.names[message[USER]] == client:
            self.database.remove_contact(message[USER], message[ACCOUNT_NAME])
            try:
                send_message(client, RESPONSE_200)
            except OSError:
                self.remove_client(client)

        # Если запрос известных пользователей:
        elif ACTION in message and message[ACTION] == USERS_REQUEST \
                and ACCOUNT_NAME in message \
                and self.names[message[ACCOUNT_NAME]] == client:
            response = RESPONSE_202
            response[LIST_INFO] = [user[0]
                                   for user in self.database.users_list()]
            try:
                send_message(client, response)
            except OSError:
                self.remove_client(client)

        # Если это запрос публичного ключа пользователя:
        elif ACTION in message and message[ACTION] == PUBLIC_KEY_REQUEST \
                and ACCOUNT_NAME in message:
                response = RESPONSE_511
                response[DATA] = self.database.get_pubkey(message[ACCOUNT_NAME])
                # может быть, что ключа ещё нет (пользователь никогда
                # не логинился, тогда шлём 400)
                if response[DATA]:
                    try:
                        send_message(client, response)
                    except OSError:
                        self.remove_client(client)
                else:
                    response = RESPONSE_400
                    response[ERROR] = 'Нет публичного ключа ' \
                                      'для данного пользователя'
                    try:
                        send_message(client, response)
                    except OSError:
                        self.remove_client(client)

        # Иначе - Bad Request:
        else:
            response = RESPONSE_400
            response[ERROR] = 'Запрос некорректен.'
            try:
                send_message(client, response)
            except OSError:
                self.remove_client(client)
            return

    def autorize_user(self, message, sock):
        '''Метод реализующий авторизцию пользователей.'''
        # Если имя пользователя уже занято то возвращаем 400:
        if message[USER][ACCOUNT_NAME] in self.names.keys():
            response = RESPONSE_400
            response[ERROR] = 'Имя пользователя уже занято.'
            try:
                send_message(sock, response)
            except OSError:
                pass
            self.clients.remove(sock)
            sock.close()
        # Проверяем что пользователь зарегистрирован на сервере:
        elif not self.database.check_user(message[USER][ACCOUNT_NAME]):
            response = RESPONSE_400
            response[ERROR] = 'Пользователь не зарегистрирован.'
            try:
                send_message(sock, response)
            except OSError:
                pass
            self.clients.remove(sock)
            sock.close()
        else:
            # Иначе отвечаем 511 и проводим процедуру авторизации
            # Словарь - заготовка:
            message_auth = RESPONSE_511
            # Набор байтов в hex представлении:
            random_str = binascii.hexlify(os.urandom(64))
            # В словарь байты нельзя, декодируем (json.dumps -> TypeError):
            message_auth[DATA] = random_str.decode('ascii')
            # Создаём хэш пароля и связки с рандомной строкой,
            # сохраняем серверную версию ключа:
            hash = hmac.new(
                self.database.get_hash(message[USER][ACCOUNT_NAME]),
                random_str)
            digest = hash.digest()
            try:
                # Обмен с клиентом:
                send_message(sock, message_auth)
                ans = recv_message(sock)
            except OSError:
                sock.close()
                return
            client_digest = binascii.a2b_base64(ans[DATA])
            # Если ответ клиента корректный, то сохраняем его
            # в список пользователей:
            if RESPONSE in ans and ans[RESPONSE] == 511 \
                    and hmac.compare_digest(digest, client_digest):
                self.names[message[USER][ACCOUNT_NAME]] = sock
                client_ip, client_port = sock.getpeername()
                try:
                    send_message(sock, RESPONSE_200)
                except OSError:
                    self.remove_client(message[USER][ACCOUNT_NAME])
                # Добавляем пользователя в список активных и если
                # у него изменился открытый ключ сохраняем новый:
                self.database.user_login(message[USER][ACCOUNT_NAME],
                                         client_ip, client_port,
                                         message[USER][PUBLIC_KEY])
            else:
                response = RESPONSE_400
                response[ERROR] = 'Неверный пароль.'
                try:
                    send_message(sock, response)
                except OSError:
                    pass
                self.clients.remove(sock)
                sock.close()

    # Функция - отправляет сервисное сообщение 205 с требованием клиентам
    # обновить списки:
    def service_update_lists(self):
        '''Метод реализующий отправки сервисного сообщения 205 клиентам.'''
        for client in self.names:
            try:
                send_message(self.names[client], RESPONSE_205)
            except OSError:
                self.remove_client(self.names[client])