"""This file provides the ability to combine vulnerabilities from multiple sources into a single model."""
# This file is part of hoppr-cop
#
# Licensed under the MIT License;
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://opensource.org/licenses/MIT
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# Copyright (c) 2022 Lockheed Martin Corporation
from __future__ import annotations

from typing import TYPE_CHECKING


if TYPE_CHECKING:
    from hoppr_cyclonedx_models.cyclonedx_1_4 import Vulnerability


def __get_all_ids_from_vulnerability(from_vulnerability: Vulnerability) -> list[str]:
    ids = list(filter(None, [from_vulnerability.id]))

    if from_vulnerability.references is not None:
        ids.extend(reference.id for reference in from_vulnerability.references)

    return list(set(ids))


def __combine_vulnerabilities(existing: Vulnerability, new: Vulnerability) -> Vulnerability:
    if existing.cwes is None or len(existing.cwes) == 0:
        existing.cwes = new.cwes

    # Combine advisories
    existing.advisories = existing.advisories or []
    new.advisories = new.advisories or []

    existing.advisories = list(
        {advisory.url: advisory for advisory in [*existing.advisories, *new.advisories]}.values(),
    )

    # Combine Tools
    existing.tools = existing.tools or []
    new.tools = new.tools or []

    existing.tools = list({tool.name: tool for tool in [*existing.tools, *new.tools]}.values())

    # Combine Ratings
    existing.ratings = existing.ratings or []
    new.ratings = new.ratings or []

    existing_reference_ids = [x.vector for x in existing.ratings]
    existing.ratings.extend(rating for rating in new.ratings if rating.vector not in existing_reference_ids)

    for new_rating in [rating for rating in new.ratings if rating.source is not None]:
        existing_rating = next(ex_rtng for ex_rtng in existing.ratings if ex_rtng.vector == new_rating.vector)

        if existing_rating and (not existing_rating.source or not existing_rating.source.name):
            existing_rating.source = new_rating.source

    # Combine References
    existing.references = existing.references or []
    new.references = new.references or []

    existing.references = list({ref.id: ref for ref in [*existing.references, *new.references]}.values())

    # Combine Recommendation
    existing.recommendation = existing.recommendation or ""

    if new.recommendation:
        existing.recommendation = f"{existing.recommendation}.  ||  {new.recommendation}"

    return existing


def __does_id_exist_in_both(existing_id_list: list[str], new_id_list: list[str]) -> bool:
    return len([value for value in existing_id_list if value in new_id_list]) > 0


def __get_matching_vulnerability_from_list(
    combined_list: list[Vulnerability], new_finding: Vulnerability
) -> Vulnerability | None:
    new_finding_ids = __get_all_ids_from_vulnerability(new_finding)
    for vuln in combined_list:
        existing_finding_ids = __get_all_ids_from_vulnerability(vuln)

        if __does_id_exist_in_both(existing_finding_ids, new_finding_ids):
            return vuln

    return None


def combine_vulnerabilities(
    vulnerabilities_list: list[dict[str, list[Vulnerability] | None]],
) -> dict[str, list[Vulnerability]]:
    """Intelligently combines the lists of vulnerabilities found by different scanners."""
    combined_result: dict[str, list[Vulnerability]] = {}

    for result in vulnerabilities_list:
        for purl in result:
            tool_vulnerabilities = result.get(purl)

            if purl not in combined_result:
                combined_result[purl] = []

            for vulnerability in tool_vulnerabilities or []:
                existing = __get_matching_vulnerability_from_list(combined_result[purl], vulnerability)

                combined_result[purl].append(
                    __combine_vulnerabilities(existing, vulnerability) if existing else vulnerability,
                )

    return combined_result
