"""CSRF protection using Double Submit Cookie pattern."""

import hashlib
import json
import os
import secrets
import time
from datetime import datetime, timedelta
from typing import Any, Dict, FrozenSet

import redis
import structlog

logger = structlog.get_logger(__name__)

# Redis configuration
REDIS_DEFAULT_HOST = "redis"
REDIS_DEFAULT_PORT = "6379"
REDIS_DEFAULT_DB = 0

# Token configuration
REDIS_KEY_PREFIX = "csrf:token:"
CSRF_TOKEN_TTL_SECONDS = 3600
TOKEN_SIZE_BYTES = 32
SECRET_KEY_SIZE_BYTES = 32
TOKEN_PARTS_COUNT = 3
TOKEN_PREVIEW_LENGTH = 16
SCAN_BATCH_SIZE = 100

# Redis TTL special values
TTL_KEY_NOT_EXISTS = -2
TTL_KEY_NO_EXPIRY = -1

# Protected HTTP methods
PROTECTED_METHODS: FrozenSet[str] = frozenset({"POST", "PUT", "DELETE", "PATCH"})

# Excluded endpoints (no CSRF protection)
EXCLUDED_ENDPOINTS: FrozenSet[str] = frozenset({
    "/auth/login",
    "/health",
    "/health/detailed",
    "/health/ready",
    "/health/live",
    "/health/routing",
    "/metrics",
    "/docs",
    "/openapi.json",
})


class CSRFProtection:
    """CSRF protection with Double Submit Cookie pattern."""

    def __init__(self) -> None:
        """
        Initialize CSRF protection.

        Raises:
            RuntimeError: If Redis connection fails.
        """
        self._redis_client = redis.Redis(
            host=os.getenv("REDIS_HOST", REDIS_DEFAULT_HOST),
            port=int(os.getenv("REDIS_PORT", REDIS_DEFAULT_PORT)),
            password=os.getenv("REDIS_PASSWORD"),
            db=REDIS_DEFAULT_DB,
            decode_responses=True,
        )

        self._csrf_token_ttl = CSRF_TOKEN_TTL_SECONDS
        self._token_size = TOKEN_SIZE_BYTES
        self._secret_key = secrets.token_bytes(SECRET_KEY_SIZE_BYTES)

        try:
            self._redis_client.ping()
            logger.info("CSRF Protection connected to Redis successfully")
        except Exception as e:
            logger.error("Failed to connect to Redis for CSRF", error=str(e))
            raise RuntimeError(f"Redis connection failed: {e}") from e

        self._protected_methods: FrozenSet[str] = PROTECTED_METHODS
        self._excluded_endpoints: FrozenSet[str] = EXCLUDED_ENDPOINTS
    
    def generate_csrf_token(self, user_id: str) -> str:
        """
        Generate CSRF token for user.

        Args:
            user_id: User ID.

        Returns:
            CSRF token.
        """
        try:
            random_bytes = secrets.token_bytes(self._token_size)

            timestamp = str(int(time.time()))
            data_to_sign = f"{user_id}:{timestamp}:{random_bytes.hex()}"
            signature = hashlib.sha256(
                f"{data_to_sign}:{self._secret_key.hex()}".encode()
            ).hexdigest()

            csrf_token = f"{random_bytes.hex()}:{timestamp}:{signature}"

            now = datetime.now()
            expires_at = now + timedelta(seconds=self._csrf_token_ttl)
            token_data = {
                "user_id": user_id,
                "created_at": now.isoformat(),
                "expires_at": expires_at.isoformat(),
                "used": False,
            }

            redis_key = f"{REDIS_KEY_PREFIX}{csrf_token}"
            self._redis_client.setex(
                redis_key, self._csrf_token_ttl, json.dumps(token_data)
            )

            logger.debug(
                "CSRF token generated in Redis",
                user_id=user_id,
                token_preview=csrf_token[:TOKEN_PREVIEW_LENGTH] + "...",
                expires_at=expires_at.isoformat(),
            )

            return csrf_token

        except Exception as e:
            logger.error("Failed to generate CSRF token", user_id=user_id, error=str(e))
            raise

    def validate_csrf_token(self, token: str, user_id: str) -> bool:
        """
        Validate CSRF token.

        Args:
            token: CSRF token.
            user_id: User ID.

        Returns:
            True if token is valid.
        """
        try:
            if not token or not user_id:
                return False

            redis_key = f"{REDIS_KEY_PREFIX}{token}"
            token_json = self._redis_client.get(redis_key)

            if not token_json:
                logger.warning(
                    "CSRF token not found in Redis",
                    token_preview=token[:TOKEN_PREVIEW_LENGTH] + "...",
                    user_id=user_id,
                )
                return False

            token_data = json.loads(token_json)

            expires_at = datetime.fromisoformat(token_data["expires_at"])
            if datetime.now() > expires_at:
                logger.warning(
                    "CSRF token expired",
                    token_preview=token[:TOKEN_PREVIEW_LENGTH] + "...",
                    user_id=user_id,
                )
                self._redis_client.delete(redis_key)
                return False

            if token_data["user_id"] != user_id:
                logger.warning(
                    "CSRF token user mismatch",
                    token_preview=token[:TOKEN_PREVIEW_LENGTH] + "...",
                    expected_user=user_id,
                    actual_user=token_data["user_id"],
                )
                return False

            if not self._verify_token_signature(token, user_id):
                logger.warning(
                    "CSRF token signature invalid",
                    token_preview=token[:TOKEN_PREVIEW_LENGTH] + "...",
                    user_id=user_id,
                )
                self._redis_client.delete(redis_key)
                return False

            token_data["used"] = True
            ttl = self._redis_client.ttl(redis_key)
            if ttl > 0:
                self._redis_client.setex(redis_key, ttl, json.dumps(token_data))

            logger.debug(
                "CSRF token validated successfully",
                token_preview=token[:TOKEN_PREVIEW_LENGTH] + "...",
                user_id=user_id,
            )

            return True

        except Exception as e:
            logger.error(
                "Error validating CSRF token",
                token_preview=token[:TOKEN_PREVIEW_LENGTH] + "..." if token else "none",
                user_id=user_id,
                error=str(e),
            )
            return False

    def _verify_token_signature(self, token: str, user_id: str) -> bool:
        """
        Verify token signature.

        Args:
            token: CSRF token.
            user_id: User ID.

        Returns:
            True if signature is valid.
        """
        try:
            parts = token.split(":")
            if len(parts) != TOKEN_PARTS_COUNT:
                return False

            random_hex, timestamp, signature = parts

            data_to_sign = f"{user_id}:{timestamp}:{random_hex}"
            expected_signature = hashlib.sha256(
                f"{data_to_sign}:{self._secret_key.hex()}".encode()
            ).hexdigest()

            return signature == expected_signature

        except Exception:
            return False

    def should_protect_endpoint(self, method: str, path: str) -> bool:
        """
        Check if endpoint needs CSRF protection.

        Args:
            method: HTTP method.
            path: Endpoint path.

        Returns:
            True if CSRF protection is needed.
        """
        if method not in self._protected_methods:
            return False

        if path in self._excluded_endpoints:
            return False

        for excluded_path in self._excluded_endpoints:
            if path.startswith(excluded_path):
                return False

        return True

    def cleanup_expired_tokens(self) -> None:
        """
        Clean up expired CSRF tokens from Redis.

        Note: Redis automatically removes keys with expired TTL.
        """
        try:
            pattern = f"{REDIS_KEY_PREFIX}*"
            keys = list(self._redis_client.scan_iter(match=pattern, count=SCAN_BATCH_SIZE))

            cleaned_count = 0
            for key in keys:
                ttl = self._redis_client.ttl(key)
                if ttl == TTL_KEY_NOT_EXISTS:
                    cleaned_count += 1
                elif ttl == TTL_KEY_NO_EXPIRY:
                    self._redis_client.delete(key)
                    cleaned_count += 1

            if cleaned_count > 0:
                logger.info(
                    "CSRF tokens cleanup completed",
                    cleaned_count=cleaned_count,
                    remaining_count=len(keys) - cleaned_count,
                )
        except Exception as e:
            logger.error("Failed to cleanup expired CSRF tokens", error=str(e))

    def get_csrf_stats(self) -> Dict[str, Any]:
        """
        Get CSRF token statistics from Redis.

        Returns:
            Dictionary with CSRF statistics.
        """
        try:
            pattern = f"{REDIS_KEY_PREFIX}*"
            keys = list(self._redis_client.scan_iter(match=pattern, count=SCAN_BATCH_SIZE))

            active_tokens = 0
            used_tokens = 0

            for key in keys:
                try:
                    token_json = self._redis_client.get(key)
                    if token_json:
                        token_data = json.loads(token_json)
                        active_tokens += 1
                        if token_data.get("used", False):
                            used_tokens += 1
                except Exception:
                    continue

            return {
                "total_tokens": len(keys),
                "active_tokens": active_tokens,
                "used_tokens": used_tokens,
                "token_ttl_seconds": self._csrf_token_ttl,
                "protected_methods": sorted(self._protected_methods),
                "excluded_endpoints": sorted(self._excluded_endpoints),
                "storage": "Redis",
            }
        except Exception as e:
            logger.error("Failed to get CSRF stats", error=str(e))
            return {"error": str(e), "storage": "Redis"}

    def revoke_user_tokens(self, user_id: str) -> None:
        """
        Revoke all user tokens from Redis.

        Args:
            user_id: User ID.
        """
        try:
            pattern = f"{REDIS_KEY_PREFIX}*"
            keys = list(self._redis_client.scan_iter(match=pattern, count=SCAN_BATCH_SIZE))

            revoked_count = 0
            for key in keys:
                try:
                    token_json = self._redis_client.get(key)
                    if token_json:
                        token_data = json.loads(token_json)
                        if token_data.get("user_id") == user_id:
                            self._redis_client.delete(key)
                            revoked_count += 1
                except Exception:
                    continue

            if revoked_count > 0:
                logger.info(
                    "Revoked user CSRF tokens from Redis",
                    user_id=user_id,
                    count=revoked_count,
                )
        except Exception as e:
            logger.error(
                "Failed to revoke user CSRF tokens", user_id=user_id, error=str(e)
            )


csrf_protection = CSRFProtection()
