"""Module contains the JCOP backends for the malware detection system."""

import hashlib
import logging
from http import HTTPStatus

import requests
from django.core.files.storage import default_storage
from django.db import transaction
from requests_toolbelt import MultipartEncoder

from ..enums import ReportStatus
from ..exceptions import MalwareDetectionInvalidAuthenticationError
from ..models import MalwareDetection, MalwareDetectionStatus
from ..tasks.jcop import analyse_file_async, trigger_new_analysis
from .base import BaseBackend

logger = logging.getLogger(__name__)


class JCOPBackend(BaseBackend):
    """A backend that uses JCOP to detect malware."""

    def __init__(  # noqa: PLR0913
        self,
        base_url: str,
        api_key: str,
        callback_path: str,
        result_timeout: int = 30,
        submit_timeout: int = 10 * 60,
        max_processing_files: int = 15,
    ):
        """Configure the JCOP backend."""
        super().__init__(
            callback_path=callback_path,
            max_processing_files=max_processing_files,
        )

        self.base_url = base_url
        self.api_key = api_key
        self.result_timeout = result_timeout
        self.submit_timeout = submit_timeout

    def analyse_file(self, file_path: str, **kwargs) -> None:
        """Trigger a process to analyse a file using JCOP service."""
        if not default_storage.exists(file_path):
            raise FileNotFoundError(f"File {file_path} not found")

        if self.max_processing_files == 0:
            # if max_processing_files is 0, we don't want to create a detection record
            # we just want to analyse the file asynchronously
            analyse_file_async.delay(
                file_path,
                **kwargs,
            )
            return

        MalwareDetection.objects.create(
            path=file_path,
            status=MalwareDetectionStatus.PENDING,
            backend=self.backend_name,
            parameters=kwargs,
        )

        self.launch_next_analysis()

    def reschedule_processing_task(self, malware_detection_record: MalwareDetection) -> None:
        """Reschedule the processing task for a malware detection record."""
        if malware_detection_record.status != MalwareDetectionStatus.PROCESSING:
            return

        if malware_detection_record.backend != self.backend_name:
            return

        # assert first the file still exists in the system
        if not default_storage.exists(malware_detection_record.path):
            logger.info("File %s not found when rescheduling processing task", malware_detection_record.path)
            malware_detection_record.delete()
            return

        analyse_file_async.delay(
            malware_detection_record.path,
            **malware_detection_record.parameters,
        )

    def launch_next_analysis(self) -> None:
        """Launch the next pending analysis."""
        if (
            MalwareDetection.objects.filter(status=MalwareDetectionStatus.PROCESSING).count()
            >= self.max_processing_files
        ):
            return

        with transaction.atomic():
            first_pending_detection = (
                MalwareDetection.objects.select_for_update()
                .filter(status=MalwareDetectionStatus.PENDING)
                .order_by("created_at")
                .first()
            )
            if first_pending_detection is None:
                return
            first_pending_detection.status = MalwareDetectionStatus.PROCESSING
            first_pending_detection.save(update_fields=["status"])

        analyse_file_async.delay(
            first_pending_detection.path,
            **first_pending_detection.parameters,
        )

    def delete_detection(self, file_path: str) -> None:
        """Delete a detection record."""
        try:
            detection = MalwareDetection.objects.get(path=file_path)
        except MalwareDetection.DoesNotExist:
            logger.warning("Detection %s not found", file_path)
            return
        else:
            detection.delete()

    def failed_analysis(
        self, file_path: str, error_code: int, error_msg: str, status: ReportStatus | None = None, **kwargs
    ) -> None:
        """Handle a failed analysis."""
        try:
            detection = MalwareDetection.objects.get(path=file_path)
        except MalwareDetection.DoesNotExist:
            logger.warning("Detection %s not found", file_path)
        else:
            detection.status = MalwareDetectionStatus.FAILED
            detection.error_code = error_code
            detection.error_msg = error_msg
            detection.save(update_fields=["status", "error_code", "error_msg"])

        self.launch_next_analysis()
        self.callback(
            file_path,
            status if status is not None else ReportStatus.UNKNOWN,
            error_info={
                "error": error_msg,
                "error_code": error_code,
            },
            **kwargs,
        )

    def succeed_analysis(self, file_path: str, **kwargs) -> None:
        """Handle a successful analysis."""
        self.delete_detection(file_path)
        self.launch_next_analysis()
        self.callback(file_path, ReportStatus.SAFE, error_info={}, **kwargs)

    def check_analysis(self, file_path: str, file_hash: str | None = None, **kwargs) -> bool:
        """Start the analysis process for a file."""
        if file_hash is None:
            with default_storage.open(file_path, "rb") as file:
                file_hash = hashlib.file_digest(file, "sha256").hexdigest()

        try:
            # try if the file as already been tested
            response = requests.get(
                f"{self.base_url}/results/{file_hash}",
                headers={
                    "X-Auth-Token": self.api_key,
                    "Accept": "application/json",
                },
                timeout=self.result_timeout,
            )
        except requests.exceptions.RequestException as exc:
            logger.error("Error getting cache result for file %s: %s", file_path, exc)
            raise

        if response.status_code == HTTPStatus.NOT_FOUND:
            # start a new analysis
            trigger_new_analysis.delay(
                file_path,
                file_hash,
                **kwargs,
            )
            return False

        if response.status_code == HTTPStatus.UNAUTHORIZED:
            self.failed_analysis(
                file_path, response.status_code, "Invalid API key", status=ReportStatus.UNKNOWN, **kwargs
            )
            raise MalwareDetectionInvalidAuthenticationError()

        if response.status_code == HTTPStatus.OK:
            content = response.json()
            if content.get("done", False) is False:
                # the analysis is not done yet, retry later
                return True

            is_malware = content.get("is_malware")
            if is_malware is True or content.get("error_code"):
                status = ReportStatus.UNSAFE if is_malware else ReportStatus.UNKNOWN
                self.failed_analysis(
                    file_path,
                    content.get("error_code", 5000),
                    content.get("error", "malware detected"),
                    status,
                    **kwargs,
                )
                return False

            if is_malware is False:
                self.succeed_analysis(file_path, **kwargs)
                return False

        # Any other case, call the callback with an unknown error
        self.failed_analysis(
            file_path, response.status_code, "Unknown treatment", status=ReportStatus.UNKNOWN, **kwargs
        )
        return False

    def trigger_new_analysis(self, file_path: str, file_hash: str, **kwargs):
        """Trigger a new analysis for a file."""
        with default_storage.open(file_path, "rb") as file:
            encoder = MultipartEncoder(
                fields={
                    "file": (file.name, file),
                }
            )
            try:
                response = requests.post(
                    f"{self.base_url}/submit",
                    headers={
                        "X-Auth-Token": self.api_key,
                        "Accept": "application/json",
                        "Content-Type": encoder.content_type,
                    },
                    data=encoder,
                    timeout=(30, self.submit_timeout),
                )
            except requests.exceptions.RequestException:
                logger.error("Error while sending file %s to JCOP with hash %s", file_path, file_hash)
                raise

        if response.status_code == HTTPStatus.OK:
            content = response.json()
            analyse_file_async.apply_async(
                countdown=5,
                args=(file_path,),
                kwargs={"file_hash": content["id"], **kwargs},
            )
            return

        if response.status_code == HTTPStatus.UNAUTHORIZED:
            self.failed_analysis(
                file_path, response.status_code, "Invalid API key", status=ReportStatus.UNKNOWN, **kwargs
            )
            raise MalwareDetectionInvalidAuthenticationError()

        if response.status_code == HTTPStatus.REQUEST_TIMEOUT:
            raise TimeoutError()

        if response.status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
            self.failed_analysis(
                file_path, response.status_code, "File too large", status=ReportStatus.UNKNOWN, **kwargs
            )
            return

        self.failed_analysis(
            file_path, response.status_code, "Unknown treatment", status=ReportStatus.UNKNOWN, **kwargs
        )
