"""IDOR-001: Insecure Direct Object Reference detection rule.

Detects patterns where user-supplied IDs are used directly to access objects
without authorization checks, enabling unauthorized access to resources.

NOTE: This module contains string references to unsafe patterns because it is
a SECURITY SCANNER that detects these patterns in target code. These are
detection signatures, not actual usage.
"""

from __future__ import annotations

import ast

from . import Rule
from ..models import Finding, Severity

# File access functions that take a path argument
FILE_ACCESS_FUNCS = {"open", "Path.read_text", "Path.write_text", "Path.read_bytes"}

# Database query patterns that may use user-controlled IDs
DB_QUERY_METHODS = {
    "execute", "find_one", "find", "get", "filter",
    "get_object_or_404", "objects.get", "objects.filter",
}

# Tokens indicating authorization/validation is present
AUTH_TOKENS = {
    "authorize", "authorise", "auth_check", "check_auth",
    "check_permission", "has_permission", "is_authorized",
    "is_allowed", "verify_access", "validate_access",
    "permission", "rbac", "acl", "check_owner",
    "verify_owner", "belongs_to",
}


class IDORRule(Rule):
    """Detects insecure direct object references.

    Flags patterns where user-supplied IDs or paths from function
    parameters (e.g. args[...], request data) are used directly to
    access files or database records without authorization checks.
    """

    @property
    def rule_id(self) -> str:
        return "IDOR-001"

    @property
    def name(self) -> str:
        return "Insecure Direct Object Reference"

    @property
    def severity(self) -> Severity:
        return Severity.HIGH

    @property
    def cwe(self) -> str:
        return "CWE-639"

    @property
    def description(self) -> str:
        return (
            "User-supplied ID or path used to access objects "
            "without authorization checks."
        )

    @property
    def detailed_description(self) -> str:
        return (
            "User-supplied ID or path used to access objects "
            "without authorization checks.\n\n"
            "When user-controlled identifiers (IDs, filenames, paths) are\n"
            "passed directly to file operations or database queries without\n"
            "verifying that the requesting user is authorized to access that\n"
            "resource, attackers can access or modify other users' data.\n\n"
            "How to fix:\n"
            "  - Add authorization checks before accessing resources\n"
            "  - Validate that the user owns or has access to the resource\n"
            "  - Use indirect references (mapping tables) instead of direct IDs"
        )

    def check(self, tree: ast.Module, source: str) -> list[Finding]:
        """Check for IDOR patterns in functions."""
        findings: list[Finding] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            if self._has_auth_check(node, source):
                continue
            findings.extend(self._check_file_access(node, source))
            findings.extend(self._check_db_queries(node, source))
        return findings

    def _check_file_access(
        self,
        func_node: ast.FunctionDef | ast.AsyncFunctionDef,
        source: str,
    ) -> list[Finding]:
        """Detect file access with user-controlled paths."""
        findings: list[Finding] = []
        param_names = self._get_param_names(func_node)
        if not param_names:
            return findings

        for child in ast.walk(func_node):
            if not isinstance(child, ast.Call):
                continue
            func_name = self._get_func_name(child)
            is_open = isinstance(child.func, ast.Name) and child.func.id == "open"
            is_path_op = func_name in FILE_ACCESS_FUNCS

            if not (is_open or is_path_op):
                continue
            if not self._call_uses_user_input(child, param_names):
                continue

            findings.append(Finding(
                rule_id=self.rule_id,
                severity=self.severity,
                title="IDOR: file access with user-controlled path",
                description=(
                    f"Function '{func_node.name}' uses user input "
                    f"in {func_name or 'open'}() without authorization."
                ),
                file="",
                line=child.lineno,
                code_snippet=self._get_snippet(source, child.lineno),
                recommendation=(
                    "Add authorization checks before file access. "
                    "Validate paths against an allowlist."
                ),
                cwe=self.cwe,
                confidence="HIGH",
            ))
        return findings

    def _check_db_queries(
        self,
        func_node: ast.FunctionDef | ast.AsyncFunctionDef,
        source: str,
    ) -> list[Finding]:
        """Detect database queries with user-controlled IDs."""
        findings: list[Finding] = []
        param_names = self._get_param_names(func_node)
        if not param_names:
            return findings

        for child in ast.walk(func_node):
            if not isinstance(child, ast.Call):
                continue
            func_name = self._get_func_name(child)
            method_name = func_name.split(".")[-1] if "." in func_name else func_name
            if method_name not in DB_QUERY_METHODS:
                continue
            if not self._call_uses_user_input(child, param_names):
                continue

            findings.append(Finding(
                rule_id=self.rule_id,
                severity=self.severity,
                title="IDOR: database query with user-controlled ID",
                description=(
                    f"Function '{func_node.name}' passes user input "
                    f"to {func_name}() without authorization."
                ),
                file="",
                line=child.lineno,
                code_snippet=self._get_snippet(source, child.lineno),
                recommendation=(
                    "Add authorization checks before database queries. "
                    "Verify the user owns or can access the resource."
                ),
                cwe=self.cwe,
                confidence="MEDIUM",
            ))
        return findings

    def _get_param_names(
        self, func_node: ast.FunctionDef | ast.AsyncFunctionDef
    ) -> set[str]:
        """Get parameter names, excluding 'self' and 'cls'."""
        return {
            arg.arg for arg in func_node.args.args
            if arg.arg not in ("self", "cls")
        }

    def _call_uses_user_input(
        self, call_node: ast.Call, param_names: set[str]
    ) -> bool:
        """Check if a call uses function parameters or subscript access."""
        for arg in call_node.args:
            if self._is_user_input(arg, param_names):
                return True
        for kw in call_node.keywords:
            if kw.value and self._is_user_input(kw.value, param_names):
                return True
        return False

    def _is_user_input(self, node: ast.AST, param_names: set[str]) -> bool:
        """Check if node represents user-controlled input."""
        # Direct parameter reference: open(path)
        if isinstance(node, ast.Name) and node.id in param_names:
            return True
        # Subscript on parameter: open(args["path"])
        if isinstance(node, ast.Subscript):
            if isinstance(node.value, ast.Name) and node.value.id in param_names:
                return True
        # Recursive check in sub-expressions
        for child in ast.walk(node):
            if child is node:
                continue
            if isinstance(child, ast.Name) and child.id in param_names:
                return True
            if isinstance(child, ast.Subscript):
                if isinstance(child.value, ast.Name) and child.value.id in param_names:
                    return True
        return False

    def _has_auth_check(
        self,
        node: ast.FunctionDef | ast.AsyncFunctionDef,
        source: str,
    ) -> bool:
        """Check if function body contains authorization logic."""
        body_src = ast.get_source_segment(source, node) or ""
        body_lower = body_src.lower()
        return any(token in body_lower for token in AUTH_TOKENS)
