"""AP011: Broken Access Control."""

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, _keyword_match


class AP011BrokenAccessControl(ProSecurityRule):
    """
    AP011: Broken Access Control.

    Detects access control issues including:
    - ID-based operations without authorization checks
    - Admin/privileged endpoints without role requirements
    - User resource endpoints without ownership validation
    - Tenant isolation issues
    """

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

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

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

    @property
    def description(self) -> str:
        return (
            "Detects broken access control patterns including missing authorization "
            "on ID-based operations, privilege escalation risks, and IDOR vulnerabilities "
            "(OWASP A01:2021)"
        )

    def evaluate(self, endpoint: Endpoint) -> Iterator[Finding]:
        """Evaluate endpoint for broken access control."""
        # 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 ID-based operations (potential IDOR)
        # Extended ID parameter patterns
        id_params = [
            "{id}", "<id>", "{user_id}", "<user_id>", "{pk}", "<pk>",
            "{uuid}", "<uuid>", "{slug}", "<slug>",
            "{record_id}", "<record_id>", "{item_id}", "<item_id>",
            "{order_id}", "<order_id>", "{account_id}", "<account_id>",
            "{profile_id}", "<profile_id>", "{payment_id}", "<payment_id>",
            "{invoice_id}", "<invoice_id>", "{document_id}", "<document_id>",
        ]
        has_id_param = any(param in route_lower for param in id_params)

        resource_keywords = [
            "user", "account", "profile", "order", "payment", "invoice",
            "document", "message", "transaction", "subscription",
        ]
        is_user_resource = any(keyword in route_lower for keyword in resource_keywords)

        if has_id_param and is_user_resource:
            has_permission_check = bool(
                endpoint.authorization.roles
                or endpoint.authorization.permissions
                or endpoint.authorization.policies
            )

            if endpoint.authorization.is_public:
                # Public endpoint with ID param accessing user resources = critical IDOR
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Public endpoint '{endpoint.full_route}' exposes user resources "
                        "by ID without any authentication"
                    ),
                    recommendation=(
                        "Require authentication for endpoints that access user-specific resources. "
                        "Add authentication decorators/dependencies before accessing resources by ID."
                    ),
                    severity=Severity.CRITICAL,
                )
            elif not has_permission_check and endpoint.authorization.requires_auth:
                # Authenticated but no ownership/permission check
                # Skip if has_specific_requirements — the framework detected some
                # form of authorization beyond just requires_auth (e.g., Depends())
                if not endpoint.authorization.has_specific_requirements:
                    severity = Severity.CRITICAL if endpoint.is_write_endpoint else Severity.HIGH
                    action = "modify" if endpoint.is_write_endpoint else "access"
                    yield self.create_finding(
                        endpoint,
                        message=(
                            f"ID-based endpoint '{endpoint.full_route}' may be vulnerable to IDOR. "
                            f"Authenticated users can {action} resources without ownership validation."
                        ),
                        recommendation=(
                            "Implement ownership validation. Verify that the authenticated user "
                            "owns or has permission to access the requested resource. "
                            "Check user.id == resource.owner_id before allowing access."
                        ),
                        severity=severity,
                    )

        # Check for admin/privileged endpoints without role requirements
        # "delete" only flagged in admin context (route contains /admin/ or /manage/)
        # Use segment-based check for /manage to avoid "file_manager" in route FP
        has_admin_route = (
            "/admin" in route_lower
            or route_lower.startswith("/manage")
            or "/manage/" in route_lower
        )

        # "manage" uses word-boundary matching to avoid "manager", "management" FPs
        admin_substring_keywords = ["admin", "ban", "suspend", "moderate"]
        is_admin_endpoint = any(
            keyword in route_lower or keyword in func_name for keyword in admin_substring_keywords
        ) or _keyword_match(route_lower, "manage") or _keyword_match(func_name, "manage")

        # Flag "delete"/"remove" only when in admin context
        delete_keywords = ["delete", "remove"]
        has_delete_keyword = any(
            keyword in route_lower or keyword in func_name for keyword in delete_keywords
        )
        if has_delete_keyword and has_admin_route:
            is_admin_endpoint = True

        if is_admin_endpoint:
            has_role_check = bool(endpoint.authorization.roles or endpoint.authorization.policies)

            if not has_role_check:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Administrative endpoint '{endpoint.full_route}' "
                        "lacks role-based access control"
                    ),
                    recommendation=(
                        "Implement role-based access control. Require specific roles "
                        "(e.g., @require_role('admin'), Depends(require_admin), "
                        "IsAdminUser permission class) for administrative operations."
                    ),
                    severity=Severity.CRITICAL,
                )

        # Check for bulk operations without authorization
        # Removed "list" (typically read-only) and "all" (too broad)
        bulk_keywords = ["bulk", "batch"]
        is_bulk_operation = any(keyword in func_name for keyword in bulk_keywords)

        if is_bulk_operation and endpoint.is_write_endpoint:
            if not endpoint.authorization.has_specific_requirements:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Bulk operation endpoint '{endpoint.full_route}' "
                        "lacks specific authorization requirements"
                    ),
                    recommendation=(
                        "Bulk operations should have strict authorization. "
                        "Require elevated permissions or implement per-item authorization checks."
                    ),
                    severity=Severity.HIGH,
                )

        # Check for direct object access patterns
        direct_access_keywords = [
            "by_id", "get_by", "find_by", "fetch_by", "lookup",
            "get_user", "get_account", "get_profile", "get_order",
        ]
        is_direct_access = any(keyword in func_name for keyword in direct_access_keywords)

        if is_direct_access and not endpoint.authorization.has_specific_requirements:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Direct object access endpoint '{endpoint.full_route}' "
                    "may expose unauthorized data"
                ),
                recommendation=(
                    "Implement authorization checks for direct object access. "
                    "Filter results by user ownership or permissions before returning data."
                ),
                severity=Severity.MEDIUM,
            )

        # Check for elevation/privilege endpoints
        elevation_keywords = ["promote", "elevate", "grant", "assign_role"]
        is_elevation_endpoint = any(keyword in func_name for keyword in elevation_keywords)

        if is_elevation_endpoint:
            if not endpoint.authorization.roles:
                yield self.create_finding(
                    endpoint,
                    message=(
                        f"Privilege elevation endpoint '{endpoint.full_route}' "
                        "lacks role-based protection"
                    ),
                    recommendation=(
                        "Privilege elevation operations must require admin/superuser roles. "
                        "Implement strict role checks to prevent privilege escalation attacks."
                    ),
                    severity=Severity.CRITICAL,
                )

        # Check for tenant isolation issues
        tenant_params = ["{tenant_id}", "<tenant_id>", "{org_id}", "<org_id>"]
        has_tenant_param = any(param in route_lower for param in tenant_params)

        if has_tenant_param and not endpoint.authorization.has_specific_requirements:
            yield self.create_finding(
                endpoint,
                message=(
                    f"Multi-tenant endpoint '{endpoint.full_route}' lacks scope validation. "
                    "Cross-tenant data access may be possible."
                ),
                recommendation=(
                    "Implement tenant isolation. Validate that the authenticated user "
                    "belongs to the requested tenant/organization. Use middleware or "
                    "dependencies to enforce tenant scoping on all queries."
                ),
                severity=Severity.HIGH,
            )
