# Source code for debsbom.apt.copyright

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

    from collections.abc import Iterable
    from functools import cached_property
    from debian.copyright import (
        Copyright as DebCopyright,
        License,
        MachineReadableFormatError,
        NotMachineReadableError,
    )
    from license_expression import LicenseExpression, Licensing, get_spdx_licensing
    import logging
    from pathlib import Path

    logger = logging.getLogger(__name__)

    # well-known list of expressions conversions, see
    # https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-specification
    WELL_KNOWN_EXPRESSIONS = {
        "AGPL-1": "AGPL-1.0-only",
        "AGPL-1+": "AGPL-1.0-or-later",
        "AGPL-3": "AGPL-3.0-only",
        "AGPL-3+": "AGPL-3.0-or-later",
        "Apache-1": "Apache-1.0",
        "Apache-2": "Apache-2.0",
        "Artistic": "Artistic-1.0",
        "Artistic-1": "Artistic-1.0",
        "Artistic-2": "Artistic-2.0",
        "BSD-2": "BSD-2-Clause",
        "BSD-3": "BSD-3-Clause",
        "BSD-4": "BSD-4-Clause",
        "BSL": "BSL-1.0",
        "BSL-1": "BSL-1.0",
        "CC-BY": "CC-BY-1.0",
        "CC-BY-1": "CC-BY-1.0",
        "CC-BY-2": "CC-BY-2.0",
        "CC-BY-3": "CC-BY-3.0",
        "CC-BY-4": "CC-BY-4.0",
        "CC-BY-NC": "CC-BY-NC-1.0",
        "CC-BY-NC-1": "CC-BY-NC-1.0",
        "CC-BY-NC-2": "CC-BY-NC-2.0",
        "CC-BY-NC-3": "CC-BY-NC-3.0",
        "CC-BY-NC-4": "CC-BY-NC-4.0",
        "CC-BY-NC-ND": "CC-BY-NC-ND-1.0",
        "CC-BY-NC-ND-1": "CC-BY-NC-ND-1.0",
        "CC-BY-NC-ND-2": "CC-BY-NC-ND-2.0",
        "CC-BY-NC-ND-3": "CC-BY-NC-ND-3.0",
        "CC-BY-NC-ND-4": "CC-BY-NC-ND-4.0",
        "CC-BY-NC-SA": "CC-BY-NC-SA-1.0",
        "CC-BY-NC-SA-1": "CC-BY-NC-SA-1.0",
        "CC-BY-NC-SA-2": "CC-BY-NC-SA-2.0",
        "CC-BY-NC-SA-3": "CC-BY-NC-SA-3.0",
        "CC-BY-NC-SA-4": "CC-BY-NC-SA-4.0",
        "CC-BY-SA": "CC-BY-SA-1.0",
        "CC-BY-SA-1": "CC-BY-SA-1.0",
        "CC-BY-SA-2": "CC-BY-SA-2.0",
        "CC-BY-SA-3": "CC-BY-SA-3.0",
        "CC-BY-SA-4": "CC-BY-SA-4.0",
        "CC0": "CC0-1.0",
        "CC0-1": "CC0-1.0",
        "CDDL": "CDDL-1.0",
        "CDDL-1": "CDDL-1.0",
        "CPL": "CPL-1.0",
        "CPL-1": "CPL-1.0",
        "EFL": "EFL-1.0",
        "EFL-1": "EFL-1.0",
        "EFL-2": "EFL-2.0",
        "EPL": "EPL-1.0",
        "EPL-1": "EPL-1.0",
        "EPL-2": "EPL-2.0",
        "Expat": "MIT",
        "GFDL": "GFDL-1.1-only",
        "GFDL+": "GFDL-1.1-or-later",
        "GFDL-1.1": "GFDL-1.1-only",
        "GFDL-1.1+": "GFDL-1.1-or-later",
        "GFDL-1.2": "GFDL-1.2-only",
        "GFDL-1.2+": "GFDL-1.2-or-later",
        "GFDL-1.3": "GFDL-1.3-only",
        "GFDL-1.3+": "GFDL-1.3-or-later",
        "GFDL-NIV": "GFDL-1.1-no-invariants-only",
        "GFDL-NIV+": "GFDL-1.1-no-invariants-or-later",
        "GFDL-NIV-1.1": "GFDL-1.1-no-invariants-only",
        "GFDL-NIV-1.1+": "GFDL-1.1-no-invariants-or-later",
        "GFDL-NIV-1.2": "GFDL-1.2-no-invariants-only",
        "GFDL-NIV-1.2+": "GFDL-1.2-no-invariants-or-later",
        "GFDL-NIV-1.3": "GFDL-1.3-no-invariants-only",
        "GFDL-NIV-1.3+": "GFDL-1.3-no-invariants-or-later",
        "GPL": "GPL-1.0-only",
        "GPL+": "GPL-1.0-or-later",
        "GPL-1": "GPL-1.0-only",
        "GPL-1+": "GPL-1.0-or-later",
        "GPL-2": "GPL-2.0-only",
        "GPL-2+": "GPL-2.0-or-later",
        "GPL-3": "GPL-3.0-only",
        "GPL-3+": "GPL-3.0-or-later",
        "GPL3+": "GPL-3.0-or-later",
        "LGPL": "LGPL-1.0-only",
        "LGPL+": "LGPL-1.0-or-later",
        "LGPL-1": "LGPL-1.0-only",
        "LGPL-1+": "LGPL-1.0-or-later",
        "LGPL-2": "LGPL-2.0-only",
        "LGPL-2+": "LGPL-2.0-or-later",
        "LGPL-3": "LGPL-3.0-only",
        "LGPL-3+": "LGPL-3.0-or-later",
        "LPGL-2.1": "LGPL-2.1-only",
        "LPGL-2.1+": "LGPL-2.1-or-later",
        "LPPL": "LPPL-1.0",
        "LPPL-1": "LPPL-1.0",
        "MPL": "MPL-1.0",
        "MPL-1": "MPL-1.0",
        "MPL-2": "MPL-2.0",
        "PSF-2": "PSF-2.0",
        "Perl": "Artistic-1.0-Perl",
        "Python": "Python-2.0",
        "QPL": "QPL-1.0",
        "QPL-1": "QPL-1.0",
        "RubyLicense": "Ruby",
        "SIL-1": "OFL-1.0",
        "SIL-1.1": "OFL-1.1",
        "SIL-OFL-1": "OFL-1.0",
        "SIL-OFL-1.1": "OFL-1.1",
        "Zope": "ZPL-1.0",
        "Zope-1": "ZPL-1.0",
        "Zope-2": "ZPL-2.0",
    }

    class UnknownLicenseError(Exception):
        """License is unknown to the SPDX standard."""

        pass

    class Copyright(DebCopyright):
        """Copyright information for a source package."""

        def __init__(self, path: Path):
            if not path.is_file():
                raise FileNotFoundError(path)
            self._path = path

        @classmethod
        def _convert_expression(cls, line: str) -> str:
            """Convert a Debian license expression to the equivalent SPDX syntax."""

            # in the Debian copyright format ',' are used for disambiguation
            # that means (with 'and' having precedence over 'or'):
            # A or B and C <=> A or (B and C)
            # A or B, and C <=> (A or B) and C
            # for more information see
            # https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-specification
            tokens = line.split(" ")
            indices = []
            for i, token in enumerate(tokens):
                if token.endswith(","):
                    tokens[i] = token[:-1]
                    indices.append(i)

            # keep track of inserted tokens
            inserted = 0
            for i in range(len(indices)):
                # the opening brace can be inserted always at the beginning
                tokens.insert(0, "(")
                # and the closing brace after the token that had the comma
                tokens.insert(indices[i] + inserted + 2, ")")
                inserted += 2

            return " ".join(tokens)

        def licenses(self) -> Iterable[License]:
            """Return all licenses found in the copyright file."""
            with open(self._path) as f:
                try:
                    yield from self._parse_licenses(DebCopyright(f))
                except NotMachineReadableError:
                    logger.debug(f"non-machine-readable copyright file: {self._path}")
                except (MachineReadableFormatError, ValueError):
                    logger.debug(f"bad format for machine-readable copyright file: {self._path}")

        def _parse_licenses(self, copyright: DebCopyright) -> Iterable[License]:
            lic = copyright.header.license
            if lic:
                yield lic

            for paragraph in copyright.all_files_paragraphs():
                yield paragraph.license

        @cached_property
        def _spdx_license_expressions(self) -> list[LicenseExpression]:
            """Return all licenses as SPDX license expressions."""
            licensing = get_spdx_licensing()
            exprs = []
            for lic in self.licenses():
                if not lic.synopsis:
                    raise UnknownLicenseError("only license text is available")

                expr = self._convert_expression(lic.synopsis)
                spdx_lic = licensing.parse(expr)
                unknown_keys = licensing.unknown_license_keys(spdx_lic)
                # TODO: how do we handle `public-domain` licensing?
                # TODO: license exceptions
                if len(unknown_keys) > 0:
                    # unknown keys in the license expression, try to replace them
                    # with well-known representations
                    unreplaced = []
                    for unknown_key in unknown_keys:
                        replacement = WELL_KNOWN_EXPRESSIONS.get(unknown_key)
                        if replacement:
                            expr = expr.replace(unknown_key, replacement)
                        else:
                            unreplaced.append(unknown_key)

                    if len(unreplaced) > 0:
                        s = ", ".join(unreplaced)
                        raise UnknownLicenseError(f"unknown license keys: {s}")
                    exprs.append(licensing.parse(expr, validate=True))
                else:
                    exprs.append(spdx_lic)

            if not len(exprs):
                raise UnknownLicenseError("no license information available")

            return exprs

        def spdx_license_expressions(self) -> Iterable[LicenseExpression]:
            yield from self._spdx_license_expressions

    class CopyrightDirectory:
        """Directory of Debian copyright information."""

        def __init__(self, path: Path):
            self.copyright_root = path

        @classmethod
        def for_rootdir(cls, root: Path) -> "CopyrightDirectory":
            return CopyrightDirectory(root / "usr/share/doc")

        def copyright(self, bin_pkg) -> Copyright:
            """Get the copyright information for a binary package."""
            copyright_path = self.copyright_root / bin_pkg.name / "copyright"
            return Copyright(copyright_path)