import base64
import json

from Cryptodome.Cipher import PKCS1_OAEP
from Cryptodome.PublicKey import RSA
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QBrush, QColor
from PyQt5.QtWidgets import QMainWindow, qApp, QMessageBox

from chatclient.сhatclient.Log.client_log_config import client_log
from chatclient.сhatclient.Mainlib.exception import ServerError
from chatclient.сhatclient.Mainlib.variables import MESSAGE_TEXT, SENDER
from chatclient.сhatclient.client.add_contact_window import AddContactDialog
from chatclient.сhatclient.client.client_main_window_conv import Ui_MainClientWindow
from chatclient.сhatclient.client.del_contact_window import DelContactDialog


class ClientMainWindow(QMainWindow):
	"""
	Class is the main user window.
    Contains all the main logic of the client module.
    The window configuration is created in QTDesigner and loaded from
    converted file main_window_conv.py
	"""
	
	def __init__(self, database, transport, keys):
		super().__init__()
		# основные переменные
		self.database = database
		self.transport = transport
		
		# объект - дешифорвщик сообщений с предзагруженным ключём
		self.decrypter = PKCS1_OAEP.new(keys)
		
		# Загружаем конфигурацию окна из дизайнера
		self.ui = Ui_MainClientWindow()
		self.ui.setupUi(self)
		
		# Кнопка "Выход"
		self.ui.menu_exit.triggered.connect(qApp.exit)
		
		# Кнопка отправить сообщение
		self.ui.btn_send.clicked.connect(self.send_message)
		
		# "добавить контакт"
		self.ui.btn_add_contact.clicked.connect(self.add_contact_window)
		self.ui.menu_add_contact.triggered.connect(self.add_contact_window)
		
		# Удалить контакт
		self.ui.btn_remove_contact.clicked.connect(self.delete_contact_window)
		self.ui.menu_del_contact.triggered.connect(self.delete_contact_window)
		
		# Дополнительные требующиеся атрибуты
		self.contacts_model = None
		self.history_model = None
		self.messages = QMessageBox()
		self.current_chat = None
		self.current_chat_key = None
		self.encryptor = None
		self.ui.list_messages.setHorizontalScrollBarPolicy(
			Qt.ScrollBarAlwaysOff)
		self.ui.list_messages.setWordWrap(True)
		
		# Даблклик по листу контактов отправляется в обработчик
		self.ui.list_contacts.doubleClicked.connect(self.select_active_user)
		
		self.clients_list_update()
		self.set_disabled_input()
		self.show()
	
	def set_disabled_input(self):
		"""
		Method making input fields inactive
		:return:
		"""
		# Надпись  - получатель.
		self.ui.label_new_message.setText(
			'Для выбора получателя дважды кликните на нем в окне контактов.')
		self.ui.text_message.clear()
		if self.history_model:
			self.history_model.clear()
		
		# Поле ввода и кнопка отправки неактивны до выбора получателя.
		self.ui.btn_clear.setDisabled(True)
		self.ui.btn_send.setDisabled(True)
		self.ui.text_message.setDisabled(True)
		
		self.encryptor = None
		self.current_chat = None
		self.current_chat_key = None
	
	def history_list_update(self):
		"""
		The filling method of the corresponding QListView
        history of correspondence with the current interlocutor.
		:return:
		"""
		# Получаем историю сортированную по дате
		list = sorted(
			self.database.get_history(
				self.current_chat),
			key=lambda item: item[3])
		# Если модель не создана, создадим.
		if not self.history_model:
			self.history_model = QStandardItemModel()
			self.ui.list_messages.setModel(self.history_model)
		# Очистим от старых записей
		self.history_model.clear()
		# Берём не более 20 последних записей.
		length = len(list)
		start_index = 0
		if length > 20:
			start_index = length - 20
		# Заполнение модели записями, так-же стоит разделить входящие
		# и исходящие выравниванием и разным фоном.
		# отображает только последие 20 сообщений
		for i in range(start_index, length):
			item = list[i]
			if item[1] == 'in':
				mess = QStandardItem(
					f'Входящее от {item[3].replace(microsecond=0)}:\n {item[2]}')
				mess.setEditable(False)
				mess.setBackground(QBrush(QColor(255, 213, 213)))
				mess.setTextAlignment(Qt.AlignLeft)
				self.history_model.appendRow(mess)
			else:
				mess = QStandardItem(
					f'Исходящее от {item[3].replace(microsecond=0)}:\n {item[2]}')
				mess.setEditable(False)
				mess.setTextAlignment(Qt.AlignRight)
				mess.setBackground(QBrush(QColor(204, 255, 204)))
				self.history_model.appendRow(mess)
		self.ui.list_messages.scrollToBottom()
	
	def select_active_user(self):
		"""
		Double click event handler method on the contact list
		:return:
		"""
		# Выбранный пользователем (даблклик) находится в выделеном элементе в
		# QListView
		# online_users_list = ServerStorage.active_users_list() # получаем активных пользователей в
		# # списке кортежей из базы сервера
		# self.current_chat = self.ui.list_contacts.currentIndex().data()
		# if self.current_chat in online_users_list.count():  # проверяем список активных пользователей
		#     self.set_active_user()  # вызываем основную функцию
		# else:
		#     self.messages.warning(
		#         self, 'Ошибка', 'Данный пользователь не в сети.')
		
		self.current_chat = self.ui.list_contacts.currentIndex().data()
		online_users_list = self.transport.online_users_list_request()  # получаем активных пользователей в
		# списке кортежей из базы сервера
		if self.current_chat in online_users_list:
			self.set_active_user()  # вызываем основную функцию
		else:
			self.messages.warning(self, 'Ошибка', 'Данный пользователь не в сети.')
	
	def set_active_user(self):
		"""
		A method for activating a chat with an interlocutor.
		:return:
		"""
		# Запрашиваем публичный ключ пользователя и создаём объект шифрования
		try:
			self.current_chat_key = self.transport.key_request(
				self.current_chat)
			client_log.debug(f'Загружен открытый ключ для {self.current_chat}')
			if self.current_chat_key:
				self.encryptor = PKCS1_OAEP.new(
					RSA.import_key(self.current_chat_key))
		except (OSError, json.JSONDecodeError):
			self.current_chat_key = None
			self.encryptor = None
			client_log.debug(f'Не удалось получить ключ для {self.current_chat}')
		
		# Если ключа нет то ошибка, что не удалось начать чат с пользователем
		if not self.current_chat_key:
			self.messages.warning(
				self, 'Ошибка', 'Для выбранного пользователя нет ключа шифрования.')
			return
		
		# Ставим надпись и активируем кнопки
		self.ui.label_new_message.setText(
			f'Введите сообщенние для {self.current_chat}:')
		self.ui.btn_clear.setDisabled(False)
		self.ui.btn_send.setDisabled(False)
		self.ui.text_message.setDisabled(False)
		
		# Заполняем окно историю сообщений по требуемому пользователю.
		self.history_list_update()
	
	def clients_list_update(self):
		"""
		Method for updating the contact list.
		:return:
		"""
		contacts_list = self.database.get_contacts()
		self.contacts_model = QStandardItemModel()
		for i in sorted(contacts_list):
			item = QStandardItem(i)
			item.setEditable(False)
			self.contacts_model.appendRow(item)
		self.ui.list_contacts.setModel(self.contacts_model)
	
	def add_contact_window(self):
		"""
		Method creating a window - a dialog for adding a contact
		:return:
		"""
		global select_dialog
		select_dialog = AddContactDialog(self.transport, self.database)
		select_dialog.btn_ok.clicked.connect(
			lambda: self.add_contact_action(select_dialog))
		select_dialog.show()
	
	def add_contact_action(self, item):
		"""
		Method to handle clicking the 'Add' button
		:param item:
		:return:
		"""
		new_contact = item.selector.currentText()
		self.add_contact(new_contact)
		item.close()
	
	def add_contact(self, new_contact):
		"""
		Method adding contact to server and client BD.
        After updating the databases, the contents of the window are also updated.
		:param new_contact:
		:return:
		"""
		try:
			self.transport.add_contact(new_contact)
		except ServerError as err:
			self.messages.critical(self, 'Ошибка сервера', err.text)
		except OSError as err:
			if err.errno:
				self.messages.critical(
					self, 'Ошибка', 'Потеряно соединение с сервером!')
				self.close()
			self.messages.critical(self, 'Ошибка', 'Таймаут соединения!')
		else:
			self.database.add_contact(new_contact)
			new_contact = QStandardItem(new_contact)
			new_contact.setEditable(False)
			self.contacts_model.appendRow(new_contact)
			client_log.info(f'Успешно добавлен контакт {new_contact}')
			self.messages.information(
				self, 'Успех', 'Контакт успешно добавлен.')
	
	def delete_contact_window(self):
		"""
		A method that creates a window for deleting a contact.
		:return:
		"""
		global remove_dialog
		remove_dialog = DelContactDialog(self.database)
		remove_dialog.btn_ok.clicked.connect(
			lambda: self.delete_contact(remove_dialog))
		remove_dialog.show()
	
	def delete_contact(self, item):
		"""
		Method for removing contact from server and client BD.
        After updating the databases, the contents of the window are also updated.
		:param item:
		:return:
		"""
		selected = item.selector.currentText()
		try:
			self.transport.remove_contact(selected)
		except ServerError as err:
			self.messages.critical(self, 'Ошибка сервера', err.text)
		except OSError as err:
			if err.errno:
				self.messages.critical(
					self, 'Ошибка', 'Потеряно соединение с сервером!')
				self.close()
			self.messages.critical(self, 'Ошибка', 'Таймаут соединения!')
		else:
			self.database.del_contact(selected)
			self.clients_list_update()
			client_log.info(f'Успешно удалён контакт {selected}')
			self.messages.information(self, 'Успех', 'Контакт успешно удалён.')
			item.close()
			# Если удалён активный пользователь, то деактивируем поля ввода.
			if selected == self.current_chat:
				self.current_chat = None
				self.set_disabled_input()
	
	def send_message(self):
		"""
		The function of sending a message to the current interlocutor.
		Implements message encryption and sending.
		"""
		# Текст в поле, проверяем что поле не пустое затем забирается сообщение
		# и поле очищается
		message_text = self.ui.text_message.toPlainText()
		self.ui.text_message.clear()
		if not message_text:
			return
		# Шифруем сообщение ключом получателя и упаковываем в base64.
		message_text_encrypted = self.encryptor.encrypt(
			message_text.encode('utf8'))
		message_text_encrypted_base64 = base64.b64encode(
			message_text_encrypted)
		try:
			self.transport.send_message(
				self.current_chat,
				message_text_encrypted_base64.decode('ascii'))
			pass
		except ServerError as err:
			self.messages.critical(self, 'Ошибка', err.text)
		except OSError as err:
			if err.errno:
				self.messages.critical(
					self, 'Ошибка', 'Потеряно соединение с сервером!')
				self.close()
			self.messages.critical(self, 'Ошибка', 'Таймаут соединения!')
		except (ConnectionResetError, ConnectionAbortedError):
			self.messages.critical(
				self, 'Ошибка', 'Потеряно соединение с сервером!')
			self.close()
		else:
			self.database.save_message(self.current_chat, 'out', message_text)
			client_log.debug(
				f'Отправлено сообщение для {self.current_chat}: {message_text}')
			self.history_list_update()
	
	@pyqtSlot(dict)
	def message(self, message):
		"""
		Slot handler of incoming messages, performs decryption
        received messages and their saving in the message history.
        Asks the user if the message is not from the current one
        interlocutor. Changes the interlocutor if necessary.
		:param message:
		:return:
		"""
		# Получаем строку байтов
		encrypted_message = base64.b64decode(message[MESSAGE_TEXT])
		# Декодируем строку, при ошибке выдаём сообщение и завершаем функцию
		try:
			decrypted_message = self.decrypter.decrypt(encrypted_message)
		except (ValueError, TypeError):
			self.messages.warning(
				self, 'Ошибка', 'Не удалось декодировать сообщение.')
			return
		# Сохраняем сообщение в базу и обновляем историю сообщений или
		# открываем новый чат.
		self.database.save_message(
			self.current_chat,
			'in',
			decrypted_message.decode('utf8'))
		
		sender = message[SENDER]
		if sender == self.current_chat:
			self.history_list_update()
		else:
			# Проверим есть ли такой пользователь у нас в контактах:
			if self.database.check_contact(sender):
				# Если есть, спрашиваем и желании открыть с ним чат и открываем
				# при желании
				if self.messages.question(
						self,
						'Новое сообщение',
						f'Получено новое сообщение от {sender}, открыть чат с ним?',
						QMessageBox.Yes,
						QMessageBox.No) == QMessageBox.Yes:
					self.current_chat = sender
					self.set_active_user()
			else:
				print('NO')
				# Раз нету,спрашиваем хотим ли добавить юзера в контакты.
				if self.messages.question(
						self,
						'Новое сообщение',
						f'Получено новое сообщение от {sender}.\n Данного пользователя нет в вашем контакт-листе.\n '
						f'Добавить в контакты и открыть чат с ним?',
						QMessageBox.Yes,
						QMessageBox.No) == QMessageBox.Yes:
					self.add_contact(sender)
					self.current_chat = sender
					# Нужно заново сохранить сообщение, иначе оно будет потеряно,
					# т.к. на момент предыдущего вызова контакта не было.
					self.database.save_message(
						self.current_chat, 'in', decrypted_message.decode('utf8'))
					self.set_active_user()
	
	@pyqtSlot()
	def connection_lost(self):
		"""
		Slot handler for lost connection to the server.
        Shows a warning window and exits the application.
		:return:
		"""
		self.messages.warning(
			self,
			'Сбой соединения',
			'Потеряно соединение с сервером. ')
		self.close()
	
	@pyqtSlot()
	def sig_205(self):
		"""
		Slot performing database update on server command
		"""
		if self.current_chat and not self.database.check_user(
				self.current_chat):
			self.messages.warning(
				self,
				'Сочувствую',
				'К сожалению собеседник был удалён с сервера.')
			self.set_disabled_input()
			self.current_chat = None
		self.clients_list_update()
	
	def make_connection(self, trans_obj):
		"""
		Method for connecting signals and slots
		:param trans_obj:
		:return:
		"""
		trans_obj.new_message.connect(self.message)
		trans_obj.connection_lost.connect(self.connection_lost)
		trans_obj.message_205.connect(self.sig_205)
