"""Defines the common interface for a vulnerability scanner.

--------------------------------------------------------------------------------
SPDX-FileCopyrightText: Copyright © 2022 Lockheed Martin <open.source@lmco.com>
SPDX-FileName: hopprcop/vulnerability_scanner.py
SPDX-FileType: SOURCE
SPDX-License-Identifier: MIT
--------------------------------------------------------------------------------
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
"""

from __future__ import annotations

import distutils.spawn
import os

from typing import TYPE_CHECKING, ClassVar

import rich

from hoppr import Component, ComponentType, Tools, cdx
from hoppr.utils import get_package_url
from typing_extensions import deprecated


if TYPE_CHECKING:
    from hoppr import Sbom, Vulnerability
    from packageurl import PackageURL


class VulnerabilityMeta(type):
    """Metaclass to define interface expectations of a vulnerability scanner."""

    @classmethod
    def __instancecheck__(cls, instance: object) -> bool:
        return cls.__subclasscheck__(type(instance))

    @classmethod
    def __subclasscheck__(cls, subclass: type) -> bool:
        return all(
            [
                callable(getattr(subclass, "get_vulnerabilities_by_purl", None)),
                callable(getattr(subclass, "get_vulnerabilities_by_sbom", None)),
                callable(getattr(subclass, "should_activate", None)),
                getattr(subclass, "_scanner_author", None),
                getattr(subclass, "_scanner_name", None),
            ]
        )


class VulnerabilitySuper(metaclass=VulnerabilityMeta):
    """A super class that defines the expected interface for a vulnerability scanner."""

    _scanner_author: str
    _scanner_name: str

    required_tools_on_path: ClassVar[list[str]] = []
    required_environment_variables: ClassVar[list[str]] = []
    offline_mode_supported: bool = False
    offline_mode: bool = False

    def __init_subclass__(
        cls, author: str | None = None, name: str | None = None, offline_mode_supported: bool = False
    ) -> None:
        super().__init_subclass__()

        cls.offline_mode_supported = offline_mode_supported

        # Set class attributes from keyword args if not set in class definition
        for attr_name in {"_scanner_author", "_scanner_name"}:
            attr_value = getattr(cls, attr_name, None) or locals().get(attr_name.removeprefix("_scanner_"))
            setattr(cls, attr_name, attr_value)

            if not getattr(cls, attr_name):
                raise NotImplementedError(f"{cls.__name__} must implement '{attr_name}' class attribute")

    def get_vulnerability_db(self) -> bool:
        """Downloads vulnerability database.

        Return status of downloading the database as a boolean.
        """
        return False

    def get_vulnerabilities_for_purl(self, purls: list[str]) -> list[Vulnerability]:
        """Get the vulnerabilities for a list of package URLS (purls).

        Returns a list of CycloneDX vulnerabilities.
        """
        return []

    def get_vulnerabilities_for_sbom(self, bom: Sbom) -> list[Vulnerability]:
        """Get the vulnerabilities for a CycloneDx compatible Software Bill of Materials (SBOM).

        Returns a list of CycloneDX vulnerabilities.
        """
        purls = [component.purl for component in bom.components or [] if component.purl]

        return self.get_vulnerabilities_for_purl(purls)

    @deprecated(
        "VulnerabilitySuper.get_vulnerabilities_by_purl is deprecated, use VulnerabilitySuper.get_vulnerabilities_for_purl"
    )
    def get_vulnerabilities_by_purl(self, purls: list[PackageURL]) -> dict[str, list[Vulnerability]]:
        """Get the vulnerabilities for a list of package URLS (purls).

        Return a dictionary of package URL to vulnerabilities or none if no vulnerabilities are found.
        """
        return {}

    @deprecated(
        "VulnerabilitySuper.get_vulnerabilities_by_sbom is deprecated, use VulnerabilitySuper.get_vulnerabilities_for_sbom"
    )
    def get_vulnerabilities_by_sbom(self, bom: Sbom) -> dict[str, list[Vulnerability]]:
        """Accepts a cyclone dx compatible SBOM and returns a list of vulnerabilities.

        Return a dictionary of package URL to vulnerabilities or none if no vulnerabilities are found.
        """
        purls = [get_package_url(component.purl) for component in bom.components or [] if component.purl]

        return self.get_vulnerabilities_by_purl(purls)

    def scanner_tools(self) -> Tools:
        """Return `Tools` object with a component describing the scanner tool."""
        return Tools(
            components=[
                Component(
                    name=self._scanner_name,
                    supplier=cdx.OrganizationalEntity(name=self._scanner_author),
                    type=ComponentType.APPLICATION,
                )
            ]
        )

    def should_activate(self) -> bool:
        """Checks if the vulnerability scanner should activate based on it's requirements."""
        activate = True

        if missing_tools := [tool for tool in self.required_tools_on_path if not distutils.spawn.find_executable(tool)]:
            activate = False
            rich.print(f"{type(self).__name__} is not activated because required tools are not on the PATH:")
            rich.print(f"{', '.join(missing_tools)}")

        if missing_env := set(self.required_environment_variables).difference(os.environ):
            activate = False
            rich.print(f"{type(self).__name__} is not activated because required environment variables are not set:")
            rich.print(f"{', '.join(missing_env)}")

        if self.offline_mode and not self.offline_mode_supported:
            activate = False
            rich.print(f"{type(self).__name__} is not activated because offline mode is not supported by this scanner")

        return activate
