from __future__ import annotations

import logging
import os
import re
import subprocess
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

from auditwheel.error import InvalidLibcError

logger = logging.getLogger(__name__)


@dataclass(frozen=True, order=True)
class LibcVersion:
    major: int
    minor: int


class Libc(Enum):
    value: str

    GLIBC = "glibc"
    MUSL = "musl"
    ANDROID = "android"

    def __str__(self) -> str:
        return self.value

    def get_current_version(self) -> LibcVersion:
        if self == Libc.MUSL:
            return _get_musl_version(_find_musl_libc())
        if self == Libc.GLIBC:
            return _get_glibc_version()
        msg = f"can't determine version of libc '{self}'"
        raise InvalidLibcError(msg)

    @staticmethod
    def detect() -> Libc:
        # check musl first, default to GLIBC
        try:
            _find_musl_libc()
            logger.debug("Detected musl libc")
            return Libc.MUSL  # noqa: TRY300
        except InvalidLibcError:
            logger.debug("Falling back to GNU libc")
            return Libc.GLIBC

    @property
    def tag_prefix(self) -> str:
        return {
            Libc.GLIBC: "manylinux",
            Libc.MUSL: "musllinux",
            Libc.ANDROID: "android",
        }[self]


def _find_musl_libc() -> Path:
    try:
        (dl_path,) = list(Path("/lib").glob("libc.musl-*.so.1"))
    except ValueError:
        msg = "musl libc not detected"
        logger.debug("%s", msg)
        raise InvalidLibcError(msg) from None

    return dl_path


def _get_musl_version(ld_path: Path) -> LibcVersion:
    try:
        ld = subprocess.run(
            [ld_path],
            check=False,
            errors="strict",
            stderr=subprocess.PIPE,
        ).stderr
    except FileNotFoundError as err:
        msg = "failed to determine musl version"
        logger.exception("%s", msg)
        raise InvalidLibcError(msg) from err

    match = re.search(r"Version (?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)", ld)
    if not match:
        msg = f"failed to parse musl version from string {ld!r}"
        raise InvalidLibcError(msg) from None

    return LibcVersion(int(match.group("major")), int(match.group("minor")))


def _get_glibc_version() -> LibcVersion:
    # CS_GNU_LIBC_VERSION is only for glibc and shall return e.g. "glibc 2.3.4"
    try:
        version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
        if version_string is None:
            msg = "failed to determine glibc version"
            raise InvalidLibcError(msg)
        _, version = version_string.rsplit()
    except (AttributeError, OSError, ValueError) as err:
        # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
        msg = "failed to determine glibc version"
        raise InvalidLibcError(msg) from err

    m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version)
    if not m:
        msg = f"failed to parse glibc version from string {version!r}"
        raise InvalidLibcError(msg)

    return LibcVersion(int(m.group("major")), int(m.group("minor")))
