"""AP015: API Key Exposure 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, _keyword_match


class AP015APIKeyExposure(ProSecurityRule):
    """
    AP015: API Key Exposure Detection.

    Detects API key exposure issues:
    - API keys in URL query parameters (GET requests)
    - Authentication tokens in URL paths
    - Credentials passed via insecure methods
    """

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

    @property
    def name(self) -> str:
        return "API Key Exposure"

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

    @property
    def description(self) -> str:
        return (
            "Detects API key and credential exposure through insecure methods "
            "like URL parameters and GET requests"
        )

    def evaluate(self, endpoint: Endpoint) -> Iterator[Finding]:
        """Check for API key exposure in endpoint design."""
        # 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.lower()
        func_name = endpoint.function_name.lower()

        # Check for API key/token in route parameters
        key_params = [
            "api_key",
            "apikey",
            "api-key",
            "token",
            "access_token",
            "auth_token",
            "key",
            "secret",
            "password",
            "credential",
        ]

        has_key_param = any(
            f"{{{param}}}" in route or f"<{param}>" in route for param in key_params
        )

        # Also check for query parameter patterns in the route definition
        query_key_patterns = [
            "?api_key=", "?apikey=", "?api-key=",
            "?token=", "?access_token=", "?auth_token=",
            "&api_key=", "&apikey=", "&api-key=",
            "&token=", "&access_token=", "&auth_token=",
        ]
        has_query_key = any(pattern in route for pattern in query_key_patterns)

        if has_query_key:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Endpoint '{endpoint.full_route}' passes API key or token as query parameter"
                ),
                recommendation=(
                    "Never pass API keys or tokens in URL query parameters. "
                    "Use HTTP headers instead: Authorization: Bearer <token> or X-API-Key: <key>. "
                    "Query parameters are logged in web server logs, browser history, "
                    "proxy logs, and can be exposed in Referer headers."
                ),
                severity=Severity.CRITICAL,
            )

        if has_key_param:
            # Exempt routes where tokens in URLs are industry standard practice
            # (password reset, email verification, account activation, confirmation links)
            token_exempt_route_keywords = [
                "reset", "forgot", "verify", "confirm", "activate",
                "unsubscribe", "invitation", "invite",
            ]
            is_token_exempt = any(
                kw in route or kw in func_name for kw in token_exempt_route_keywords
            )

            if not is_token_exempt:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Endpoint '{endpoint.full_route}' accepts API key or token as URL parameter"
                    ),
                    recommendation=(
                        "Never pass API keys or tokens in URL parameters. Use HTTP headers instead: "
                        "Authorization: Bearer <token> or X-API-Key: <key>. "
                        "URL parameters are logged in web server logs, browser history, and "
                        "can be exposed in Referer headers."
                    ),
                    severity=Severity.CRITICAL,
                )

        # Check for authentication via GET method
        # Only match auth-PROVIDER routes, not auth-consumer routes
        # (e.g., /login, /signin — NOT /authenticated-route, /auth-required)
        auth_provider_route_segments = [
            "/login", "/signin", "/sign-in", "/sign_in",
            "/authenticate",
        ]
        auth_provider_func_exact = ["login", "signin", "sign_in", "authenticate"]
        # Exclude auth-consumer patterns (endpoints that REQUIRE auth, not provide it)
        auth_consumer_patterns = ["authenticated", "auth_required", "require_auth"]

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

        is_auth_provider = (not is_consumer) and (
            any(
                func_name == kw or func_name.startswith(kw + "_")
                or func_name.endswith("_" + kw)
                for kw in auth_provider_func_exact
            ) or any(
                route.endswith(seg) or (seg + "/") in route
                for seg in auth_provider_route_segments
            )
        )

        # Exclude OAuth2 authorize (spec requires GET) and token verification
        oauth_exempt = ["authorize", "callback", "test_token", "test-token", "verify"]
        is_oauth = any(kw in route or kw in func_name for kw in oauth_exempt)

        # Also check for credential params in route (e.g., /<passwd>)
        credential_route_params = [
            "{password}", "<password>", "{passwd}", "<passwd>",
            "{secret}", "<secret>", "{credential}", "<credential>",
        ]
        has_credential_param = any(param in route for param in credential_route_params)

        if (is_auth_provider or has_credential_param) and not is_oauth and HttpMethod.GET in endpoint.methods:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Authentication endpoint '{endpoint.full_route}' accepts GET requests"
                ),
                recommendation=(
                    "Authentication endpoints should only accept POST requests. "
                    "GET requests expose credentials in URLs, which are logged and cached. "
                    "Use POST with credentials in the request body."
                ),
                severity=Severity.HIGH,
            )

        # Check for password/secret in function name with GET
        # "key" uses word-boundary matching to avoid "monkey", "keyboard", "turkey" FPs
        secret_substring_keywords = ["password", "secret", "credential"]
        handles_secrets = (
            any(keyword in func_name for keyword in secret_substring_keywords)
            or _keyword_match(func_name, "key")
        )

        if handles_secrets and HttpMethod.GET in endpoint.methods:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Endpoint '{endpoint.full_route}' handles secrets/credentials via GET"
                ),
                recommendation=(
                    "Use POST or PUT methods for operations involving secrets. "
                    "GET request data is visible in URLs and logs. "
                    "Sensitive data should be sent in request body with proper encryption."
                ),
                severity=Severity.HIGH,
            )

        # Check for API key validation endpoints
        validate_keywords = ["validate", "verify", "check"]
        validates_key = any(keyword in func_name for keyword in validate_keywords)

        if validates_key and any(keyword in func_name for keyword in ["key", "token", "credential"]):
            if endpoint.authorization.is_public:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"API key validation endpoint '{endpoint.full_route}' is publicly accessible"
                    ),
                    recommendation=(
                        "API key validation endpoints should require authentication. "
                        "Public validation endpoints can be abused for key enumeration attacks."
                    ),
                    severity=Severity.MEDIUM,
                )

        # Check for callback URLs with tokens
        if "callback" in route or "callback" in func_name:
            if has_key_param or any(word in route for word in ["token", "key", "code"]):
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"OAuth/callback endpoint '{endpoint.full_route}' "
                        "may expose tokens in URL"
                    ),
                    recommendation=(
                        "For OAuth callbacks, use authorization code flow instead of implicit flow. "
                        "Validate redirect URIs and use short-lived codes. "
                        "Consider using PKCE for additional security."
                    ),
                    severity=Severity.MEDIUM,
                )
