# Copyright (C) 2025 Siemens
#
# SPDX-License-Identifier: MIT

from collections.abc import Set
from dataclasses import dataclass
from enum import Enum
import re

from .dpkg.package import BinaryPackage, Dependency, Package

CDX_REF_PREFIX = "CDXRef-"
CDX_PACKAGE_EXTREF_TYPE_WEBSITE = "website"

SPDX_REF_PREFIX = "SPDXRef-"
SPDX_REF_DOCUMENT = SPDX_REF_PREFIX + "DOCUMENT"
SPDX_REFERENCE_TYPE_PURL = "purl"
SPDX_REFERENCE_TYPE_VCS = "vcs"
SPDX_REFERENCE_TYPE_DISTRIBUTION = "distribution"
# SPDX IDs only allow alphanumeric, '.' and '-'
SPDX_ID_RE = re.compile(r"[^A-Za-z0-9.\-]+")

# cues for an organization in the maintainer name
SPDX_SUPPLIER_ORG_CUE = [
    "maintainers",
    "group",
    "developers",
    "team",
    "project",
    "task force",
    "strike force",
    "packagers",
    "users",
]


# pattern to match the common "John Doe <john@doe.com>"
SUPPLIER_PATTERN = re.compile("(?P<supplier_name>^[^<]+)(\\<(?P<supplier_email>.+)\\>)?")


class SBOMType(Enum):
    """Supported SBOM types"""

    CycloneDX = (0,)
    """CycloneDX"""
    SPDX = (1,)
    """SPDX"""

    @classmethod
    def from_str(cls, bomtype: str):
        match bomtype.lower():
            case "cdx":
                return cls.CycloneDX
            case "spdx":
                return cls.SPDX
            case _:
                raise RuntimeError(f"Unknown SBOM type '{bomtype}'")

    def __str__(self):
        match self:
            case self.CycloneDX:
                return "cdx"
            case self.SPDX:
                return "spdx"
            case _:
                assert False, "unreachable"

    def validate_dependency_availability(self) -> None:
        """
        Check if the required imports for the SBOM type are available.
        Raises RuntimeError if not available.
        """
        if self == SBOMType.CycloneDX:
            try:
                import cyclonedx.model.component
            except ModuleNotFoundError as e:
                raise RuntimeError(
                    "Missing dependency: cyclonedx-python-lib; install it by enabling the"
                    "dependency extra `cdx`: `pip install debsbom[cdx]`"
                )
        elif self == SBOMType.SPDX:
            try:
                import spdx_tools.spdx.model.package
            except ModuleNotFoundError as e:
                raise RuntimeError(
                    "Missing dependency: spdx-tools; install it by enabling the"
                    "dependency extra `spdx`: `pip install debsbom[spdx]`"
                )


class BOM_Standard(Enum):
    """Controls the data representation and added values in the SBOM"""

    DEFAULT = (0,)
    STANDARD_BOM = (1,)


@dataclass
class Reference:
    """Generic reference in a SBOM."""

    target: str
    is_source: bool = False

    def as_str(self, sbom_type: SBOMType) -> str:
        """Return a string representation for the given SBOM type."""
        if sbom_type == SBOMType.CycloneDX:
            s = CDX_REF_PREFIX + self.target
        elif sbom_type == SBOMType.SPDX:
            s = SPDX_REF_PREFIX + re.sub(SPDX_ID_RE, ".", self.target)

        if self.is_source:
            s += "-srcpkg"
        return s

    @classmethod
    def lookup(
        cls,
        pkg: BinaryPackage,
        dep: Dependency,
        sbom_type: SBOMType,
        dependency_refs: dict[str, Package],
        native_arch: str,
    ) -> str | None:
        """
        For imprecise references (without architecture), locate the matching
        reference id within the id passed as known_refs. Returns None if
        not found. To avoid downstream double caching of IDs in two formats, we pass the
        known_refs as a string set.
        """
        if dep.arch:
            ref = Reference.make_from_dep(dep).as_str(sbom_type)
            ref_pkg = dependency_refs.get(ref)
            if ref_pkg and ref_pkg.is_binary():
                if ref_pkg.satisfies(dep):
                    return ref
                else:
                    return None
            else:
                # source package dependencies are not versioned
                return ref
        candidates = map(
            lambda a: Reference.make_from_dep(dep, a).as_str(sbom_type),
            set([pkg.architecture, native_arch, "all"]),
        )
        for candidate in candidates:
            pkg = dependency_refs.get(candidate)
            if pkg and pkg.satisfies(dep):
                return Reference.make_from_pkg(pkg).as_str(sbom_type)
        return None

    @staticmethod
    def make_from_pkg(pkg: Package) -> "Reference":
        """
        Return a reference to a package in the list of all packages.
        This representation must match the one returned by ``make_from_dep``.
        """
        if pkg.is_source():
            return Reference(target=f"{pkg.name}-{pkg.version}", is_source=True)
        elif pkg.is_binary():
            return Reference(target=f"{pkg.name}-{pkg.architecture}", is_source=False)
        raise NotImplementedError()

    @staticmethod
    def make_from_dep(dep: Dependency, to_arch: str | None = None) -> "Reference":
        """
        Return a reference to a package from a dependency. If the dependency does
        not specify an architecture, the caller is responsible for providing this
        in ``to_arch``.
        """
        if "source" in [dep.arch, to_arch]:
            return Reference(target=f"{dep.name}-{dep.version[1]}", is_source=True)
        else:
            to_arch = to_arch or dep.arch
            return Reference(target=f"{dep.name}-{to_arch}", is_source=False)


class BomSpecific:
    """Mixin to denote that a class processes bom type specific data"""

    @classmethod
    def sbom_type(cls) -> SBOMType:
        """Type of SBOM this class can handle"""
        return cls._sbom_type


class CDXType(BomSpecific):
    """CycloneDX type mixin"""

    _sbom_type = SBOMType.CycloneDX


class SPDXType(BomSpecific):
    """SPDX type mixin"""

    _sbom_type = SBOMType.SPDX
