"""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 typing import List, Optional, Dict

from hoppr_cyclonedx_models.cyclonedx_1_4 import Vulnerability


def __get_all_ids_from_vulnerability(from_vulnerability: Vulnerability):
    ids = [from_vulnerability.id]
    if from_vulnerability.references is not None:
        for reference in from_vulnerability.references:
            ids.append(reference.id)
    return list(set(ids))


def __combine_vunerabilities(
    existing: Vulnerability, new: Vulnerability
) -> Vulnerability:

    # pylint: disable=cell-var-from-loop

    if existing.cwes is None or len(existing.cwes) == 0:
        existing.cwes = new.cwes

    ## Combine advisories
    if existing.advisories is None and new.advisories is not None:
        existing.advisories = new.advisories
    elif new.advisories is not None:
        existing_urls = list(map(lambda x: x.url, existing.advisories))
        new_advisories = list(
            filter(lambda x: x.url not in existing_urls, new.advisories)
        )
        existing.advisories = existing.advisories + new_advisories

    # Combine Tools
    existing.tools = [] if existing.tools is None else existing.tools
    if new.tools is not None:
        existing_tool_names = list(map(lambda x: x.name, existing.tools))
        new_tools = list(filter(lambda x: x.name not in existing_tool_names, new.tools))
        existing.tools = new_tools + existing.tools

    # Combine Ratings
    existing.ratings = [] if existing.ratings is None else existing.ratings
    if new.ratings is not None:
        existing_reference_ids = list(map(lambda x: str(x.vector), existing.ratings))
        new_ratings = list(
            filter(lambda x: str(x.vector) not in existing_reference_ids, new.ratings)
        )
        existing.ratings = new_ratings + existing.ratings
        for rating in new.ratings:
            if rating.source is not None:
                existing_rating = list(
                    filter(lambda x: x.vector == rating.vector, existing.ratings)
                )[0]
                if (
                    existing_rating.source is None
                    or existing_rating.source.name is None
                ):
                    existing_rating.source = rating.source

    # Combine References
    existing.references = [] if existing.references is None else existing.references
    if new.references is not None:
        existing_reference_ids = list(map(lambda x: str(x.id), existing.references))
        new_references = list(
            filter(lambda x: str(x.id) not in existing_reference_ids, new.references)
        )
        existing.references = new_references + existing.references

    # Combine Recommendation
    existing.recommendation = (
        "" if existing.recommendation is None else existing.recommendation
    )
    if new.recommendation is not None and 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:
    length = len([value for value in existing_id_list if value in new_id_list]) > 0

    return length


def __get_matching_vulnerability_from_list(
    combined_list: List[Vulnerability], new_finding: Vulnerability
) -> Optional[Vulnerability]:
    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 "CVE-2021-23926" in existing_finding_ids or "CVE-2021-23926" in new_finding_ids:
        #     print(f"{existing_finding_ids} new = {new_finding_ids}  in both = {__does_id_exist_in_both(existing_finding_ids, new_finding_ids)}")

        if __does_id_exist_in_both(existing_finding_ids, new_finding_ids):
            return vuln
    return None


def combine_vulnerabilities(
    vulnerabilities_list: List[Dict[str, Optional[List[Vulnerability]]]]
) -> Dict[str, Optional[List[Vulnerability]]]:
    """Intelligently combines the lists of vulnerabilities found by different scanners"""
    combined_result = {}
    for result in vulnerabilities_list:
        for purl in result:
            tool_vulnerabilities = result[purl]
            if purl not in combined_result:
                combined_result[purl] = []

            for vulnerability in tool_vulnerabilities:
                exisiting = __get_matching_vulnerability_from_list(
                    combined_result[purl], vulnerability
                )
                if exisiting is None:
                    combined_result[purl].append(vulnerability)
                else:
                    __combine_vunerabilities(exisiting, vulnerability)
    return combined_result
