# Copyright (c) 2015, JINGROW and Contributors
# License: MIT. See LICENSE
import hashlib
import json
import os

import jingrow
from jingrow.model.base_page import get_controller
from jingrow.modules import get_module_path, scrub_dt_dn
from jingrow.query_builder import PageType
from jingrow.utils import get_datetime, now


def calculate_hash(path: str) -> str:
	"""Calculate md5 hash of the file in binary mode

	Args:
	        path (str): Path to the file to be hashed

	Returns:
	        str: The calculated hash
	"""
	hash_md5 = hashlib.md5(usedforsecurity=False)
	with open(path, "rb") as f:
		for chunk in iter(lambda: f.read(4096), b""):
			hash_md5.update(chunk)
	return hash_md5.hexdigest()


ignore_values = {
	"Report": ["disabled", "prepared_report", "add_total_row"],
	"Print Format": ["disabled"],
	"Notification": ["enabled"],
	"Print Style": ["disabled"],
	"Module Onboarding": ["is_complete"],
	"Onboarding Step": ["is_complete", "is_skipped"],
	"Workspace": ["is_hidden"],
}

ignore_pagetypes = [""]


def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False):
	if isinstance(module, list):
		return [
			import_file(
				m[0],
				m[1],
				m[2],
				force=force,
				pre_process=pre_process,
				reset_permissions=reset_permissions,
			)
			for m in module
		]
	else:
		return import_file(
			module, dt, dn, force=force, pre_process=pre_process, reset_permissions=reset_permissions
		)


def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions=False):
	"""Sync a file from txt if modifed, return false if not updated"""
	path = get_file_path(module, dt, dn)
	return import_file_by_path(path, force, pre_process=pre_process, reset_permissions=reset_permissions)


def get_file_path(module, dt, dn):
	dt, dn = scrub_dt_dn(dt, dn)

	path = os.path.join(get_module_path(module), os.path.join(dt, dn, f"{dn}.json"))

	return path


def import_file_by_path(
	path: str,
	force: bool = False,
	data_import: bool = False,
	pre_process=None,
	ignore_version: bool | None = None,
	reset_permissions: bool = False,
):
	"""Import file from the given path

	Some conditions decide if a file should be imported or not.
	Evaluation takes place in the order they are mentioned below.

	- Check if `force` is true. Import the file. If not, move ahead.
	- Get `db_modified_timestamp`(value of the modified field in the database for the file).
	        If the return is `none,` this file doesn't exist in the DB, so Import the file. If not, move ahead.
	- Check if there is a hash in DB for that file. If there is, Calculate the Hash of the file to import and compare it with the one in DB if they are not equal.
	        Import the file. If Hash doesn't exist, move ahead.
	- Check if `db_modified_timestamp` is older than the timestamp in the file; if it is, we import the file.

	If timestamp comparison happens for pagetypes, that means the Hash for it doesn't exist.
	So, even if the timestamp is newer on DB (When comparing timestamps), we import the file and add the calculated Hash to the DB.
	So in the subsequent imports, we can use hashes to compare. As a precautionary measure, the timestamp is updated to the current time as well.

	Args:
	        path (str): Path to the file.
	        force (bool, optional): Load the file without checking any conditions. Defaults to False.
	        data_import (bool, optional): [description]. Defaults to False.
	        pre_process ([type], optional): Any preprocesing that may need to take place on the pg. Defaults to None.
	        ignore_version (bool, optional): ignore current version. Defaults to None.
	        reset_permissions (bool, optional): reset permissions for the file. Defaults to False.

	Returns:
	        [bool]: True if import takes place. False if it wasn't imported.
	"""
	try:
		pgs = read_pg_from_file(path)
	except OSError:
		print(f"{path} missing")
		return

	calculated_hash = calculate_hash(path)

	if pgs:
		if not isinstance(pgs, list):
			pgs = [pgs]

		for pg in pgs:
			# modified timestamp in db, none if pagetype's first import
			db_modified_timestamp = jingrow.db.get_value(pg["pagetype"], pg["name"], "modified")
			is_db_timestamp_latest = db_modified_timestamp and (
				get_datetime(pg.get("modified")) <= get_datetime(db_modified_timestamp)
			)

			if not force and db_modified_timestamp:
				stored_hash = None
				if pg["pagetype"] == "PageType":
					try:
						stored_hash = jingrow.db.get_value(pg["pagetype"], pg["name"], "migration_hash")
					except Exception:
						pass

				# if hash exists and is equal no need to update
				if stored_hash and stored_hash == calculated_hash:
					continue

				# if hash doesn't exist, check if db timestamp is same as json timestamp, add hash if from pagetype
				if is_db_timestamp_latest and pg["pagetype"] != "PageType":
					continue

			import_pg(
				docdict=pg,
				data_import=data_import,
				pre_process=pre_process,
				ignore_version=ignore_version,
				reset_permissions=reset_permissions,
				path=path,
			)

			if pg["pagetype"] == "PageType":
				pagetype_table = PageType("PageType")
				jingrow.qb.update(pagetype_table).set(pagetype_table.migration_hash, calculated_hash).where(
					pagetype_table.name == pg["name"]
				).run()

			new_modified_timestamp = pg.get("modified")

			# if db timestamp is newer, hash must have changed, must update db timestamp
			if is_db_timestamp_latest and pg["pagetype"] == "PageType":
				new_modified_timestamp = now()

			if new_modified_timestamp:
				update_modified(new_modified_timestamp, pg)

	return True


def read_pg_from_file(path):
	pg = None
	if os.path.exists(path):
		with open(path) as f:
			try:
				pg = json.loads(f.read())
			except ValueError:
				print(f"bad json: {path}")
				raise
	else:
		raise OSError("%s missing" % path)

	return pg


def update_modified(original_modified, pg):
	# since there is a new timestamp on the file, update timestamp in
	if pg["pagetype"] == pg["name"] and pg["name"] != "PageType":
		singles_table = PageType("Singles")

		jingrow.qb.update(singles_table).set(singles_table.value, original_modified).where(
			singles_table["field"] == "modified",  # singles_table.field is a method of pypika Selectable
		).where(singles_table.pagetype == pg["name"]).run()
	else:
		pagetype_table = PageType(pg["pagetype"])

		jingrow.qb.update(pagetype_table).set(pagetype_table.modified, original_modified).where(
			pagetype_table.name == pg["name"]
		).run()


def import_pg(
	docdict,
	data_import=False,
	pre_process=None,
	ignore_version=None,
	reset_permissions=False,
	path=None,
):
	jingrow.flags.in_import = True
	docdict["__islocal"] = 1

	controller = get_controller(docdict["pagetype"])
	if controller and hasattr(controller, "prepare_for_import") and callable(controller.prepare_for_import):
		controller.prepare_for_import(docdict)

	pg = jingrow.get_pg(docdict)

	reset_tree_properties(pg)
	load_code_properties(pg, path)

	pg.run_method("before_import")

	pg.flags.ignore_version = ignore_version
	if pre_process:
		pre_process(pg)

	if jingrow.db.exists(pg.pagetype, pg.name):
		delete_old_pg(pg, reset_permissions)

	pg.flags.ignore_links = True
	if not data_import:
		pg.flags.ignore_validate = True
		pg.flags.ignore_permissions = True
		pg.flags.ignore_mandatory = True

	pg.insert()

	jingrow.flags.in_import = False

	return pg


def load_code_properties(pg, path):
	"""Load code files stored in separate files with extensions"""
	if path:
		if hasattr(pg, "get_code_fields"):
			dirname, filename = os.path.split(path)
			for key, extn in pg.get_code_fields().items():
				codefile = os.path.join(dirname, filename.split(".", 1)[0] + "." + extn)
				if os.path.exists(codefile):
					with open(codefile) as txtfile:
						pg.set(key, txtfile.read())


def delete_old_pg(pg, reset_permissions):
	ignore = []
	old_pg = jingrow.get_pg(pg.pagetype, pg.name)

	if pg.pagetype in ignore_values:
		# update ignore values
		for key in ignore_values.get(pg.pagetype) or []:
			pg.set(key, old_pg.get(key))

	# update ignored pgs into new pg
	for df in pg.meta.get_table_fields():
		if df.options in ignore_pagetypes and not reset_permissions:
			pg.set(df.fieldname, [])
			ignore.append(df.options)

	# delete old
	jingrow.delete_pg(pg.pagetype, pg.name, force=1, ignore_pagetypes=ignore, for_reload=True)

	pg.flags.ignore_children_type = ignore


def reset_tree_properties(pg):
	# Note on Tree PageTypes:
	# The tree structure is maintained in the database via the fields "lft" and
	# "rgt". They are automatically set and kept up-to-date. Importing them
	# would destroy any existing tree structure.
	if getattr(pg.meta, "is_tree", None) and any([pg.lft, pg.rgt]):
		print(f'Ignoring values of `lft` and `rgt` for {pg.pagetype} "{pg.name}"')
		pg.lft = None
		pg.rgt = None
