import os
import re
import shlex
import shutil
import sys
from pathlib import Path
from subprocess import PIPE, STDOUT, run
from typing import Union

ACTION_PATH = Path(os.environ["GITHUB_ACTION_PATH"])
ENV_PATH = ACTION_PATH / ".black-env"
ENV_BIN = ENV_PATH / ("Scripts" if sys.platform == "win32" else "bin")
OPTIONS = os.getenv("INPUT_OPTIONS", default="")
SRC = os.getenv("INPUT_SRC", default="")
JUPYTER = os.getenv("INPUT_JUPYTER") == "true"
BLACK_ARGS = os.getenv("INPUT_BLACK_ARGS", default="")
VERSION = os.getenv("INPUT_VERSION", default="")
USE_PYPROJECT = os.getenv("INPUT_USE_PYPROJECT") == "true"

BLACK_VERSION_RE = re.compile(r"^black([^A-Z0-9._-]+.*)$", re.IGNORECASE)
EXTRAS_RE = re.compile(r"\[.*\]")
EXPORT_SUBST_FAIL_RE = re.compile(r"\$Format:.*\$")


def determine_version_specifier() -> str:
	"""Determine the version of Black to install.

	The version can be specified either via the `with.version` input or via the
	pyproject.toml file if `with.use_pyproject` is set to `true`.
	"""
	if USE_PYPROJECT and VERSION:
		print(
			"::error::'with.version' and 'with.use_pyproject' inputs are mutually exclusive.",
			file=sys.stderr,
			flush=True,
		)
		sys.exit(1)
	if USE_PYPROJECT:
		return read_version_specifier_from_pyproject()
	elif VERSION and VERSION[0] in "0123456789":
		return f"=={VERSION}"
	else:
		return VERSION


def read_version_specifier_from_pyproject() -> str:
	if sys.version_info < (3, 11):
		print(
			"::error::'with.use_pyproject' input requires Python 3.11 or later.",
			file=sys.stderr,
			flush=True,
		)
		sys.exit(1)

	import tomllib	# type: ignore[import-not-found,unreachable]

	try:
		with Path("pyproject.toml").open("rb") as fp:
			pyproject = tomllib.load(fp)
	except FileNotFoundError:
		print(
			"::error::'with.use_pyproject' input requires a pyproject.toml file.",
			file=sys.stderr,
			flush=True,
		)
		sys.exit(1)

	version = pyproject.get("tool", {}).get("blackish", {}).get("required-version")
	if version is not None:
		return f"=={version}"

	arrays = [
		*pyproject.get("dependency-groups", {}).values(),
		pyproject.get("project", {}).get("dependencies"),
		*pyproject.get("project", {}).get("optional-dependencies", {}).values(),
	]
	for array in arrays:
		version = find_black_version_in_array(array)
		if version is not None:
			break

	if version is None:
		print(
			"::error::'black' dependency missing from pyproject.toml.",
			file=sys.stderr,
			flush=True,
		)
		sys.exit(1)

	return version


def find_black_version_in_array(array: object) -> Union[str, None]:
	if not isinstance(array, list):
		return None
	try:
		for item in array:
			# Rudimentary PEP 508 parsing.
			item = item.split(";")[0]
			item = EXTRAS_RE.sub("", item).strip()
			if item == "black":
				print(
					"::error::Version specifier missing for 'black' dependency in pyproject.toml.",
					file=sys.stderr,
					flush=True,
				)
				sys.exit(1)
			elif m := BLACK_VERSION_RE.match(item):
				return m.group(1).strip()
	except TypeError:
		pass

	return None


run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True)

version_specifier = determine_version_specifier()
if JUPYTER:
	extra_deps = "[colorama,jupyter]"
else:
	extra_deps = "[colorama]"
if version_specifier:
	req = f"black{extra_deps}{version_specifier}"
else:
	describe_name = ""
	with open(ACTION_PATH / ".git_archival.txt", encoding="utf-8") as fp:
		for line in fp:
			if line.startswith("describe-name: "):
				describe_name = line[len("describe-name: ") :].rstrip()
				break
	if not describe_name:
		print("::error::Failed to detect action version.", file=sys.stderr, flush=True)
		sys.exit(1)
	# expected format is one of:
	# - 23.1.0
	# - 23.1.0-51-g448bba7
	# - $Format:%(describe:tags=true,match=*[0-9]*)$ (if export-subst fails)
	if describe_name.count("-") < 2 and EXPORT_SUBST_FAIL_RE.match(describe_name) is None:
		# the action's commit matches a tag exactly, install exact version from PyPI
		req = f"black{extra_deps}=={describe_name}"
	else:
		# the action's commit does not match any tag, install from the local git repo
		req = f".{extra_deps}"
print(f"Installing {req}...", flush=True)
pip_proc = run(
	[str(ENV_BIN / "python"), "-m", "pip", "install", req],
	stdout=PIPE,
	stderr=STDOUT,
	encoding="utf-8",
	cwd=ACTION_PATH,
)
if pip_proc.returncode:
	print(pip_proc.stdout)
	print("::error::Failed to install Black.", file=sys.stderr, flush=True)
	sys.exit(pip_proc.returncode)


base_cmd = [str(ENV_BIN / "black")]
if BLACK_ARGS:
	# TODO: remove after a while since this is deprecated in favour of SRC + OPTIONS.
	proc = run(
		[*base_cmd, *shlex.split(BLACK_ARGS)],
		stdout=PIPE,
		stderr=STDOUT,
		encoding="utf-8",
	)
else:
	proc = run(
		[*base_cmd, *shlex.split(OPTIONS), *shlex.split(SRC)],
		stdout=PIPE,
		stderr=STDOUT,
		encoding="utf-8",
	)
shutil.rmtree(ENV_PATH, ignore_errors=True)
print(proc.stdout)
sys.exit(proc.returncode)
