#!/usr/bin/env python3

import sys
import os
import re
import signal
import socket
import pickle, traceback
from random import randint
from pathlib import Path

from TCP import server as TCPServer
from UDP import server as UDPServer
from UDP import client as UDPClient

pids = []
imParent = False

class serverTCP(TCPServer.TCPServer):
	
	def __init__(self, HOST, PORT):
		TCPServer.TCPServer.__init__(self, HOST, PORT)
		
		# self.possibleMessages has all the available commands to execute
		self.possibleMessages = {
			# AUT user pass
			'AUT': {
				'function': 'login',
				'commandReturn': 'AUR' # AUR status
			},
			# DLU
			'DLU': {
				'function': 'deleteUser',
				'commandReturn': 'DLR' # DLR status
			},
			# BCK dir N (filename date_time size)*
			# see page 6 ( f) )
			'BCK': {
				'function': 'doBackup',
				'commandReturn': 'BCK' # BKR IPBS portBS N (filename date_time size)*
			},
			# RST dir
			'RST': {
				'function': 'restoreFolder',
				'commandReturn': 'RSR' # RSR IPBS portBS
			},
			# LSD
			'LSD': {
				'function': 'listDirBackedUp',
				'commandReturn': 'LDR' # LDR N (dirname)*
			},
			# LSF dir
			'LSF': {
				'function': 'listFilesBackedUpInDir',
				'commandReturn': 'LFD' # LFD BSip BSport N (filename date_time size)*
			},
			# DEL dir
			'DEL': {
				'function': 'deleteDir',
				'commandReturn': 'DDR' # DDR status
			}
		}
	
	"""
	this function will interpret the message given
	if there is no errors with the input, it will call the right function to execute with the command given
	
	in case of any error, this function will raise an IOError with the corresponding message
	"""
	def interpretMessage(self, message):
		# listMessage is a list. every position will have one word of the input
		listMessage = message.split()
		command = listMessage[0]
		args = listMessage[1:]
		
		# if there is no errors then will execute the right fucntion
		return getattr(self, self.possibleMessages[command]['function'])(args)
	
	def existsUser(self, user):
		return user is not None
	
	def getRandomBS(self):
		backupServers = getStuffFromFile("backupServers")
		
		# backupServers is not empty
		if len(backupServers) > 0:
			backupServers[randint(0, len(backupServers) - 1)]
			return backupServers[randint(0, len(backupServers) - 1)]
		else:
			raise IOError("No Backup Servers connected")
	
	def login(self, args):		
		if not isinstance(args, list) or len(args) != 2 or len(args[0]) != 5 or len(args[1]) != 8:
			raise IOError("AUR ERR")
		try:
			args[0] = int(args[0])
		except ValueError:
			raise IOError("AUR ERR")
			
		if not isinstance(args[1], str):
			raise IOError("AUR ERR")
		
		user, password = args
		
		users = getStuffFromFile("CSUsers")		
		isUser = (user in users.keys())
		correctPassword = (isUser and users[user]['password'] == password)
		
		if isUser and correctPassword:
			self.currentUser = user
			self.currentPassword = password
			print("Login user: " + str(user))
			return "AUR OK"
		elif isUser:
			return "AUR NOK"
		else:
			users[user] = {
				'password': password,
				'dirs': {}
			}
			setStuffToFile("CSUsers", users)
			self.currentUser = user
			self.currentPassword = password
			print("New user: " + str(user))
			return "AUR NEW"
	
	def deleteUser(self, args):
		if not self.existsUser(self.currentUser):
			raise IOError("ERR")
		
		users = getStuffFromFile("CSUsers")
		if users[self.currentUser]['dirs'] and users[self.currentUser]['dirs']:
			return "DLR NOK"
		
		users.pop(self.currentUser, None)
		setStuffToFile("CSUsers", users)
		print("   Delete user: " + str(user))
		
		return "DLR OK"
		
	def doBackup(self, args):		
		if not self.existsUser(self.currentUser):
			raise IOError("ERR")
		
		# verify all args
		if not isinstance(args, list):
			raise IOError("BKR ERR")
			
		lenArgs = len(args)
		try:
			args[1] = int(args[1])
		except ValueError:
			raise IOError("BKR ERR")
		
		if lenArgs < 2 or not isinstance(args[0], str) or lenArgs != args[1] * 4 + 2:
			raise IOError("BKR ERR")
		
		for i in range(2, lenArgs):
			if i % 4 == 2:
				if not isinstance(args[i], str):
					raise IOError("BKR ERR")
			elif i % 4 == 3:
				try:
					day, month, year = re.search('([0-9]{2})\.([0-9]{2})\.([0-9]{4})', args[i]).groups()
				except AttributeError:
					raise IOError("BKR ERR")
			elif i % 4 == 0:
				try:
					hours, minutes, seconds = re.search('([0-2][0-9])\:([0-5][0-9])\:([0-5][0-9])', args[i]).groups()
				except AttributeError:
					raise IOError("BKR ERR")
			else:
				try:
					args[i] = int(args[i])
				except ValueError:
					raise IOError("BKR ERR")
		
		users = getStuffFromFile("CSUsers")
		
		dir = args[0]
		n = args[1]
		filesToUpload = []
		# will construct what files are sent
		for i in range(2, lenArgs, 4):
			file = {}
			file['filename'] = args[i]
			file['date_time'] = args[i + 1] + " " + args[i + 2]
			file['size'] = int(args[i + 3])
			filesToUpload.append(file)
		
		backupServers = getStuffFromFile("backupServers")
		
		print("   BCK " + dir + " of " + str(self.currentUser))
		
		# if the given dir is not saved
		if not users[self.currentUser]['dirs'] or dir not in users[self.currentUser]['dirs'].keys() or users[self.currentUser]['dirs'][dir]['BS'] not in backupServers:
						
			try:
				bs = self.getRandomBS()
			except IOError:
				raise IOError("BKR EOF")
			
			client_UDP = clientUDP(bs)
			try:
				reply = client_UDP.registerUserToBS(self.currentUser, self.currentPassword)
				if reply == 'OK':
					users[self.currentUser]['dirs'][dir] = {'BS': bs}
					setStuffToFile("CSUsers", users)
			except IOError:
				raise IOError("BKR EOF")
				
		else:
			bs = users[self.currentUser]['dirs'][dir]['BS']
			client_UDP = clientUDP(bs)
			try:
				receivedMessage = client_UDP.listFiles(self.currentUser, dir)
			except IOError:
				raise IOError("BKR EOF")
			
			words = receivedMessage.split()
			for i in range(0, len(words), 4):
				file = {}
				file['filename'] = args[i]
				file['date_time'] = args[i + 1] + " " + args[i + 2]
				file['size'] = int(args[i + 3])
				if file in filesToUpload:
					filesToUpload.remove(file)
		
		stringToReturn = "BKR " + str(users[self.currentUser]['dirs'][dir]['BS']['IPBS']) + " " + str(users[self.currentUser]['dirs'][dir]['BS']['portBS']) + " " + str(len(filesToUpload))
		for file in filesToUpload:
			stringToReturn += " " + str(file['filename']) + " " + str(file['date_time']) + " " + str(file['size'])
		print(stringToReturn)
		
		return stringToReturn
		
	def listDirBackedUp(self, args):		
		if not self.existsUser(self.currentUser):
			raise IOError("ERR")
		
		if len(args) != 0:
			raise IOError("ERR")
		
		print("   LDR of user")
		
		users = getStuffFromFile("CSUsers")
		backupServers = getStuffFromFile("backupServers")
		strToReturn = "LDR " + str(len(users[self.currentUser]['dirs'].keys()))
		
		for dir in users[self.currentUser]['dirs'].keys():
			if users[self.currentUser]['dirs'][dir]['BS'] in backupServers:
				strToReturn += " " + dir
		
		return strToReturn
	
	def listFilesBackedUpInDir(self, args):
		if not self.existsUser(self.currentUser):
			raise IOError("ERR")
		
		if len(args) != 1 and not isinstance(args[0], str):
			raise IOError("LSF ERR")
		
		users = getStuffFromFile("CSUsers")
		backupServers = getStuffFromFile("backupServers")
		
		if args[0] not in users[self.currentUser]['dirs']:
			return "LFD NOK"
		
		print("   LFD of " + str(args[0]))
		
		bs = users[self.currentUser]['dirs'][args[0]]['BS']
		
		# we cannot retrieve the files because there is no BS where the dir was saved
		if bs not in backupServers:
			raise IOError("LFD NOK")
		
		client_UDP = clientUDP(bs)
		try:
			files = client_UDP.listFiles(self.currentUser, args[0])
		except IOError:
			raise IOError("LFD NOK")
		
		strToReturn = "LFD " + str(bs['IPBS']) + " " + str(bs['portBS']) + " " + str(len(files))
		
		for file in files:
			strToReturn += " " + str(file['filename']) + " " + str(file['date_time']) + " " + str(file['size'])
		
		return strToReturn
	
	def restoreFolder(self, args):
		if not self.existsUser(self.currentUser):
			raise IOError("ERR")
		
		if len(args) != 1 and not isinstance(args[0], str):
			raise IOError("RSR ERR")
		
		users = getStuffFromFile("CSUsers")
		backupServers = getStuffFromFile("backupServers")
		
		if args[0] not in users[self.currentUser]['dirs']:
			return "RSR EOF"
				
		bs = users[self.currentUser]['dirs'][args[0]]['BS']
				
		if bs not in backupServers:
			raise IOError("RSR EOF")
		
		print("   RST of " + args[0] + " from " + bs['IPBS'] + " " + str(bs['portBS']))
		
		return "RSR " + bs['IPBS'] + " " + str(bs['portBS'])
	
	def deleteDir(self, args):
		try:
			if not self.existsUser(self.currentUser):
				raise IOError("ERR")
			
			if len(args) != 1 and not isinstance(args[0], str):
				raise IOError("DDR NOK")
			
			users = getStuffFromFile("CSUsers")
			backupServers = getStuffFromFile("backupServers")
			
			if args[0] not in users[self.currentUser]['dirs']:
				return "DDR NOK"
			
			bs = users[self.currentUser]['dirs'][args[0]]['BS']
			
			if bs not in backupServers:
				raise IOError("DDR NOK")
			
			print("   DEL " + args[0] + " from " + bs['IPBS'] + " " + str(bs['portBS']))
			
			client_UDP = clientUDP(bs)
			try:
				reply = client_UDP.deleteDir(self.currentUser, args[0])
				if reply == "OK":
					del users[self.currentUser]['dirs'][args[0]]
					setStuffToFile("CSUsers", users)
				
				return "DDR " + reply
			except IOError:
				raise IOError("DDR NOK")
		except Exception as e:
		    exc_type, exc_obj, exc_tb = sys.exc_info()
		    fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
		    print(exc_type, fname, exc_tb.tb_lineno)

class serverUDP(UDPServer.UDPServer):
	
	def __init__(self, HOST, PORT):
		UDPServer.UDPServer.__init__(self, HOST, PORT)
		
		# self.possibleMessages has all the available commands to execute
		self.possibleMessages = {
			# REG IPBS portBS
			'REG': {
				'function': 'registerBS',
				'commandReturn': 'RGR' # RGR status
			},
			# UNR IPBS portBS
			'UNR': {
				'function': 'unregisterBS',
				'commandReturn': 'UAR' # UAR status
			}
		}
		
	
	"""
	this function will interpret the message given
	if there is no errors with the input, it will call the right function to execute with the command given
	
	in case of any error, this function will raise an IOError with the corresponding message
	"""
	def interpretMessage(self, message):
		# listMessage is a list. every position will have one word of the input
		listMessage = message.split()
		command = listMessage[0]
		args = listMessage[1:]
		
		# if there is no errors then will execute the right fucntion
		return getattr(self, self.possibleMessages[command]['function'])(args)
		
	def registerBS(self, args):		
		try:
			args[1] = int(args[1])
		except ValueError:
			raise IOError("RGR ERR")
		
		if len(args) != 2 or not isinstance(args[0], str):
			raise IOError("RGR ERR")
		
		backupServers = getStuffFromFile("backupServers")
		
		bs = {'IPBS': args[0], 'portBS': args[1]}
		if bs not in backupServers:
			backupServers.append(bs)
			setStuffToFile("backupServers", backupServers)
			
			print("+BS:", str(args[0]), str(args[1]))
			return "RGR OK"
		else:
			return "RGR NOK"
	
	def unregisterBS(self, args):		
		try:
			args[1] = int(args[1])
		except ValueError:
			raise IOError("UAR ERR")
		
		if len(args) != 2 or not isinstance(args[0], str):
			raise IOError("UAR ERR")
		
		backupServers = getStuffFromFile("backupServers")
		
		bs = {'IPBS': args[0], 'portBS': args[1]}
		if bs in backupServers:
			backupServers.remove(bs)
			setStuffToFile("backupServers", backupServers)
			
			print("-BS:", str(args[0]), str(args[1]))
			return "UAR OK"
		else:
			return "UAR NOK"

			
class clientUDP(UDPClient.UDPClient):
	
	def __init__(self, bs):
		UDPClient.UDPClient.__init__(self, bs['IPBS'], bs['portBS'])
		# self.possibleMessages has all the available commands to execute
		self.possibleMessages = {
			# LSF user dir
			'LSF': {
				'function': 'listFiles',
				'commandReturn': 'LFD' # LFD N (filename date_time size)*
			},
			# LSU user pass
			'LSU': {
				'function': 'registerUserToBS',
				'commandReturn': 'LUR' # LUR status
			},
			# DLB user dir
			'DLB': {
				'function': 'deleteDir',
				'commandReturn': 'DBR' # DBR status
			}
		}
	
	def listFiles(self, user, dir):
		try:
			user = int(user)
		except ValueError:
			raise IOError("LFD ERR")
		
		if not isinstance(dir, str):
			raise IOError("LFD ERR")
		
		print("chegou aqui")
		reply = self.sendMessage('LSF ' + str(user) + ' ' + dir)		
		print("enviou")	
		reply = reply.split()
			
		try:
			reply[1] = int(reply[1])
		except ValueError:
			raise IOError("LFD ERR")

		print("   LSF " + str(dir) + " from " + str(self.UDP_PORT) + " " + str(self.UDP_IP))
		
		if reply[0] == 'LFD' and (len(reply) - 2) % 4 == 0 and (len(reply) - 2) / 4 == reply[1]:
			files = []
			for i in range(2, reply[1] * 4 + 2, 4):
				file = {}
				file['filename'] = reply[i]
				file['date_time'] = reply[i + 1] + " " + reply[i + 2]
				file['size'] = reply[i + 3]
				files.append(file)
			return files
		else:
			raise IOError("LFD ERR")
	
	def registerUserToBS(self, user, password):
		try:
			user = int(user)
		except ValueError:
			raise IOError("LSU ERR")
		
		if not isinstance(password, str):
			raise IOError("LSU ERR")
		
		print("   LSU " + str(user) + " to " + str(self.UDP_IP) + " " + str(self.UDP_PORT))
		
		reply = self.sendMessage('LSU ' + str(user) + ' ' + str(password))
		
		reply = reply.split()
		if len(reply) == 2 and reply[0] == 'LUR' and ('OK' in reply[1] or 'NOK' in reply[1]):
			return reply[1]
		else:
			raise IOError("LSU ERR")
	
	def deleteDir(self, user, dir):
		try:
			user = int(user)
		except ValueError:
			raise IOError("DLB ERR")
		
		if not isinstance(dir, str):
			raise IOError("DLB ERR")
		
		print("   DLB of " + dir + " from " + str(self.UDP_IP) + " " + str(self.UDP_PORT))
		
		reply = self.sendMessage('DLB ' + str(user) + ' ' + str(dir))
		
		reply = reply.split()
		if len(reply) == 2 and reply[0] == 'DBR' and ('OK' in reply[1] or 'NOK' in reply[1]):
			return reply[1]
		else:
			raise IOError("DLB ERR")			
			
			
def getStuffFromFile(fileName):
	try:
		file = open(fileName, "rb")
		result = pickle.load(file)
		file.close()
	except:
		raise IOError("ERR")
	return result

def setStuffToFile(fileName, stuff):
	try:
		file = open(fileName, "wb")
		pickle.dump(stuff, file, protocol=pickle.HIGHEST_PROTOCOL)
		file.close()
	except:
		raise IOError("ERR")
	
def pidExists(pid):
	try:
		os.kill(pid, 0)
	except OSError:
		# if it raises OSError, there is no process with the given pid
		return False
	
	return True
			
def serverErrorHandle(pids):
	for pid in pids:
		if pidExists(pid):
			os.kill(pid, signal.SIGTERM)
		

def sigterm_handler(_signo, _stack_frame):
	global imParent, pids
	if imParent:
		pids.remove(os.getpid())
		serverErrorHandle(pids)
		print("\nThanks for using this CS")
	
	os._exit(0)

if __name__ == "__main__":
	# for Ctrl+c command
	signal.signal(signal.SIGINT, sigterm_handler)
	# for kill command
	signal.signal(signal.SIGTERM, sigterm_handler)
	
	PORT = 58004
	HOST = socket.gethostbyname(socket.gethostname())
	# read all arguments (it only matters the "-p" argument)
	lenArgv = len(sys.argv)
	for index in range(0, lenArgv):
		if (sys.argv[index] == "-p"):
			index += 1
			PORT = int(sys.argv[index])
	
	print("CS is running at " + str(HOST) + ":" + str(PORT))
	
	'''
	backupServers = Path("backupServers")
	if not backupServers.is_file():
		setStuffToFile("backupServers", [])
	users = Path("CSUsers")
	if not users.is_file():
		setStuffToFile("CSUsers", {})
	'''
	setStuffToFile("backupServers", [])
	setStuffToFile("CSUsers", {})
	
	pids = []
	pids.append(os.getpid())
	pidUDPServer = os.fork()
	if pidUDPServer == 0:
		#this will be the child process
		try:
			server_UDP = serverUDP(HOST, PORT)
			while True:
				server_UDP.run()
		except IOError as error:
			print(error)
		except Exception as e:
			print("Something went wrong. Please restart the Central Server")
			
	elif pidUDPServer != -1:
		# this will be the parent process
		imParent = True
		pids.append(pidUDPServer)
		
		try:
			server_TCP = serverTCP(HOST, PORT)
			server_TCP.run()
		except IOError as error:
			print(error)
		except Exception:
			print("Something went wrong. Please restart the Central Server")
			
	else:
		print("No fork was made. Please try restart the Central Server")
		serverErrorHandle()
			