# Copyright © 2024 Contrast Security, Inc.
# See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
from functools import cached_property
from typing import Optional, Callable

from contrast.agent import scope

from contrast.agent.exclusions import Exclusions
from contrast.agent.request import Request
from contrast.agent.settings import Settings
from contrast.api.route_coverage import Route
from contrast_vendor import structlog as logging
from contrast.utils.digest_utils import Digest
from contrast.utils.decorators import fail_quietly
from contrast.reporting.request_masker import RequestMasker

logger = logging.getLogger("contrast")


class RequestContext:
    @scope.contrast_scope()
    def __init__(
        self,
        environ,
        assess_enabled: bool,
        exclusions: Optional[Exclusions] = None,
        request_data_masker: Optional[RequestMasker] = None,
    ):
        self.request = Request(environ)
        self.response = None

        # This contains any Observed Library Usage seen during the life of this request.
        self.observed_library_usage = None

        self.exclusions = exclusions

        self.request_data_masker = None

        if request_data_masker:
            self.request_data_masker = RequestMasker.new_request_masker(
                request_data_masker.mask_rules
            )

        # This contains input exclusions that should always apply regardless of rule on this request
        self.input_exclusions = None

        # This contains exclusions that only apply at trigger time based on the source event
        self.input_exclusions_trigger_time = None

        self.excluded_assess_rules = []
        self.excluded_protect_rules = []

        # For protect: store attacks made during a request to report at the end
        self.attacks = []

        # For assess: store findings made during a request to report at the end
        self.findings = []

        self.user_input_analysis = []
        self.response = None
        self.do_not_track = False

        self.view_func: Optional[Callable] = None
        self.view_func_str: Optional[str] = None

        self.observed_route = Route("", "", "")
        self.database_info = []

        self.assess_enabled = assess_enabled
        if self.assess_enabled:
            self.source_count = 0
            self.propagation_count = 0

            self.max_sources_logged = False
            self.max_propagators_logged = False

    @cached_property
    @fail_quietly("Unable to compute request hash")
    def hash(self):
        """
        Generates the hash checksum for the request. Converts the request method, uri,
        param names and content length to CRC checksum and returns string representation

        :return: str
        """
        hasher = Digest()

        hasher.update(self.request.method)
        hasher.update(self.request.get_normalized_uri())
        for key in self.request.params:
            hasher.update(key)

        hasher.update(self.request.content_length)
        return hasher.finish()

    def stop_source_creation(self, source_type, source_name):
        """
        Compare `source_count` to `max_context_source_events` config option
        :return: true if source_count within this request is equal to or
                 greater than configured threshold for source creation
        """
        if self.max_sources_logged:
            return True

        if self.exclusions and self.exclusions.evaluate_input_exclusions(
            self, source_type, source_name
        ):
            return True

        threshold_reached = self.source_count >= Settings().max_sources
        if threshold_reached:
            logger.warning(
                "Will not create more sources in this request. %s sources reached",
                self.source_count,
            )
            self.max_sources_logged = True

        return threshold_reached

    @property
    def stop_propagation(self):
        """
        Compare `propagation_count` to `max_propagation_events` config option
        :return: true if propagation_count within this request is equal to or
                 greater than configured threshold for propagation
        """
        if self.max_propagators_logged:
            return True

        threshold_reached = self.propagation_count >= Settings().max_propagation
        if threshold_reached:
            logger.warning(
                "Will not propagate any more in this request. %s propagations reached.",
                self.propagation_count,
            )
            self.max_propagators_logged = True

        return threshold_reached

    @property
    def propagate_assess(self):
        # TODO: PYT-644 move this property of out this class?
        return self.assess_enabled and not scope.in_contrast_or_propagation_scope()

    def source_created(self):
        """
        Increase the running count of sources created in this request
        """
        self.source_count += 1

    def propagated(self):
        """
        Increase the running count of propagations created in this request
        """
        self.propagation_count += 1

    def extract_response_to_context(self, response):
        """
        Append response to request context.

        :response: Subclass of BaseResponseWrapper
        """

        self.response = response

    def evaluate_exclusions(self):
        """
        Check if the request is excluded based on the URL or input exclusions.
        """
        if self.exclusions:
            path = self.request.path_info
            if self.exclusions.evaluate_url_exclusions(self, path):
                # Stop analyzing this endpoint since the URL exclusion applies
                return True

            self.exclusions.set_input_exclusions_by_url(self, path)

        return False
