import flask
import random
import chardet
import pathlib
import datetime
import werkzeug
import unidecode
import flask_restful

detector = chardet.UniversalDetector()
webapp = flask.Flask(__name__)
api = flask_restful.Api(webapp)
webapp.secret_key = b"7fb45965e367bbb20f203043b4cf4e662bc859650b6322af5a9a9d3c\
9b281938"
max_file_size = 10 * 1000 * 1000 # 10 MB
webapp.config["MAX_CONTENT_LENGTH"] = max_file_size
sessions = set()
cod_reg = "180"
len_c1, pos_c1, pos_count_c1 = 177 + 1, 115, 135 + 12
len_c2, pos_c2, pos_count_c2 = 650 + 1,  34,  94 + 12

def get_flux_type(pathlib_file):
	flux_type = pathlib_file.name[-1]
	assert flux_type == 'A' or flux_type == 'B', \
		"get_flux_type: flux_type == \"{}\"".format(flux_type)
	if flux_type == "A":
		return "flusso A"
	return "flusso B"

def size_to_human_readable(int_size):
	multipliers = iter(["", "kilo", "mega", "giga", "tera"])
	unit_multiplier = next(multipliers)
	try:
		while int_size >= 1000:
			unit_multiplier = next(multipliers)
			int_size //= 1000
		return "{} {}byte".format(int_size, unit_multiplier)
	except StopIteration:
		return "infiniti byte"

def is_text_data(bytes_data):
	textchars = bytearray(set([7, 8, 9, 10, 12, 13, 27]) |
		set(range(0x20, 0x100)) - {0x7f})
	return bool(bytes_data.translate(None, textchars))

class WebappException(Exception):
	pass

class NotATextFile(WebappException):
	def __init__(self, flux_file):
		self.flux_type = get_flux_type(flux_file)
	
	def __str__(self):
		return """\
Il {} non e' un file di testo.""".format(self.flux_type)

class TooBigFile(WebappException):
	def __init__(self, flux_file):
		flux_file = pathlib.Path(flux_file)
		self.flux_type = get_flux_type(flux_file)

	def __str__(self):
		global max_file_size
		return """\
Il {} eccede la dimensione massima consentita di {}""".format(\
self.flux_type, size_to_human_readable(max_file_size))

class CodRegNotFoundException(WebappException):
	def __init__(self, flux_file, first_line):
		self.flux_type = get_flux_type(flux_file)
		self.first_line = first_line

	def __str__(self):
		global cod_reg

		return """\
Codice regionale "{}" non trovato nel {}.""".format(cod_reg, self.flux_type)

class UnexpectedLenException(WebappException):
	def __init__(self, flux_file, len_riga):
		self.flux_type = get_flux_type(flux_file)
		self.len_riga = len_riga

	def __str__(self):
		return """\
Lunghezza riga inattesa nel {}.
Trovata lunghezza "{}" (incluso newline).""".format(self.flux_type, \
self.len_riga)

class MalformedDateException(WebappException):
	def __init__(self, flux_file, compare_date):
		self.flux_type = get_flux_type(flux_file)
		self.compare_date = compare_date

	def __str__(self):
		return """\
Data malformata nel {}.
Trovata data "{}".""".format(self.flux_type, self.compare_date)

class DifferentFluxException(WebappException):
	def __init__(self, fluxtype_a, fluxtype_b):
		self.fluxtype_a = fluxtype_a
		self.fluxtype_b = fluxtype_b

	def __str__(self):
		return """\
Sono stati passati file di tipo C1 e di tipo C2.
Si possono fondere file con lo stesso tipo di flusso."""

def extract_int(line, pos_count):
	return int(line[pos_count:pos_count + 7])

def ensure_crlf(line):
	if line.endswith("\r\n"):
		return line
	if line[-1] == "\n":
		return line[:-1] + "\r\n"
	return line + "\r\n"

def remove_minus_zero(line):
	return line.replace("-0", "00")

def save_new_flux(flux_a, flux_b, flux_type, flux_c):
	assert flux_type == 1 or flux_type == 2, \
		"save_new_flux: flux_type == \"{}\"".format(flux_type)
	if flux_type == 1:
		pos_count = pos_count_c1
	else:
		pos_count = pos_count_c2

	while True:
		cur_line = unidecode.unidecode(flux_a.readline())
		if cur_line == "":
			break
		add_digit = extract_int(cur_line, pos_count)
		new_line = remove_minus_zero(ensure_crlf(cur_line))
		flux_c.write(new_line)

	while True:
		cur_line = unidecode.unidecode(flux_b.readline())
		if cur_line == "":
			break
		cur_digit = extract_int(cur_line, pos_count)
		new_digit = str(cur_digit + add_digit).rjust(7, '0')
		new_line = cur_line[:pos_count] + new_digit + cur_line[pos_count + 7:]
		new_line = remove_minus_zero(ensure_crlf(new_line))
		flux_c.write(new_line)

def check_flux(flux_file):
	global cod_reg, len_c1, pos_c1, len_c2, pos_c2

	first_line = flux_file.readline()
	idx = first_line.find(cod_reg)
	if idx != 0:
		raise CodRegNotFoundException(flux_file, first_line)

	date_format = "%d%m%Y"
	lower_date = datetime.datetime.strptime('01011900', date_format)
	upper_date = datetime.datetime.strptime('31122100', date_format)
	len_line = len(first_line)
	if len_line == len_c1:
		# test per C1
		pos = pos_c1
	elif len_line == len_c2:
		# test per C2
		pos = pos_c2
	else:
		raise UnexpectedLenException(flux_file, len_line)
	compare = first_line[pos:(pos+8)]

	try:
		find_date = datetime.datetime.strptime(compare, date_format)
		if lower_date <= find_date <= upper_date:
			assert pos == pos_c1 or pos == pos_c2, \
				"check_flux: pos == \"{}\"".format(pos)
			if pos == pos_c1:
				return 1
			else:
				return 2
	except ValueError:
		raise MalformedDateException(flux_file, compare)

def new_session(session_chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU\
VWXYZ0123456789_", session_str_len=5):
	global sessions
	session_str = None
	while True:
		session_str = "".join([random.choice(session_chars) for _ in\
			range(session_str_len)])
		if session_str not in sessions:
			break
	sessions.add(session_str)
	return session_str

@webapp.route("/")
def index():
	flask.session["id"] = new_session()
	return flask.render_template("index.html")

@webapp.route("/templates/<path>")
def serve_file(path):
	return flask.send_from_directory("templates", path)

def safe_file_remove(pathlib_file):
	if pathlib_file is None:
		return

	if pathlib_file.exists():
		pathlib_file.unlink()

def detect_encoding_and_save(werkzeug_file, filename, chunk_size=16384):
	global detector

	ret_path = pathlib.Path(filename)
	detector.reset()
	first_iteration = True
	with ret_path.open(mode="wb") as f:
		while True:
			read_bytes = werkzeug_file.read(chunk_size)
			if is_text_data(read_bytes):
				raise NotATextFile(ret_path)
			if first_iteration and read_bytes[:2] == b"\r\n":
				read_bytes = read_bytes[2:]
				first_iteration = False
			if read_bytes == b"":
				break
			detector.feed(read_bytes)
			f.write(read_bytes)
	detector.close()
	return ret_path, detector.result["encoding"]

def get_flux_file_from_exception(e):
	return e.flux_file.name[-1]

@webapp.route("/upload", methods=["POST"])
def upload():
	global sessions

	try:
		session_id = flask.session["id"]
		if session_id not in sessions:
			session_id = new_session()

		path_flusso_a = session_id + "A"
		path_flusso_b = session_id + "B"
		path_flusso_c = session_id + "C"
		pfile_flusso_a, pfile_flusso_b, pfile_flusso_c = None, None, None
		try:
			pfile_flusso_a, encoding_flusso_a = detect_encoding_and_save(\
				flask.request.files["flussoA"], path_flusso_a)
		except werkzeug.exceptions.RequestEntityTooLarge:
			raise TooBigFile(path_flusso_a)
		try:
			pfile_flusso_b, encoding_flusso_b = detect_encoding_and_save(\
				flask.request.files["flussoB"], path_flusso_b)
		except werkzeug.exceptions.RequestEntityTooLarge:
			raise TooBigFile(path_flusso_b)

		pfile_flusso_c = pathlib.Path(path_flusso_c)

		with pfile_flusso_a.open(encoding=encoding_flusso_a) as fa:
			with pfile_flusso_b.open(encoding=encoding_flusso_b) as fb:
				fluxtype_a = check_flux(fa)
				fluxtype_b = check_flux(fb)
				if fluxtype_a != fluxtype_b:
					raise DifferentFluxException(fluxtype_a, fluxtype_b)
				fa.seek(0, 0)
				fb.seek(0, 0)
				with pfile_flusso_c.open(mode="w", encoding="ascii") as fc:
					save_new_flux(fa, fb, fluxtype_a, fc)
					fc.flush()

					return flask.send_from_directory(".", path_flusso_c,\
						mimetype="text/plain; charset=UTF-8",\
						as_attachment=True), 200, {"X-FluxType": fluxtype_a}
	except (NotATextFile, TooBigFile, CodRegNotFoundException,
		UnexpectedLenException, MalformedDateException,
		DifferentFluxException) as e:
		return str(e)
	except AssertionError as e:
		return """\
Asserzione fallita: "{}".
Notificare il programmatore.""".format(str(e))
	finally:
		sessions.discard(session_id)
		safe_file_remove(pfile_flusso_a)
		safe_file_remove(pfile_flusso_b)
		safe_file_remove(pfile_flusso_c)
	return """\
Errore intatteso.
Notificare il programmatore."""

if __name__ == "__main__":
	webapp.run(host="127.0.0.1", port=5000)
