import inspect
from itertools import chain
from typing import Any
from typing import Dict
from typing import Iterable
from typing import Optional

from ddtrace.appsec._constants import STACK_TRACE
from ddtrace.settings.asm import config as asm_config
from ddtrace.trace import Span


def report_stack(
    message: Optional[str] = None,
    span: Optional[Span] = None,
    crop_stack: Optional[str] = None,
    stack_id: Optional[str] = None,
    namespace: str = STACK_TRACE.RASP,
) -> bool:
    """
    Report a stack trace to the current span.
    This is used to report stack traces for exploit prevention.
    Return the stack id for the reported stack trace to link it in triggers.
    """
    if not asm_config._ep_stack_trace_enabled:
        # stack trace report disabled
        return False
    if namespace == STACK_TRACE.RASP and not (asm_config._asm_enabled and asm_config._ep_enabled):
        # exploit prevention stack trace with ep disabled
        return False
    if namespace == STACK_TRACE.IAST and not (asm_config._iast_enabled):
        # iast stack trace with iast disabled
        return False

    if span is None:
        from ddtrace.trace import tracer

        span = tracer.current_span()

    if span is None or stack_id is None:
        return False
    root_span = span._local_root or span
    appsec_traces = root_span.get_struct_tag(STACK_TRACE.TAG) or {}
    current_list = appsec_traces.get(namespace, [])
    total_length = len(current_list)

    # Do not report more than the maximum number of stack traces
    if asm_config._ep_max_stack_traces and total_length >= asm_config._ep_max_stack_traces:
        return False

    stack = inspect.stack()
    if crop_stack is not None:
        for i, frame in enumerate(stack):
            if stack[i].frame.f_code.co_name == crop_stack:
                stack = stack[i + 1 :]
                break
    res: Dict[str, Any] = {
        "language": "python",
        "id": stack_id,
    }
    if message is not None:
        res["message"] = message
    if len(stack) > asm_config._ep_max_stack_trace_depth > 0:
        top_stack = int(asm_config._ep_max_stack_trace_depth * asm_config._ep_stack_top_percent / 100)
        bottom_stack = asm_config._ep_max_stack_trace_depth - top_stack
        iterator: Iterable[int] = chain(range(top_stack), range(len(stack) - bottom_stack, len(stack)))
    else:
        iterator = range(len(stack))
    frames = [
        {
            "id": i,
            "function": getattr(stack[i].frame.f_code, "co_qualname", stack[i].frame.f_code.co_name),
            "file": stack[i].filename,
            "line": stack[i].lineno,
        }
        for i in iterator
    ]
    res["frames"] = frames
    current_list.append(res)
    appsec_traces[namespace] = current_list
    root_span.set_struct_tag(STACK_TRACE.TAG, appsec_traces)
    return True
