"""
Gitweb configuration: settings, config file loading, loadavg, features, snapshot formats.
Ported from gitweb/gitweb.perl (evaluate_gitweb_config, read_config_file, get_loadavg,
check_loadavg, known_snapshot_formats, feature_*, gitweb_get_feature, gitweb_check_feature,
filter_snapshot_fmts, filter_and_validate_refs, configure_gitweb_features, get_branch_refs).
"""

from __future__ import annotations

import os
import re
from pathlib import Path
from typing import Any

# Allowed actions (from %actions in gitweb.perl)
ACTIONS = {
	"blame",
	"blame_incremental",
	"blame_data",
	"blobdiff",
	"blobpatch",
	"blob",
	"blob_plain",
	"commitdiff",
	"commit",
	"heads",
	"history",
	"log",
	"patch",
	"patches",
	"remotes",
	"rss",
	"atom",
	"search",
	"search_help",
	"shortlog",
	"summary",
	"tag",
	"tags",
	"tree",
	"snapshot",
	"object",
	"opml",
	"project_list",
	"project_index",
}

# Map file extension to highlight.js language (class name)
BLOB_LANG = {
	"py": "python",
	"js": "javascript",
	"ts": "typescript",
	"jsx": "javascript",
	"tsx": "typescript",
	"html": "html",
	"htm": "html",
	"css": "css",
	"scss": "scss",
	"json": "json",
	"md": "markdown",
	"sh": "bash",
	"bash": "bash",
	"yml": "yaml",
	"yaml": "yaml",
	"xml": "xml",
	"go": "go",
	"rs": "rust",
	"java": "java",
	"c": "c",
	"h": "c",
	"cpp": "cpp",
	"cc": "cpp",
	"cxx": "cpp",
	"sql": "sql",
	"r": "r",
	"rb": "ruby",
	"php": "php",
	"swift": "swift",
	"kt": "kotlin",
	"vue": "xml",
	"toml": "toml",
	"ini": "ini",
	"cfg": "ini",
	"dockerfile": "dockerfile",
}

# Defaults (equivalent to @GITWEB_*@ in gitweb.perl)
PROJECTROOT = os.getenv("GITWEB_PROJECTROOT", str(Path.home()))
PROJECT_MAXDEPTH = int(os.getenv("GITWEB_PROJECT_MAXDEPTH", "1"))
PROJECTS_LIST = os.getenv("GITWEB_LIST", PROJECTROOT)
SITE_NAME = os.getenv("GITWEB_SITENAME", "") or "DistGit"
EXPORT_OK = os.getenv("GITWEB_EXPORT_OK", "")
# When True, list all directories under project root without repo/export_ok checks (default on for now).
LIST_ALL = os.getenv("GITWEB_LIST_ALL", "1").lower() in ("1", "true", "yes")
STRICT_EXPORT = os.getenv("GITWEB_STRICT_EXPORT", "0").lower() in (
	"1",
	"true",
	"yes",
)
GIT_BINDIR = os.getenv("GIT_BINDIR", "")
GIT = (GIT_BINDIR + "/git") if GIT_BINDIR else "git"
MAXLOAD: float | None = None  # 300 in perl; None = disabled

"""
The auth provider to use. "None" disables authentication entirely, bypassing distgit. USE WITH CAUTION.
RootProvider: Stub which provides admin login with a user/password. Parent class for other providers.
SSHProvider: TODO - Will authenticate using existing SSH keys. Easiest coming from raw git-daemon.
OAuth2Provider: TODO - Will authenticate against an OAuth2 server.
OIDCProvider: TODO - Will authenticate against an OIDC server.
MatrixProvider: TODO - Will authenticate against the Matrix network.
LDAPProvider: TODO - Will authenticate against a local LDAP server.
"""
DISTGIT_AUTH: str | None = os.getenv("DISTGIT_AUTH", None)
DISTGIT_ADMIN_USER: str | None = os.getenv("DISTGIT_ADMIN_USER", None)
DISTGIT_ADMIN_PASSWORD: str | None = os.getenv("DISTGIT_ADMIN_PASSWORD", None)
DISTGIT_SESSION_TIMEOUT: str | int | None = os.getenv("DISTGIT_SESSION_TIMEOUT", 3600 * 24 * 7)

# Config file paths (can be overridden by env)
GITWEB_CONFIG: str | None = os.getenv("GITWEB_CONFIG", None)
GITWEB_CONFIG_SYSTEM: str | None = os.getenv("GITWEB_CONFIG_SYSTEM", None)
GITWEB_CONFIG_COMMON: str | None = os.getenv("GITWEB_CONFIG_COMMON", None)

# Snapshot formats (from %known_snapshot_formats)
KNOWN_SNAPSHOT_FORMATS: dict[str, dict[str, Any]] = {
	"tgz": {
		"display": "tar.gz",
		"type": "application/x-gzip",
		"suffix": ".tar.gz",
		"format": "tar",
		"compressor": ["gzip", "-n"],
	},
	"tbz2": {
		"display": "tar.bz2",
		"type": "application/x-bzip2",
		"suffix": ".tar.bz2",
		"format": "tar",
		"compressor": ["bzip2"],
	},
	"txz": {
		"display": "tar.xz",
		"type": "application/x-xz",
		"suffix": ".tar.xz",
		"format": "tar",
		"compressor": ["xz"],
		"disabled": True,
	},
	"zip": {
		"display": "zip",
		"type": "application/zip",
		"suffix": ".zip",
		"format": "zip",
	},
}

KNOWN_SNAPSHOT_FORMAT_ALIASES: dict[str, str | None] = {
	"gzip": "tgz",
	"bzip2": "tbz2",
	"xz": "txz",
	"x-gzip": None,
	"gz": None,
	"x-bzip2": None,
	"bz2": None,
	"x-zip": None,
	"": None,
}


# Feature defaults; override in config or per-repo
_feature_snapshot_default = ["tgz"]
_snapshot_fmts: list[str] = []
_extra_branch_refs: list[str] = []


def read_config_file(filename: str | None) -> bool:
	"""Load and execute a Python config file. Returns True on success. Port of read_config_file."""
	if not filename or not Path(filename).exists():
		return False
	try:
		with open(filename) as f:
			code = compile(f.read(), filename, "exec")
			glob = {
				"PROJECTROOT": PROJECTROOT,
				"PROJECTS_LIST": PROJECTS_LIST,
				"SITE_NAME": SITE_NAME,
				"EXPORT_OK": EXPORT_OK,
				"LIST_ALL": LIST_ALL,
				"STRICT_EXPORT": STRICT_EXPORT,
				"GIT": GIT,
				"MAXLOAD": MAXLOAD,
				"KNOWN_SNAPSHOT_FORMATS": KNOWN_SNAPSHOT_FORMATS,
				"os": os,
				"Path": Path,
			}
			exec(code, glob)
			for k in (
				"PROJECTROOT",
				"PROJECTS_LIST",
				"SITE_NAME",
				"EXPORT_OK",
				"LIST_ALL",
				"STRICT_EXPORT",
				"GIT",
				"MAXLOAD",
				"KNOWN_SNAPSHOT_FORMATS",
			):
				if k in glob:
					globals()[k] = glob[k]
		return True
	except Exception:
		raise


def evaluate_gitweb_config() -> None:
	"""Resolve config paths and load common + instance/system config. Port of evaluate_gitweb_config."""
	global GITWEB_CONFIG, GITWEB_CONFIG_SYSTEM, GITWEB_CONFIG_COMMON
	if not GITWEB_CONFIG:
		GITWEB_CONFIG = os.getenv("GITWEB_CONFIG", "")
	if not GITWEB_CONFIG_SYSTEM:
		GITWEB_CONFIG_SYSTEM = os.getenv("GITWEB_CONFIG_SYSTEM", "")
	if not GITWEB_CONFIG_COMMON:
		GITWEB_CONFIG_COMMON = os.getenv("GITWEB_CONFIG_COMMON", "")

	if GITWEB_CONFIG == GITWEB_CONFIG_COMMON:
		GITWEB_CONFIG = ""
	if GITWEB_CONFIG_SYSTEM == GITWEB_CONFIG_COMMON:
		GITWEB_CONFIG_SYSTEM = ""

	if GITWEB_CONFIG_COMMON and Path(GITWEB_CONFIG_COMMON).exists():
		read_config_file(GITWEB_CONFIG_COMMON)
	if GITWEB_CONFIG and Path(GITWEB_CONFIG).exists():
		read_config_file(GITWEB_CONFIG)
		return
	if GITWEB_CONFIG_SYSTEM and Path(GITWEB_CONFIG_SYSTEM).exists():
		read_config_file(GITWEB_CONFIG_SYSTEM)


def get_loadavg() -> float:
	"""First element of load average, or 0 if unavailable. Port of get_loadavg."""
	try:
		return os.getloadavg()[0]
	except (OSError, AttributeError):
		pass
	try:
		with open("/proc/loadavg") as f:
			return float(f.read().split()[0])
	except (OSError, ValueError):
		return 0.0


def check_loadavg() -> None:
	"""Raise 503 if load exceeds maxload. Port of check_loadavg."""
	if MAXLOAD is not None and get_loadavg() > MAXLOAD:
		raise RuntimeError("503:The load average on the server is too high")


def gitweb_get_feature(
	name: str,
	git_dir: str | None = None,
	get_project_config: Any = None,
) -> list[Any]:
	"""Return feature value(s); project override when git_dir and get_project_config set. Port of gitweb_get_feature."""
	if name == "snapshot":
		defaults = _feature_snapshot_default
		if git_dir and get_project_config:
			val = get_project_config("snapshot") if callable(get_project_config) else None
			if val:
				defaults = (
					[] if val.strip().lower() == "none" else [x.strip() for x in re.split(r"[\s,]+", val) if x.strip()]
				)
		return list(defaults)
	if name == "avatar":
		return ["gravatar"]  # default
	if name == "extra-branch-refs":
		if git_dir and get_project_config and callable(get_project_config):
			val = get_project_config("extrabranchrefs")
			if val:
				parts = [val] if isinstance(val, str) else (val if isinstance(val, list) else [])
				return [x for part in parts for x in str(part).split()]
		return []
	return []


def gitweb_check_feature(name: str, git_dir: str | None = None, get_project_config: Any = None) -> bool | Any:
	"""First value of gitweb_get_feature. Port of gitweb_check_feature."""
	vals = gitweb_get_feature(name, git_dir, get_project_config)
	return vals[0] if vals else False


def filter_snapshot_fmts(fmts: list[str]) -> list[str]:
	"""Resolve aliases and drop unknown/disabled. Port of filter_snapshot_fmts."""
	result = []
	for f in fmts:
		key = KNOWN_SNAPSHOT_FORMAT_ALIASES.get(f, f)
		if key is None:
			continue
		if key not in KNOWN_SNAPSHOT_FORMATS:
			continue
		opt = KNOWN_SNAPSHOT_FORMATS[key]
		if opt.get("disabled"):
			continue
		result.append(key)
	return result


def filter_and_validate_refs(refs: list[str], is_valid_ref_format: Any) -> list[str]:
	"""Validate ref names and unique sort; 'heads' omitted (added in get_branch_refs).

	Port of filter_and_validate_refs.
	"""
	seen: set[str] = set()
	for ref in refs:
		if not is_valid_ref_format(ref):
			raise ValueError(f"Invalid ref '{ref}' in 'extra-branch-refs' feature")
		if ref != "heads":
			seen.add(ref)
	return sorted(seen)


def configure_gitweb_features(
	get_project_config: Any = None,
	git_dir: str | None = None,
	is_valid_ref_format: Any = None,
) -> None:
	"""Set snapshot_fmts and extra_branch_refs. Port of configure_gitweb_features."""
	global _snapshot_fmts, _extra_branch_refs
	_snapshot_fmts = filter_snapshot_fmts(gitweb_get_feature("snapshot", git_dir, get_project_config))
	avatar = gitweb_get_feature("avatar", git_dir, get_project_config)
	if avatar and avatar[0] not in ("gravatar", "picon"):
		avatar = [""]
	raw = gitweb_get_feature("extra-branch-refs", git_dir, get_project_config)
	_extra_branch_refs = filter_and_validate_refs(raw, is_valid_ref_format) if is_valid_ref_format else []


def get_branch_refs() -> list[str]:
	"""Return ['heads', ...extra_branch_refs]. Port of get_branch_refs."""
	return ["heads"] + _extra_branch_refs


def get_snapshot_fmts() -> list[str]:
	return _snapshot_fmts