"""
Security middleware for webhook authentication and rate limiting.
"""

import hashlib
import hmac
import logging
import time
from typing import Optional, List, Callable
from datetime import datetime
import ipaddress

from fastapi import Request, Response, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
import redis.asyncio as redis
from sqlalchemy import select

from fmatch.saas.database import session_context
from fmatch.saas.model_defs.webhook_models import WebhookApiKey
from fmatch.saas import settings

logger = logging.getLogger(__name__)


class WebhookAuthMiddleware(BaseHTTPMiddleware):
    """
    Middleware for webhook authentication, rate limiting, and security.
    """

    def __init__(
        self,
        app: ASGIApp,
        redis_url: Optional[str] = None,
        rate_limit_enabled: bool = True,
        signature_verification_enabled: bool = True,
        ip_allowlist_enabled: bool = False,
        global_rate_limit: int = 1000,  # Global rate limit per minute
        per_key_rate_limit: int = 100,  # Default per-key rate limit
        burst_multiplier: float = 1.5,  # Allow burst up to 1.5x normal rate
    ):
        super().__init__(app)
        self.redis_url = redis_url or settings.REDIS_URL
        self.redis_client = None
        self.rate_limit_enabled = rate_limit_enabled
        self.signature_verification_enabled = signature_verification_enabled
        self.ip_allowlist_enabled = ip_allowlist_enabled
        self.global_rate_limit = global_rate_limit
        self.per_key_rate_limit = per_key_rate_limit
        self.burst_multiplier = burst_multiplier

        # Path patterns that may carry inbound webhooks and require webhook auth
        # Keep broad prefixes but explicitly exempt management routes below.
        self.webhook_paths = [
            "/webhooks/",  # legacy generic inbound
            "/api/v2/webhooks/",  # includes /ingest and dynamic /{integration_id}
            "/slack/",  # legacy slack inbound
            "/integrations/webhook/",  # any other integration-specific inbound
        ]

        # Paths exempt from webhook auth (handled by normal app auth or separate verification)
        self.exempt_paths = [
            # Slack event handlers (use their own signature flow)
            "/webhooks/slack/interactive",  # Uses Slack signature (legacy prefix)
            "/webhooks/slack/events",  # Uses Slack signature (legacy prefix)
            "/api/v2/webhooks/slack/interactive",  # Current prefix
            "/api/v2/webhooks/slack/events",  # Current prefix
            # Management & UI-facing endpoints (should use session/JWT auth)
            "/api/v2/webhooks/endpoints",
            "/api/v2/webhooks/deliveries",
            "/api/v2/webhooks/api-keys",
            "/api/v2/webhooks/gmail-push",
            "/api/v2/webhooks/gmail-watch-renew",
            "/api/v2/webhooks/sheets",
            # Infra
            "/health",
            "/metrics",
        ]

    async def dispatch(self, request: Request, call_next: Callable) -> Response:
        """Process request through middleware."""
        # Always let preflight requests pass through so CORS can handle them
        if request.method == "OPTIONS":
            return await call_next(request)
        # Check if this is a webhook path
        if not self._is_webhook_path(request.url.path):
            return await call_next(request)

        # Check if path is exempt
        if self._is_exempt_path(request.url.path):
            return await call_next(request)

        # Initialize Redis client if needed
        if not self.redis_client and self.redis_url:
            self.redis_client = await redis.from_url(
                self.redis_url, encoding="utf-8", decode_responses=True
            )

        try:
            # Extract authentication info
            api_key = self._extract_api_key(request)
            signature = self._extract_signature(request)
            source_ip = self._get_client_ip(request)

            # Verify authentication
            if api_key:
                authenticated, account_id = await self._verify_api_key(
                    api_key, source_ip, request
                )
                if not authenticated:
                    return JSONResponse(
                        status_code=401, content={"error": "Invalid or expired API key"}
                    )

                # Add account ID to request state
                request.state.account_id = account_id
                request.state.auth_method = "api_key"

            elif signature and self.signature_verification_enabled:
                verified = await self._verify_signature(signature, request)
                if not verified:
                    return JSONResponse(
                        status_code=401, content={"error": "Invalid signature"}
                    )
                request.state.auth_method = "signature"

            else:
                # No authentication provided
                return JSONResponse(
                    status_code=401, content={"error": "Authentication required"}
                )

            # Check rate limiting
            if self.rate_limit_enabled:
                rate_limit_key = api_key if api_key else source_ip
                allowed, retry_after = await self._check_rate_limit(
                    rate_limit_key, source_ip
                )

                if not allowed:
                    return JSONResponse(
                        status_code=429,
                        content={
                            "error": "Rate limit exceeded",
                            "retry_after": retry_after,
                        },
                        headers={"Retry-After": str(retry_after)},
                    )

            # Check IP allowlist if enabled
            if self.ip_allowlist_enabled:
                if not await self._check_ip_allowlist(source_ip, account_id):
                    return JSONResponse(
                        status_code=403, content={"error": "IP address not allowed"}
                    )

            # Process request
            response = await call_next(request)

            # Add security headers
            response.headers["X-Content-Type-Options"] = "nosniff"
            response.headers["X-Frame-Options"] = "DENY"
            response.headers["X-XSS-Protection"] = "1; mode=block"

            return response

        except HTTPException as e:
            return JSONResponse(status_code=e.status_code, content={"error": e.detail})
        except Exception as e:
            logger.error(f"Webhook auth middleware error: {e}")
            return JSONResponse(
                status_code=500, content={"error": "Internal server error"}
            )

    def _is_webhook_path(self, path: str) -> bool:
        """Check if path requires webhook authentication."""
        return any(path.startswith(p) for p in self.webhook_paths)

    def _is_exempt_path(self, path: str) -> bool:
        """Check if path is exempt from authentication."""
        return any(path.startswith(p) for p in self.exempt_paths)

    def _extract_api_key(self, request: Request) -> Optional[str]:
        """Extract API key from request headers."""
        # Check X-API-Key header
        api_key = request.headers.get("X-API-Key")
        if api_key:
            return api_key

        # Check Authorization header
        auth_header = request.headers.get("Authorization")
        if auth_header and auth_header.startswith("Bearer "):
            return auth_header[7:]

        # Check query parameter (less secure, but sometimes needed)
        if "api_key" in request.query_params:
            return request.query_params["api_key"]

        return None

    def _extract_signature(self, request: Request) -> Optional[str]:
        """Extract signature from request headers."""
        # Check various signature headers
        for header in [
            "X-Webhook-Signature",
            "X-Webhook-Signature-256",
            "X-Hub-Signature",
            "X-Hub-Signature-256",
            "X-Signature",
        ]:
            signature = request.headers.get(header)
            if signature:
                return signature

        return None

    def _get_client_ip(self, request: Request) -> str:
        """Get client IP address, considering proxies."""
        # Check X-Forwarded-For header (for proxies/load balancers)
        forwarded_for = request.headers.get("X-Forwarded-For")
        if forwarded_for:
            # Take the first IP in the chain
            return forwarded_for.split(",")[0].strip()

        # Check X-Real-IP header
        real_ip = request.headers.get("X-Real-IP")
        if real_ip:
            return real_ip

        # Fall back to direct client IP
        if request.client:
            return request.client.host

        return "unknown"

    async def _verify_api_key(
        self, api_key: str, source_ip: str, request: Request
    ) -> tuple[bool, Optional[int]]:
        """
        Verify API key and return (authenticated, account_id).
        """
        try:
            # Hash the API key
            key_hash = hashlib.sha256(api_key.encode()).hexdigest()

            async with session_context() as session:
                # Look up the key
                query = select(WebhookApiKey).where(
                    WebhookApiKey.key_hash == key_hash, WebhookApiKey.active == True
                )
                result = await session.execute(query)
                key_record = result.scalar_one_or_none()

                if not key_record:
                    logger.warning(f"Invalid API key attempt from {source_ip}")
                    return False, None

                # Check expiration
                if key_record.expires_at and key_record.expires_at < datetime.utcnow():
                    logger.warning(
                        f"Expired API key {key_record.id} used from {source_ip}"
                    )
                    return False, None

                # Check IP whitelist if configured
                if key_record.ip_whitelist:
                    if not self._is_ip_allowed(source_ip, key_record.ip_whitelist):
                        logger.warning(
                            f"API key {key_record.id} used from unauthorized IP {source_ip}"
                        )
                        key_record.failed_requests += 1
                        await session.commit()
                        return False, None

                # Check scopes if needed (path-based)
                if key_record.scopes:
                    if not self._check_scopes(request.url.path, key_record.scopes):
                        logger.warning(
                            f"API key {key_record.id} lacks scope for {request.url.path}"
                        )
                        return False, None

                # Update usage stats
                key_record.last_used_at = datetime.utcnow()
                key_record.last_used_ip = source_ip
                key_record.total_requests += 1
                key_record.successful_requests += 1

                await session.commit()

                return True, key_record.account_id

        except Exception as e:
            logger.error(f"Error verifying API key: {e}")
            return False, None

    async def _verify_signature(self, signature: str, request: Request) -> bool:
        """Verify webhook signature."""
        try:
            # Get request body
            body = await request.body()

            # Try different signature formats
            if signature.startswith("sha256="):
                # GitHub/Standard format
                expected = signature[7:]
                algorithm = hashlib.sha256
            elif signature.startswith("sha1="):
                # Legacy format
                expected = signature[5:]
                algorithm = hashlib.sha1
            else:
                # Raw signature
                expected = signature
                algorithm = hashlib.sha256

            # Get secret from settings or endpoint config
            # This would need to look up the endpoint based on path
            secret = settings.WEBHOOK_SECRET
            if not secret:
                logger.warning("No webhook secret configured")
                return False

            # Calculate expected signature
            calculated = hmac.new(secret.encode(), body, algorithm).hexdigest()

            # Compare signatures
            return hmac.compare_digest(calculated, expected)

        except Exception as e:
            logger.error(f"Error verifying signature: {e}")
            return False

    async def _check_rate_limit(self, key: str, source_ip: str) -> tuple[bool, int]:
        """
        Check rate limit and return (allowed, retry_after_seconds).
        """
        if not self.redis_client:
            return True, 0

        try:
            # Use sliding window rate limiting
            now = time.time()
            window_start = now - 60  # 1 minute window

            # Create unique key for this client
            rate_key = f"rate_limit:{key}"

            # Remove old entries outside the window
            await self.redis_client.zremrangebyscore(rate_key, 0, window_start)

            # Count requests in current window
            current_count = await self.redis_client.zcard(rate_key)

            # Determine rate limit
            limit = self.per_key_rate_limit

            # Check for custom rate limit if using API key
            if key.startswith("sk_"):  # Assuming API keys have a prefix
                # Could look up custom rate limit from database
                pass

            # Allow burst
            burst_limit = int(limit * self.burst_multiplier)

            if current_count >= burst_limit:
                # Rate limit exceeded
                # Calculate when the oldest request will expire
                oldest = await self.redis_client.zrange(rate_key, 0, 0, withscores=True)
                if oldest:
                    retry_after = int(oldest[0][1] + 60 - now) + 1
                else:
                    retry_after = 60

                logger.warning(f"Rate limit exceeded for {key} from {source_ip}")
                return False, retry_after

            # Add current request
            await self.redis_client.zadd(rate_key, {str(now): now})
            await self.redis_client.expire(rate_key, 60)

            # Check global rate limit
            global_key = "rate_limit:global"
            await self.redis_client.zremrangebyscore(global_key, 0, window_start)
            global_count = await self.redis_client.zcard(global_key)

            if global_count >= self.global_rate_limit:
                logger.warning("Global rate limit exceeded")
                return False, 60

            await self.redis_client.zadd(global_key, {f"{key}:{now}": now})
            await self.redis_client.expire(global_key, 60)

            return True, 0

        except Exception as e:
            logger.error(f"Error checking rate limit: {e}")
            # Fail open on error
            return True, 0

    async def _check_ip_allowlist(
        self, source_ip: str, account_id: Optional[int]
    ) -> bool:
        """Check if IP is in allowlist."""
        if not account_id:
            return True

        try:
            # Get account's IP allowlist from database or cache
            # This is a simplified version
            if self.redis_client:
                allowlist_key = f"ip_allowlist:{account_id}"
                allowlist = await self.redis_client.smembers(allowlist_key)

                if not allowlist:
                    # No allowlist configured, allow all
                    return True

                return self._is_ip_allowed(source_ip, list(allowlist))

            return True

        except Exception as e:
            logger.error(f"Error checking IP allowlist: {e}")
            return True

    def _is_ip_allowed(self, ip: str, allowlist: List[str]) -> bool:
        """Check if IP is in allowlist (supports CIDR notation)."""
        try:
            source_ip = ipaddress.ip_address(ip)

            for allowed in allowlist:
                try:
                    # Check if it's a network (CIDR notation)
                    if "/" in allowed:
                        network = ipaddress.ip_network(allowed, strict=False)
                        if source_ip in network:
                            return True
                    else:
                        # Direct IP comparison
                        if source_ip == ipaddress.ip_address(allowed):
                            return True
                except:
                    # Invalid IP/network format
                    continue

            return False

        except:
            # Invalid source IP
            return False

    def _check_scopes(self, path: str, scopes: List[str]) -> bool:
        """Check if API key has required scope for path."""
        # Map paths to required scopes
        scope_map = {
            "/webhooks/ingest": "webhook:write",
            "/webhooks/endpoints": "webhook:admin",
            "/webhooks/deliveries": "webhook:read",
            "/api/v2/webhooks": "webhook:write",
        }

        # Find required scope for path
        required_scope = None
        for path_pattern, scope in scope_map.items():
            if path.startswith(path_pattern):
                required_scope = scope
                break

        if not required_scope:
            # No specific scope required
            return True

        # Check if key has required scope or wildcard
        return required_scope in scopes or "webhook:*" in scopes or "*" in scopes


class SignatureVerifier:
    """Helper class for verifying various webhook signatures."""

    @staticmethod
    def verify_slack(body: bytes, timestamp: str, signature: str, secret: str) -> bool:
        """Verify Slack webhook signature."""
        # Check timestamp (prevent replay attacks)
        try:
            ts = int(timestamp)
            if abs(time.time() - ts) > 300:  # 5 minutes
                return False
        except:
            return False

        # Compute signature
        sig_basestring = f"v0:{timestamp}:{body.decode('utf-8')}"
        expected = (
            "v0="
            + hmac.new(
                secret.encode(), sig_basestring.encode(), hashlib.sha256
            ).hexdigest()
        )

        return hmac.compare_digest(expected, signature)

    @staticmethod
    def verify_github(body: bytes, signature: str, secret: str) -> bool:
        """Verify GitHub webhook signature."""
        if signature.startswith("sha256="):
            expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
            return hmac.compare_digest(expected, signature[7:])
        elif signature.startswith("sha1="):
            expected = hmac.new(secret.encode(), body, hashlib.sha1).hexdigest()
            return hmac.compare_digest(expected, signature[5:])

        return False

    @staticmethod
    def verify_stripe(body: bytes, signature: str, secret: str) -> bool:
        """Verify Stripe webhook signature."""
        # Stripe uses a different format
        # signature = "t=timestamp,v1=signature"
        elements = {}
        for element in signature.split(","):
            key_value = element.split("=", 1)
            if len(key_value) == 2:
                elements[key_value[0]] = key_value[1]

        if "t" not in elements or "v1" not in elements:
            return False

        # Check timestamp
        try:
            timestamp = int(elements["t"])
            if abs(time.time() - timestamp) > 300:  # 5 minutes
                return False
        except:
            return False

        # Compute signature
        signed_payload = f"{elements['t']}.{body.decode('utf-8')}"
        expected = hmac.new(
            secret.encode(), signed_payload.encode(), hashlib.sha256
        ).hexdigest()

        return hmac.compare_digest(expected, elements["v1"])

    @staticmethod
    def verify_generic(
        body: bytes, signature: str, secret: str, algorithm: str = "sha256"
    ) -> bool:
        """Verify generic HMAC signature."""
        if algorithm == "sha256":
            hash_func = hashlib.sha256
        elif algorithm == "sha1":
            hash_func = hashlib.sha1
        else:
            return False

        expected = hmac.new(secret.encode(), body, hash_func).hexdigest()

        # Handle prefixed signatures
        if signature.startswith(f"{algorithm}="):
            signature = signature[len(algorithm) + 1 :]

        return hmac.compare_digest(expected, signature)
