"""AP009: Broken Authentication Detection."""

from collections.abc import Iterator

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

from apiposture_pro.rules.base import ProSecurityRule


class AP009BrokenAuthentication(ProSecurityRule):
    """
    AP009: Broken Authentication Detection.

    Detects authentication-related security issues including:
    - Write endpoints without authentication
    - Authentication endpoints with weak configuration
    - Endpoints handling credentials without proper protection
    - Missing rate limiting on auth endpoints
    - Password change without re-authentication
    - Registration spam risks
    """

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

    @property
    def name(self) -> str:
        return "Broken Authentication"

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

    @property
    def description(self) -> str:
        return (
            "Detects broken authentication patterns including write operations "
            "without authentication, weak authentication configuration, and "
            "credential handling vulnerabilities (OWASP A07:2021)"
        )

    def evaluate(self, endpoint: Endpoint) -> Iterator[Finding]:
        """Evaluate endpoint for broken authentication issues."""
        # Skip test/example/docs files — these are 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

        # Check for write operations without authentication
        # Exclude endpoints that are legitimately public (login, register, password reset, etc.)
        route_lower = endpoint.route.lower()
        func_lower = endpoint.function_name.lower()
        public_write_exempt = [
            "login", "signin", "sign_in", "sign-in",
            "register", "signup", "sign_up", "sign-up",
            "reset-password", "reset_password", "forgot-password", "forgot_password",
            "password-recovery", "password_recovery",
            "request-verify", "request_verify", "verify",
            "token", "authorize", "callback", "webhook", "health",
        ]
        is_exempt_public_write = any(
            kw in route_lower or kw in func_lower for kw in public_write_exempt
        )

        if endpoint.is_write_endpoint and endpoint.authorization.is_public and not is_exempt_public_write:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Write endpoint '{endpoint.full_route}' accepts {endpoint.display_methods} "
                    "without authentication"
                ),
                recommendation=(
                    "Require authentication for write operations. "
                    "Add authentication decorators/dependencies (e.g., Depends(get_current_user), "
                    "@login_required, IsAuthenticated permission class)"
                ),
            )

        # Check for authentication endpoints with suspicious patterns
        # Only match auth-PROVIDER routes (login, signin, token issuance),
        # not auth-CONSUMER routes (e.g., /authenticated-route, /auth-required)
        # Use exact function name matching to avoid "authenticated_route" matching "authenticate"
        auth_provider_func_exact = [
            "login", "signin", "sign_in", "authenticate",
            "register", "signup", "sign_up",
        ]
        # Exclude functions with suffixed forms like "authenticated_*"
        auth_consumer_patterns = ["authenticated", "auth_required", "require_auth"]

        # These are exact route-segment keywords (not substrings of other words)
        auth_route_segments = [
            "/login", "/signin", "/sign-in", "/sign_in",
            "/authenticate", "/register", "/signup", "/sign-up", "/sign_up",
            "/token", "/session", "/verify", "/confirm",
        ]

        is_consumer = any(pat in func_lower for pat in auth_consumer_patterns)

        is_auth_endpoint = (not is_consumer) and (
            any(
                func_lower == keyword or func_lower.startswith(keyword + "_")
                or func_lower.endswith("_" + keyword)
                for keyword in auth_provider_func_exact
            ) or any(
                route_lower.endswith(seg) or (seg + "/") in route_lower
                for seg in auth_route_segments
            )
        )

        if is_auth_endpoint:
            # Authentication endpoint should be POST, not GET
            # Exclude OAuth2 authorize endpoint (spec requires GET) and
            # read-only verification/token-test endpoints
            oauth_exempt = ["authorize", "callback", "test_token", "test-token"]
            is_oauth_or_verify = any(
                kw in route_lower or kw in func_lower for kw in oauth_exempt
            )

            if HttpMethod.GET in endpoint.methods and not is_oauth_or_verify:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Authentication endpoint '{endpoint.full_route}' accepts GET requests"
                    ),
                    recommendation=(
                        "Authentication endpoints should only accept POST requests. "
                        "Credentials should never be sent via GET parameters."
                    ),
                    severity=Severity.HIGH,
                )

            # Check for credential-related endpoints that are public
            credential_keywords = ["password", "credential", "secret"]
            is_credential_endpoint = any(
                keyword in route_lower or keyword in func_lower
                for keyword in credential_keywords
            )

            if is_credential_endpoint and endpoint.authorization.is_public:
                # Password reset endpoints should be public, but others should not
                if "reset" not in route_lower and "forgot" not in route_lower:
                    yield self.create_finding(
                        endpoint,
                        message=(
                            f"Credential-handling endpoint '{endpoint.full_route}' "
                            "is publicly accessible"
                        ),
                        recommendation=(
                            "Credential-handling endpoints should require authentication "
                            "unless specifically designed for password reset flows."
                        ),
                        severity=Severity.HIGH,
                    )

        # Check for rate limiting on login/signin endpoints
        login_func_exact = ["login", "signin", "sign_in", "authenticate"]
        login_route_segments = ["/login", "/signin", "/sign-in", "/authenticate"]
        is_login_endpoint = (not is_consumer) and (
            any(
                func_lower == keyword or func_lower.startswith(keyword + "_")
                or func_lower.endswith("_" + keyword)
                for keyword in login_func_exact
            ) or any(
                route_lower.endswith(seg) or (seg + "/") in route_lower
                for seg in login_route_segments
            )
        )

        # Exclude token verification/test endpoints from the login rate-limit check
        token_verify_patterns = ["test_token", "test-token", "verify_token", "verify-token"]
        is_token_verify = any(pat in route_lower or pat in func_lower for pat in token_verify_patterns)

        if is_login_endpoint and not is_token_verify:
            # Heuristic: check if function name or decorators hint at rate limiting
            rate_limit_hints = ["rate_limit", "ratelimit", "throttle", "limiter", "slowdown"]
            has_rate_limit_hint = any(hint in func_lower for hint in rate_limit_hints)

            if not has_rate_limit_hint:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Login endpoint '{endpoint.full_route}' has no indication of rate limiting"
                    ),
                    recommendation=(
                        "Apply rate limiting to authentication endpoints to prevent brute-force attacks. "
                        "Use tools like slowapi, flask-limiter, or django-ratelimit. "
                        "Consider implementing account lockout after N failed attempts."
                    ),
                    severity=Severity.MEDIUM,
                )

        # Check for password change without re-authentication
        # Exclude password reset flows (reset-password uses a token, not current auth)
        password_change_keywords = ["change_password", "update_password", "set_password"]
        is_password_change = any(keyword in func_lower for keyword in password_change_keywords)
        # Endpoints with "reset" or "recovery" in the route are token-based reset flows
        is_reset_flow = any(
            kw in route_lower for kw in ["reset", "recovery", "forgot"]
        )

        if is_password_change and endpoint.authorization.is_public and not is_reset_flow:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Password change endpoint '{endpoint.full_route}' is publicly accessible"
                ),
                recommendation=(
                    "Password change endpoints must require authentication. "
                    "Require the user to provide their current password before allowing changes. "
                    "Consider requiring re-authentication for sensitive operations."
                ),
                severity=Severity.HIGH,
            )

        # Check for public registration endpoints without rate limiting
        registration_keywords = ["register", "signup", "sign_up", "create_account"]
        is_registration = any(keyword in func_lower for keyword in registration_keywords)

        if is_registration and endpoint.authorization.is_public:
            rate_limit_hints = ["rate_limit", "ratelimit", "throttle", "limiter", "captcha"]
            has_rate_limit_hint = any(hint in func_lower for hint in rate_limit_hints)

            if not has_rate_limit_hint:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Registration endpoint '{endpoint.full_route}' is public without "
                        "rate limiting indication"
                    ),
                    recommendation=(
                        "Apply rate limiting and CAPTCHA to registration endpoints to prevent "
                        "automated account creation. Use tools like slowapi or flask-limiter."
                    ),
                    severity=Severity.MEDIUM,
                )

        # Check for session/token management without auth
        session_keywords = ["session", "logout", "signout", "revoke"]
        is_session_endpoint = any(
            keyword in route_lower or keyword in func_lower for keyword in session_keywords
        )

        if is_session_endpoint and endpoint.authorization.is_public:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Session management endpoint '{endpoint.full_route}' "
                    "is publicly accessible"
                ),
                recommendation=(
                    "Session management endpoints (logout, token revocation) "
                    "should require authentication to prevent session fixation attacks."
                ),
                severity=Severity.MEDIUM,
            )
