# -*- coding: utf-8 -*-
# Copyright © 2023 Contrast Security, Inc.
# See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
import sys
from contrast_vendor.wrapt import register_post_import_hook

import contrast
from contrast.agent import scope
from contrast.agent.policy.applicator import apply_module_patches
from contrast.agent.middlewares.route_coverage.common import build_route
from contrast.agent.middlewares.route_coverage.bottle_routes import create_bottle_routes
from contrast.agent.policy import patch_manager
from contrast.utils.patch_utils import build_and_apply_patch, wrap_and_watermark
from contrast.utils.decorators import fail_quietly

from contrast_vendor import structlog as logging

MODULE_NAME = "bottle"

logger = logging.getLogger("contrast")


def build_prepare_patch(orig_func, patch_policy, bottle_module):
    del patch_policy

    def prepare_patch(wrapped, instance, args, kwargs):
        """
        Patch for bottle.SimpleTemplate.prepare.

        This is needed because of the unfortunate way bottle calls on
        `html_escape` as a kwarg in the prepare definition in SimpleTemplate.
        See https://github.com/bottlepy/bottle/blob/master/bottle.py#L3952

        Because of this behavior, the `html_escape` func is not our patched
        `html_escape` defined in policy.
        By patching prepare, we intercept its call and instead of allowing
        it to use the default kwarg for `html_escape`, we pass our own
        patched `html_escape` in order to prevent false positive XSS findings.
        """
        del instance

        # html_escape MUST already be patched by policy in order
        # to pass in the patched func to prepare
        kwargs.setdefault("escape_func", bottle_module.html_escape)
        return wrapped(*args, **kwargs)

    return wrap_and_watermark(orig_func, prepare_patch)


def build_call_patch(orig_func, patch_policy):
    """
    Patch for bottle.Bottle.__call__

    This is how we perform route discovery for Bottle. Many different patches could
    probably work here, since we really just need a reference to the Bottle instance
    after all routes have been registered.
    """
    del patch_policy

    def call_patch(wrapped, instance, args, kwargs):
        result = wrapped(*args, **kwargs)
        do_bottle_route_discovery(instance)
        return result

    return wrap_and_watermark(orig_func, call_patch)


@fail_quietly("unable to perform Bottle route discovery")
def do_bottle_route_discovery(bottle_instance):
    with scope.contrast_scope():
        from contrast.agent import agent_state

        if not agent_state.is_first_request():
            return

        discovered_routes = create_bottle_routes(bottle_instance)
        agent_state.update_routes(discovered_routes)
        logger.debug(
            "Discovered the following Bottle routes: %s",
            [f"{route.verb} {route.url}" for route in discovered_routes.values()],
        )


def build_match_patch(orig_func, patch_policy):
    """
    Patch for bottle.Router.match()

    This sets up request context with all necessary info for current route observation
    """
    del patch_policy

    def match_patch(wrapped, instance, args, kwargs):
        del instance

        result = wrapped(*args, **kwargs)
        do_bottle_route_observation(result)
        return result

    return wrap_and_watermark(orig_func, match_patch)


@fail_quietly("unable to perform bottle route coverage in Bottle match patch")
def do_bottle_route_observation(match_result):
    with scope.contrast_scope():
        if match_result is None:
            return

        context = contrast.CS__CONTEXT_TRACKER.current()
        if context is None:
            return

        context.view_func = match_result[0].callback
        context.view_func_str = build_route(
            context.request.get_normalized_uri(),
            context.view_func,
        )
        logger.debug("Observed Bottle route: %s", context.view_func_str)


def patch_bottle(bottle_module):
    # We ask policy to go ahead and do all bottle patches here (even though policy
    # patches will happen later on) because we MUST have some bottle policy patches
    # already applied for these non-policy patches to work.
    # This would not be necessary if in _enable_patches policy_patches were applied
    # first.
    apply_module_patches(bottle_module)

    build_and_apply_patch(
        bottle_module.SimpleTemplate,
        "prepare",
        build_prepare_patch,
        builder_args=(bottle_module,),
    )
    build_and_apply_patch(bottle_module.Bottle, "__call__", build_call_patch)
    build_and_apply_patch(bottle_module.Router, "match", build_match_patch)


def register_patches():
    register_post_import_hook(patch_bottle, MODULE_NAME)


def reverse_patches():
    bottle_module = sys.modules.get(MODULE_NAME)
    if not bottle_module:
        return

    patch_manager.reverse_patches_by_owner(bottle_module)
    patch_manager.reverse_patches_by_owner(bottle_module.SimpleTemplate)
    patch_manager.reverse_patches_by_owner(bottle_module.Bottle)
    patch_manager.reverse_patches_by_owner(bottle_module.Router)
