from slither.core.cfg.node import NodeType, Node
from slither.detectors.abstract_detector import (
    AbstractDetector,
    DetectorClassification,
    DETECTOR_INFO,
)
from slither.slithir.operations import InternalCall
from slither.core.declarations import SolidityVariableComposed, Contract
from slither.utils.output import Output
from slither.slithir.variables.constant import Constant
from slither.core.variables import Variable
from slither.core.expressions.literal import Literal

Result = list[tuple[Node, list[str]]]


def detect_msg_value_in_loop(contract: Contract) -> Result:
    results: Result = []
    for f in contract.functions_entry_points:
        if f.is_implemented and f.payable:
            msg_value_in_loop(f.entry_point, 0, [], [], results)
    return results


def msg_value_in_loop(
    node: Node | None,
    in_loop_counter: int,
    visited: list[Node],
    calls_stack: list[str],
    results: Result,
) -> None:
    if node is None:
        return

    if node in visited:
        return
    # shared visited
    visited.append(node)

    if node.type == NodeType.STARTLOOP:
        in_loop_counter += 1
    elif node.type == NodeType.ENDLOOP:
        in_loop_counter -= 1

    for ir in node.irs:
        if in_loop_counter > 0 and SolidityVariableComposed("msg.value") in ir.read:
            # If we find a conditional expression with msg.value and is compared to 0 we don't report it
            if ir.node.is_conditional() and SolidityVariableComposed("msg.value") in ir.read:
                compared_to = (
                    ir.read[1]
                    if ir.read[0] == SolidityVariableComposed("msg.value")
                    else ir.read[0]
                )
                if (isinstance(compared_to, Constant) and compared_to.value == 0) or (
                    isinstance(compared_to, Variable)
                    and isinstance(compared_to.expression, Literal)
                    and str(compared_to.expression.value) == "0"
                ):
                    continue
            results.append((ir.node, calls_stack.copy()))
        if isinstance(ir, (InternalCall)):
            calls_stack.append(node.function.canonical_name)
            msg_value_in_loop(
                ir.function.entry_point, in_loop_counter, visited, calls_stack, results
            )
            calls_stack.pop()

    for son in node.sons:
        msg_value_in_loop(son, in_loop_counter, visited, calls_stack, results)


class MsgValueInLoop(AbstractDetector):
    """
    Detect the use of msg.value inside a loop
    """

    ARGUMENT = "msg-value-loop"
    HELP = "msg.value inside a loop"
    IMPACT = DetectorClassification.HIGH
    CONFIDENCE = DetectorClassification.MEDIUM

    WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation/#msgvalue-inside-a-loop"

    WIKI_TITLE = "`msg.value` inside a loop"
    WIKI_DESCRIPTION = "Detect the use of `msg.value` inside a loop."

    # region wiki_exploit_scenario
    WIKI_EXPLOIT_SCENARIO = """
```solidity
contract MsgValueInLoop{

    mapping (address => uint256) balances;

    function bad(address[] memory receivers) public payable {
        for (uint256 i=0; i < receivers.length; i++) {
            balances[receivers[i]] += msg.value;
        }
    }

}
```
"""
    # endregion wiki_exploit_scenario

    WIKI_RECOMMENDATION = """
Provide an explicit array of amounts alongside the receivers array, and check that the sum of all amounts matches `msg.value`.
"""

    def _detect(self) -> list[Output]:
        """"""
        results: list[Output] = []
        for c in self.compilation_unit.contracts_derived:
            values = detect_msg_value_in_loop(c)
            for node, calls_stack in values:
                func = node.function

                info: DETECTOR_INFO = [func, " use msg.value in a loop: ", node, "\n"]

                if len(calls_stack) > 0:
                    info.append("\tCalls stack containing the loop:\n")
                    for call in calls_stack:
                        info.extend(["\t\t", call, "\n"])

                res = self.generate_result(info)
                results.append(res)

        return results
