import json
import re
from collections import Counter
from datetime import timedelta
from pathlib import Path
from typing import Any, Optional, TypedDict

import jingrow

PackageManagers = TypedDict(
	"PackageManagers",
	{
		"repo_path": str,
		"pyproject": Optional[dict[str, Any]],
		"packagejsons": list[dict[str, Any]],
	},
)
PackageManagerFiles = dict[str, PackageManagers]


def get_package_manager_files(repo_path_map: dict[str, str]) -> PackageManagerFiles:
	# Return pyproject.toml and package.json files
	pfiles_map = {}
	for app, repo_path in repo_path_map.items():
		pfiles_map[app] = get_package_manager_files_from_repo(app, repo_path)

	return pfiles_map


def get_package_manager_files_from_repo(app: str, repo_path: str):
	pypt, pckjs = _get_package_manager_files_from_repo(
		repo_path,
		True,
	)

	pm: PackageManagers = {
		"repo_path": repo_path,
		"pyproject": None,
		"packagejsons": [],
	}

	if pypt is not None:
		pm["pyproject"] = load_pyproject(app, pypt.absolute().as_posix())

	for pckj in pckjs:
		package_json = load_package_json(
			app,
			pckj.absolute().as_posix(),
		)
		pm["packagejsons"].append(package_json)

	return pm


def _get_package_manager_files_from_repo(
	repo_path: str,
	recursive: bool,
) -> tuple[Path | None, list[Path]]:
	pyproject_toml: Optional[Path] = None
	package_jsons: list[Path] = []  # An app can have multiple

	for p in Path(repo_path).iterdir():
		if p.name == "pyproject.toml":
			pyproject_toml = p
		elif p.name == "package.json":
			package_jsons.append(p)

		if not (recursive and p.is_dir()):
			continue

		pypt, pckjs = _get_package_manager_files_from_repo(p, False)
		if pypt is not None and pyproject_toml is None:
			pyproject_toml = pypt

		package_jsons.extend(pckjs)

	return pyproject_toml, package_jsons


def load_pyproject(app: str, pyproject_path: str):
	try:
		from tomli import TOMLDecodeError, load
	except ImportError:
		from tomllib import TOMLDecodeError, load

	with open(pyproject_path, "rb") as f:
		try:
			return load(f)
		except TOMLDecodeError:
			# Do not edit without updating deploy_notifications.py
			raise Exception("App has invalid pyproject.toml file", app) from None


def load_package_json(app: str, package_json_path: str):
	with open(package_json_path, "rb") as f:
		try:
			return json.load(f)
		except json.JSONDecodeError:
			# Do not edit without updating deploy_notifications.py
			raise Exception(
				"App has invalid package.json file", app, package_json_path
			) from None


def get_error_key(error_substring: str | list[str]) -> str:
	if isinstance(error_substring, list):
		error_substring = " ".join(error_substring)
	"""
	Converts `MatchStrings` into error keys, these are set on
	DeployCandidates on UA Failures for two reasons:
	1. To check if a subsequent deploy will fail for the same reasons.
	2. To track the kind of UA errors the users are facing.
	"""

	return re.sub(
		r"[\"'\[\],:]|\.$",
		"",
		error_substring.lower(),
	)


def get_will_fail_checker(error_key: str):
	from jcloud.jcloud.pagetype.deploy_candidate.deploy_notifications import handlers

	for error_substring, _, will_fail_checker in handlers():
		if get_error_key(error_substring) == error_key:
			return will_fail_checker


def is_suspended() -> bool:
	return bool(jingrow.db.get_single_value("Jcloud Settings", "suspend_builds"))


class BuildValidationError(jingrow.ValidationError):
	...


def get_build_server(group: str | None = None) -> str | None:
	"""
	Order of build server selection precedence:
	1. Build Server set on Release Group
	2. Build Server with least active builds
	3. Build Server set in Jcloud Settings
	"""

	if group and (server := jingrow.get_value("Release Group", group, "build_server")):
		return server

	if server := get_build_server_with_least_active_builds():
		return server

	return jingrow.get_value("Jcloud Settings", None, "build_server")


def get_build_server_with_least_active_builds() -> str | None:
	build_servers = jingrow.get_all(
		"Server",
		filters={"use_for_build": True, "status": "Active"},
		pluck="name",
	)

	if not build_servers:
		return None

	if len(build_servers) == 1:
		return build_servers[0]

	build_count = get_active_build_count_by_build_server()

	# Build server might not be in build_count, or might be inactive
	build_count_tuples = [(s, build_count[s]) for s in build_servers]
	build_count_tuples.sort(key=lambda x: x[1])
	return build_count_tuples[0][0]


def get_active_build_count_by_build_server():
	build_servers = jingrow.get_all(
		"Deploy Candidate",
		fields=["build_server"],
		filters={
			"status": ["in", ["Running", "Preparing"]],
			"modified": [">", jingrow.utils.now_datetime() - timedelta(hours=4)],
		},
		pluck="build_server",
	)
	return Counter(build_servers)
