import hashlib

from anchore_engine.db import AnalysisArtifact
from anchore_engine.services.policy_engine.engine.policy.gate import BaseTrigger, Gate
from anchore_engine.utils import ensure_bytes, ensure_str

MALWARE_CONTEXT_KEY = "malware_scans"


class ScanFindingsTrigger(BaseTrigger):
    __trigger_name__ = "scans"
    __description__ = (
        "Triggers if any malware scanner has found any matches in the image."
    )

    @staticmethod
    def _trigger_id(scanner, file, signature):
        """
        Trigger id is a string, but encoded as scanner name, signature, and m5hash of the file path (to keep size within reasonable bounds)
        :param scanner:
        :param file:
        :param signature:
        :return:
        """
        return "{}+{}+{}".format(
            scanner,
            signature,
            ensure_str(
                hashlib.new(
                    "md5", ensure_bytes(file), usedforsecurity=False
                ).hexdigest()
            ),
        )

    def evaluate(self, image_obj, context):
        try:
            scans = context.data[MALWARE_CONTEXT_KEY]
        except KeyError:
            scans = {}

        for scanner, scan in scans.items():
            scan_name = scan.get("scanner", scanner)
            for finding in scan.get("findings", []):
                signature = finding.get("signature")
                path = finding.get("path")
                if not path or not signature:
                    # Invalid finding, just for safety
                    continue

                trigger_inst_id = self._trigger_id(scan_name, path, signature)
                self._fire(
                    instance_id=trigger_inst_id,
                    msg="Malware scan finding: scanner={} file={} signature={}".format(
                        scan_name, path, signature
                    ),
                )


class ScanNotRunTrigger(BaseTrigger):
    __trigger_name__ = "scan_not_run"
    __description__ = "Triggers if no malware scan has been run on the image."

    def evaluate(self, image_obj, context):

        try:
            scans = context.data.get(MALWARE_CONTEXT_KEY)
        except AttributeError:
            scans = None

        if not scans:
            self._fire(msg="No malware scans found for image")


class MalwareGate(Gate):
    __gate_name__ = "malware"
    __description__ = "Checks for malware scan findings in the image"
    __triggers__ = [ScanFindingsTrigger, ScanNotRunTrigger]

    def prepare_context(self, image_obj, context):
        """
        prepare the context by extracting the file name list once and placing it in the eval context to avoid repeated
        loads from the db. this is an optimization and could removed.

        :rtype:
        :param image_obj:
        :param context:
        :return:
        """

        scans = image_obj.analysis_artifacts.filter(
            AnalysisArtifact.analyzer_id == "malware",
            AnalysisArtifact.analyzer_artifact == "malware",
            AnalysisArtifact.analyzer_type == "base",
        ).all()

        context.data[MALWARE_CONTEXT_KEY] = {
            scanner.artifact_key: scanner.json_value for scanner in scans
        }

        return context
