# Copyright (c) 2022, JINGROW and Contributors
# License: MIT. See LICENSE
import os
import re
import shutil
import subprocess
from contextlib import suppress
from subprocess import getoutput
from tempfile import mkdtemp
from urllib.parse import urlparse

import click
from semantic_version import Version

import jingrow

timestamps = {}
app_paths = None
sites_path = os.path.abspath(os.getcwd())
WHITESPACE_PATTERN = re.compile(r"\s+")
HTML_COMMENT_PATTERN = re.compile(r"(<!--.*?-->)")


class AssetsNotDownloadedError(Exception):
	pass


class AssetsDontExistError(Exception):
	pass


def download_file(url, prefix):
	from requests import get

	filename = urlparse(url).path.split("/")[-1]
	local_filename = os.path.join(prefix, filename)
	with get(url, stream=True, allow_redirects=True) as r:
		r.raise_for_status()
		with open(local_filename, "wb") as f:
			for chunk in r.iter_content(chunk_size=8192):
				f.write(chunk)
	return local_filename


def build_missing_files():
	"""Check which files dont exist yet from the assets.json and run build for those files"""

	missing_assets = []
	current_asset_files = []

	for type in ["css", "js"]:
		folder = os.path.join(sites_path, "assets", "jingrow", "dist", type)
		current_asset_files.extend(os.listdir(folder))

	development = jingrow.local.conf.developer_mode or jingrow.local.dev_server
	build_mode = "development" if development else "production"

	assets_json = jingrow.read_file("assets/assets.json")
	if assets_json:
		assets_json = jingrow.parse_json(assets_json)

		for bundle_file, output_file in assets_json.items():
			if not output_file.startswith("/assets/jingrow"):
				continue

			if os.path.basename(output_file) not in current_asset_files:
				missing_assets.append(bundle_file)

		if missing_assets:
			click.secho("\nBuilding missing assets...\n", fg="yellow")
			files_to_build = ["jingrow/" + name for name in missing_assets]
			bundle(build_mode, files=files_to_build)
	else:
		# no assets.json, run full build
		bundle(build_mode, apps="jingrow")


def get_assets_link(jingrow_head) -> str:
	import requests

	tag = getoutput(
		r"cd ../apps/jingrow && git show-ref --tags -d | grep %s | sed -e 's,.*"
		r" refs/tags/,,' -e 's/\^{}//'" % jingrow_head
	)

	if tag:
		# if tag exists, download assets from github release
		url = f"http://git.jingrow.com/jingrow/jingrow/releases/download/{tag}/assets.tar.gz"
	else:
		url = f"http://assets.framework.jingrow.com/{jingrow_head}.tar.gz"

	if not requests.head(url):
		reference = f"Release {tag}" if tag else f"Commit {jingrow_head}"
		raise AssetsDontExistError(f"Assets for {reference} don't exist")

	return url


def fetch_assets(url, jingrow_head):
	click.secho("Retrieving assets...", fg="yellow")

	prefix = mkdtemp(prefix="jingrow-assets-", suffix=jingrow_head)
	assets_archive = download_file(url, prefix)

	if not assets_archive:
		raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}")

	click.echo(click.style("✔", fg="green") + f" Downloaded Jingrow assets from {url}")

	return assets_archive


def setup_assets(assets_archive):
	import tarfile

	directories_created = set()

	click.secho("\nExtracting assets...\n", fg="yellow")
	with tarfile.open(assets_archive) as tar:
		for file in tar:
			if not file.isdir():
				dest = "." + file.name.replace("./jingrow-bench/sites", "")
				asset_directory = os.path.dirname(dest)
				show = dest.replace("./assets/", "")

				if asset_directory not in directories_created:
					if not os.path.exists(asset_directory):
						os.makedirs(asset_directory, exist_ok=True)
					directories_created.add(asset_directory)

				tar.makefile(file, dest)
				click.echo(click.style("✔", fg="green") + f" Restored {show}")

	return directories_created


def download_jingrow_assets(verbose=True):
	"""Downloads and sets up Jingrow assets if they exist based on the current
	commit HEAD.
	Returns True if correctly setup else returns False.
	"""
	jingrow_head = getoutput("cd ../apps/jingrow && git rev-parse HEAD")

	if not jingrow_head:
		return False

	try:
		url = get_assets_link(jingrow_head)
		assets_archive = fetch_assets(url, jingrow_head)
		setup_assets(assets_archive)
		build_missing_files()
		return True

	except AssetsDontExistError as e:
		click.secho(str(e), fg="yellow")

	except Exception as e:
		# TODO: log traceback in bench.log
		click.secho(str(e), fg="red")

	finally:
		try:
			shutil.rmtree(os.path.dirname(assets_archive))
		except Exception:
			pass

	return False


def symlink(target, link_name, overwrite=False):
	"""
	Create a symbolic link named link_name pointing to target.
	If link_name exists then FileExistsError is raised, unless overwrite=True.
	When trying to overwrite a directory, IsADirectoryError is raised.

	Source: https://stackoverflow.com/a/55742015/10309266
	"""

	if not overwrite:
		return os.symlink(target, link_name)

	# Create link to target with temporary filename
	while True:
		temp_link_name = f"tmp{jingrow.generate_hash()}"

		# os.* functions mimic as closely as possible system functions
		# The POSIX symlink() returns EEXIST if link_name already exists
		# https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
		try:
			os.symlink(target, temp_link_name)
			break
		except FileExistsError:
			pass

	# Replace link_name with temp_link_name
	try:
		# Pre-empt os.replace on a directory with a nicer message
		if os.path.isdir(link_name):
			raise IsADirectoryError(f"Cannot symlink over existing directory: '{link_name}'")
		try:
			shutil.move(temp_link_name, link_name)
		except AttributeError:
			os.renames(temp_link_name, link_name)
	except Exception:
		if os.path.islink(temp_link_name):
			os.remove(temp_link_name)
		raise


def setup():
	global app_paths, assets_path

	pymodules = []
	for app in jingrow.get_all_apps(True):
		try:
			pymodules.append(jingrow.get_module(app))
		except ImportError:
			pass
	app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules]
	assets_path = os.path.join(jingrow.local.sites_path, "assets")


def bundle(
	mode,
	apps=None,
	hard_link=False,
	verbose=False,
	skip_jingrow=False,
	files=None,
	save_metafiles=False,
):
	"""Copy pre-built frontend assets"""
	setup()
	make_asset_dirs(hard_link=hard_link)

	# Only copy pre-built new frontend
	build_new_frontend(verbose=verbose)

	with suppress(Exception):
		jingrow.cache.flushall()


def watch(apps=None):
	"""Watch mode removed - use frontend dev server directly"""
	click.secho("Legacy watch mode removed. Use 'cd frontend && yarn dev' for development.", fg="yellow")


# Node.js related functions removed - no longer needed for pre-built frontend


def build_new_frontend(verbose=False):
	"""Copy pre-built frontend to public directory"""
	jingrow_app_path = jingrow.get_app_source_path("jingrow")
	
	# Source: frontend/dist (pre-built)
	frontend_path = os.path.join(jingrow_app_path, "frontend")
	frontend_dist = os.path.join(frontend_path, "dist")
	
	# Target: jingrow/public/frontend
	jingrow_module_path = os.path.join(jingrow_app_path, "jingrow")
	frontend_public = os.path.join(jingrow_module_path, "public", "frontend")

	if not os.path.exists(frontend_dist):
		if verbose:
			click.secho(f"Pre-built frontend not found at {frontend_dist}, skipping", fg="yellow")
		return

	try:
		if os.path.exists(frontend_public):
			shutil.rmtree(frontend_public)
		shutil.copytree(frontend_dist, frontend_public, dirs_exist_ok=True)
		click.echo(click.style("✔", fg="green") + " Frontend assets copied")
	except Exception as e:
		click.secho(f"Failed to copy frontend assets: {str(e)}", fg="red")
		if verbose:
			import traceback
			traceback.print_exc()


def generate_assets_map():
	symlinks = {}

	for app_name in jingrow.get_all_apps():
		app_pg_path = None

		pymodule = jingrow.get_module(app_name)
		app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__))
		app_public_path = os.path.join(app_base_path, "public")
		app_node_modules_path = os.path.join(app_base_path, "..", "node_modules")
		app_pgs_path = os.path.join(app_base_path, "pgs")
		app_www_pgs_path = os.path.join(app_base_path, "www", "pgs")

		app_assets = os.path.abspath(app_public_path)
		app_node_modules = os.path.abspath(app_node_modules_path)

		# {app}/public > assets/{app}
		if os.path.isdir(app_assets):
			symlinks[app_assets] = os.path.join(assets_path, app_name)

		# {app}/node_modules > assets/{app}/node_modules
		if os.path.isdir(app_node_modules):
			symlinks[app_node_modules] = os.path.join(assets_path, app_name, "node_modules")

		# {app}/pgs > assets/{app}_pgs
		if os.path.isdir(app_pgs_path):
			app_pg_path = os.path.join(app_base_path, "pgs")
		elif os.path.isdir(app_www_pgs_path):
			app_pg_path = os.path.join(app_base_path, "www", "pgs")
		if app_pg_path:
			app_pgs = os.path.abspath(app_pg_path)
			symlinks[app_pgs] = os.path.join(assets_path, app_name + "_pgs")

	return symlinks


def setup_assets_dirs():
	for dir_path in (os.path.join(assets_path, x) for x in ("js", "css")):
		os.makedirs(dir_path, exist_ok=True)


def clear_broken_symlinks():
	for path in os.listdir(assets_path):
		path = os.path.join(assets_path, path)
		if os.path.islink(path) and not os.path.exists(path):
			os.remove(path)


def unstrip(message: str) -> str:
	"""Pads input string on the right side until the last available column in the terminal"""
	_len = len(message)
	try:
		max_str = os.get_terminal_size().columns
	except Exception:
		max_str = 80

	if _len < max_str:
		_rem = max_str - _len
	else:
		_rem = max_str % _len

	return f"{message}{' ' * _rem}"


def make_asset_dirs(hard_link=False):
	setup_assets_dirs()
	clear_broken_symlinks()
	symlinks = generate_assets_map()

	for source, target in symlinks.items():
		start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}")
		fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}")

		# Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes
		try:
			print(start_message, end="\r")
			link_assets_dir(source, target, hard_link=hard_link)
		except Exception as e:
			print(e)
			print(fail_message)

	click.echo(unstrip(click.style("✔", fg="green") + " Application Assets Linked") + "\n")


def link_assets_dir(source, target, hard_link=False):
	if not os.path.exists(source):
		return

	if os.path.exists(target):
		if os.path.islink(target):
			os.unlink(target)
		else:
			shutil.rmtree(target)

	if hard_link:
		shutil.copytree(source, target, dirs_exist_ok=True)
	else:
		symlink(source, target, overwrite=True)


def scrub_html_template(content):
	"""Returns HTML content with removed whitespace and comments"""
	# remove whitespace to a single space
	content = WHITESPACE_PATTERN.sub(" ", content)

	# strip comments
	content = HTML_COMMENT_PATTERN.sub("", content)

	return content.replace("'", "'")


def html_to_js_template(path, content):
	"""returns HTML template content as Javascript code, adding it to `jingrow.templates`"""
	return """jingrow.templates["{key}"] = '{content}';\n""".format(
		key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content)
	)
