"""
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.environ.get("GITWEB_PROJECTROOT", str(Path.home()))
PROJECT_MAXDEPTH = int(os.environ.get("GITWEB_PROJECT_MAXDEPTH", "1"))
PROJECTS_LIST = os.environ.get("GITWEB_LIST", PROJECTROOT)
SITE_NAME = os.environ.get("GITWEB_SITENAME", "") or "DistGit"
EXPORT_OK = os.environ.get("GITWEB_EXPORT_OK", "")
# When True, list all directories under project root without repo/export_ok checks (default on for now).
LIST_ALL = os.environ.get("GITWEB_LIST_ALL", "1").lower() in ("1", "true", "yes")
STRICT_EXPORT = os.environ.get("GITWEB_STRICT_EXPORT", "0").lower() in ("1", "true", "yes")
GIT_BINDIR = os.environ.get("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. This is useful for testing, development, or as a local admin panel - but USE WITH CAUTION.
RootProvider: Mostly a stub but provides admin login with an admin user/password. All other providers inherit from this.
SSHProvider: TODO - Will authenticate against a local SSH server using existing 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 = os.environ.get("DISTGIT_AUTH", "None")
DISTGIT_ADMIN_USER: str = os.environ.get("DISTGIT_ADMIN_USER", None)
DISTGIT_ADMIN_PASSWORD: str = os.environ.get("DISTGIT_ADMIN_PASSWORD", None)
DISTGIT_SESSION_TIMEOUT: str = os.environ.get("DISTGIT_SESSION_TIMEOUT", 3600 * 24 * 7)

# Config file paths (can be overridden by env)
GITWEB_CONFIG = os.environ.get("GITWEB_CONFIG", "")
GITWEB_CONFIG_SYSTEM = os.environ.get("GITWEB_CONFIG_SYSTEM", "")
GITWEB_CONFIG_COMMON = os.environ.get("GITWEB_CONFIG_COMMON", "")

# 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 os.path.exists(filename):
        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.environ.get("GITWEB_CONFIG", "")
    if not GITWEB_CONFIG_SYSTEM:
        GITWEB_CONFIG_SYSTEM = os.environ.get("GITWEB_CONFIG_SYSTEM", "")
    if not GITWEB_CONFIG_COMMON:
        GITWEB_CONFIG_COMMON = os.environ.get("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 os.path.exists(GITWEB_CONFIG_COMMON):
        read_config_file(GITWEB_CONFIG_COMMON)
    if GITWEB_CONFIG and os.path.exists(GITWEB_CONFIG):
        read_config_file(GITWEB_CONFIG)
        return
    if GITWEB_CONFIG_SYSTEM and os.path.exists(GITWEB_CONFIG_SYSTEM):
        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