"""AP010: Injection Vector Detection."""

from collections.abc import Iterator

from apiposture.core.models.endpoint import Endpoint
from apiposture.core.models.enums import Severity
from apiposture.core.models.finding import Finding

from apiposture_pro.rules.base import ProSecurityRule, _any_keyword_match, _keyword_match


class AP010InjectionVectors(ProSecurityRule):
    """
    AP010: Injection Vector Detection.

    Detects potential injection vulnerabilities including:
    - Path parameters that could enable path traversal
    - Query/file operation endpoints without validation markers
    - Database query endpoints without ORM protection markers
    - SSRF via URL parameters
    - Header injection and log injection
    """

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

    @property
    def name(self) -> str:
        return "Injection Vector Detection"

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

    @property
    def description(self) -> str:
        return (
            "Detects potential injection vulnerabilities including SQL injection, "
            "command injection, SSRF, and path traversal risks (OWASP A03:2021)"
        )

    def evaluate(self, endpoint: Endpoint) -> Iterator[Finding]:
        """Evaluate endpoint for injection vulnerabilities."""
        # Skip test/example/docs files — not production endpoints
        file_parts = endpoint.file_path.parts
        if any(
            part in ("tests", "test", "examples", "example", "docs", "testing")
            or part.startswith("test_")
            for part in file_parts
        ):
            return

        route = endpoint.route
        func_name = endpoint.function_name.lower()
        route_lower = route.lower()

        # Check for file path parameters (potential path traversal)
        # "file" and "path" use word-boundary matching to avoid "profile", catch-all routes, etc.
        file_boundary_keywords = ["file", "path"]
        file_substring_keywords = ["filename", "filepath", "document", "attachment"]
        has_path_param = "{" in route or "<" in route

        has_file_keyword = (
            _any_keyword_match(route_lower, file_boundary_keywords)
            or any(keyword in route_lower for keyword in file_substring_keywords)
        )

        if has_path_param and has_file_keyword:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Endpoint '{endpoint.full_route}' accepts file/path parameters "
                    "which may be vulnerable to path traversal"
                ),
                recommendation=(
                    "Validate and sanitize file paths. Use allowlists for allowed directories, "
                    "reject '..' and absolute paths, and use pathlib.resolve() to normalize paths."
                ),
                severity=Severity.HIGH,
            )

        # Check for query/search endpoints (potential SQL injection)
        # Reduce false positives: skip if function name indicates ORM usage or Django framework
        # Use word-boundary matching to avoid "jquery" matching "query", etc.
        query_keywords = ["query", "search", "filter", "sql", "where"]
        is_query_endpoint = any(
            _keyword_match(func_name, keyword) or _keyword_match(route_lower, keyword)
            for keyword in query_keywords
        )

        if is_query_endpoint:
            # Don't flag if function name contains ORM hint
            orm_hints = ["orm", "queryset", "model_"]
            is_orm_usage = any(hint in func_name for hint in orm_hints)

            if not is_orm_usage:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Query endpoint '{endpoint.full_route}' may be vulnerable to injection"
                    ),
                    recommendation=(
                        "Use parameterized queries or ORM methods. Never construct SQL from "
                        "user input using string concatenation or f-strings. "
                        "For Django: use ORM querysets. For SQLAlchemy: use bound parameters. "
                        "For raw SQL: use cursor.execute(sql, params)."
                    ),
                    severity=Severity.HIGH,
                )

        # Check for command execution patterns
        # "process" removed — too generic (matches task processing, payment processing, etc.)
        command_keywords = ["exec", "execute", "cmd", "command", "shell"]
        is_command_endpoint = any(keyword in func_name for keyword in command_keywords)

        if is_command_endpoint and endpoint.is_write_endpoint:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Command execution endpoint '{endpoint.full_route}' "
                    "may be vulnerable to command injection"
                ),
                recommendation=(
                    "Avoid executing system commands from user input. If required, "
                    "use subprocess.run() with a list of arguments (not shell=True), "
                    "validate inputs against a strict allowlist, and run with minimal privileges."
                ),
                severity=Severity.CRITICAL,
            )

        # Check for template/code endpoints (potential template injection)
        # Reduce false positives: skip safe patterns like render_template (Flask) and TemplateResponse
        template_keywords = ["template", "render", "eval", "compile"]
        is_template_endpoint = any(keyword in func_name for keyword in template_keywords)

        if is_template_endpoint:
            # Safe patterns: render_template (Flask), template_response (FastAPI/Starlette),
            # return_template, missing_template, get_template, load_template, template_stream
            safe_template_patterns = [
                "render_template", "template_response", "templateresponse",
                "return_template", "missing_template", "get_template",
                "load_template", "template_stream",
            ]
            is_safe_pattern = any(safe in func_name for safe in safe_template_patterns)

            if not is_safe_pattern:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Template/code endpoint '{endpoint.full_route}' "
                        "may be vulnerable to template injection"
                    ),
                    recommendation=(
                        "Never use eval(), exec(), or compile() with user input. "
                        "Use sandboxed template engines and disable unsafe template features. "
                        "For Jinja2: use autoescaping and disable unsafe extensions."
                    ),
                    severity=Severity.CRITICAL,
                )

        # Check for redirect/URL parameters (open redirect)
        # Reduced keyword set — removed overly generic "return" and "next"
        redirect_keywords = ["redirect", "callback", "continue"]
        has_redirect_pattern = any(keyword in func_name for keyword in redirect_keywords)

        if has_redirect_pattern and has_path_param:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Redirect endpoint '{endpoint.full_route}' accepts URL parameters "
                    "which may enable open redirect attacks"
                ),
                recommendation=(
                    "Validate redirect URLs against an allowlist of trusted domains. "
                    "Never redirect to arbitrary user-provided URLs. "
                    "Use relative URLs or validate against a list of allowed destinations."
                ),
                severity=Severity.MEDIUM,
            )

        # SSRF detection: endpoints accepting URL/URI parameters
        ssrf_params = ["{url}", "{uri}", "{link}", "{webhook}", "{callback_url}",
                       "<url>", "<uri>", "<link>", "<webhook>", "<callback_url>"]
        has_ssrf_param = any(param in route_lower for param in ssrf_params)

        if has_ssrf_param:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Endpoint '{endpoint.full_route}' accepts URL parameters "
                    "which may be vulnerable to SSRF attacks"
                ),
                recommendation=(
                    "Validate and sanitize URL parameters to prevent SSRF. "
                    "Use allowlists for permitted domains/IPs. "
                    "Block requests to internal/private IP ranges (127.0.0.0/8, 10.0.0.0/8, "
                    "172.16.0.0/12, 192.168.0.0/16). Consider using a URL validation library."
                ),
                severity=Severity.HIGH,
            )

        # Header injection: endpoints with header-related keywords + write operations
        header_keywords = ["header", "set_header", "add_header"]
        is_header_endpoint = any(keyword in func_name for keyword in header_keywords)

        if is_header_endpoint and endpoint.is_write_endpoint:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Header manipulation endpoint '{endpoint.full_route}' "
                    "may be vulnerable to header injection"
                ),
                recommendation=(
                    "Validate and sanitize header values. Reject values containing "
                    "newline characters (\\r\\n) to prevent HTTP header injection. "
                    "Use framework-provided header setting methods."
                ),
                severity=Severity.MEDIUM,
            )

        # Log injection: endpoints with "log" in name that accept user input
        # "log" uses word-boundary matching to avoid "catalog", "blog", "dialog" FPs
        log_boundary_keywords = ["log"]
        log_substring_keywords = ["audit", "event_log"]
        is_log_endpoint = (
            _any_keyword_match(func_name, log_boundary_keywords)
            or any(keyword in func_name for keyword in log_substring_keywords)
        )

        if is_log_endpoint and has_path_param:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Logging endpoint '{endpoint.full_route}' accepts user input "
                    "which may enable log injection"
                ),
                recommendation=(
                    "Sanitize user input before logging. Remove or encode newline characters "
                    "and other control characters. Use structured logging formats (JSON) "
                    "to prevent log forging attacks."
                ),
                severity=Severity.MEDIUM,
            )
