import cProfile
import ipaddress
import logging
import os
import re
import time
import tracemalloc
import zoneinfo
from logging import getLogger
from logging.handlers import RotatingFileHandler

import pytz
import sentry_sdk
from django.conf import settings
from django.core.cache import cache
from django.core.mail import mail_managers
from django.db import connection
from django.http import HttpResponseForbidden
from django.middleware.common import BrokenLinkEmailsMiddleware
from django.test.utils import CaptureQueriesContext
from django.utils import timezone, translation
from django.utils.deprecation import MiddlewareMixin

from .utils import get_language_code

logger = getLogger(__file__)
logger.setLevel(logging.INFO)


BLOCKED_IPS = [x.strip() for x in os.getenv("BLOCKED_IPS", "").split(",") if x.strip()]
BLOCKED_NETWORKS = [
    x.strip() for x in os.getenv("BLOCKED_NETWORKS", "").split(",") if x.strip()
]


class TimeZoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tz_header = request.headers.get("TZ")
        # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

        if tz_header:
            try:
                timezone.activate(tz_header)
                logger.info(
                    f"[TimeZoneMiddleware] Timezone activated: {tz_header} for request {request.path}"
                )
                print(
                    f"UTC: {timezone.now()} | Local ({tz_header}): {timezone.localtime()}"
                )
            except (pytz.UnknownTimeZoneError, zoneinfo.ZoneInfoNotFoundError):
                logger.error(
                    f"[TimeZoneMiddleware] Invalid timezone '{tz_header}' provided in"
                    f" TZ header for request {request.path}. "
                    f"Falling back to UTC."
                )
                timezone.activate("UTC")
        else:
            # Set default timezone if TZ header is not provided
            logger.debug(
                f"[TimeZoneMiddleware] No TZ header provided for request {request.path}. Using default timezone: UTC"
            )
            timezone.activate("UTC")

        response = self.get_response(request)

        logger.debug(
            f"[TimeZoneMiddleware] Timezone deactivated for request {request.path}"
        )
        timezone.deactivate()
        return response


class LanguageMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Extract language from _lang query parameter
        lang = request.GET.get("_lang")
        if lang:
            lang = get_language_code(lang).upper()
        if lang:
            # Activate the new language if it's valid
            translation.activate(lang)
        else:
            # Fallback to default language if not valid
            translation.activate("EN")

        response = self.get_response(request)
        # Restore the original language
        translation.activate("EN")
        return response


class RequestTimeMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Record the start time
        start_time = time.time()

        # Process the request
        response = self.get_response(request)

        # Calculate the time taken
        duration = time.time() - start_time
        logger.info(f"Request to {request.path} took {duration:.4f} seconds")

        response["X-Request-Duration"] = f"{duration:.4f} seconds"
        return response


class MemoryUsageMiddleware(MiddlewareMixin):
    def process_request(self, request):
        tracemalloc.start()  # Start tracking memory

    def process_response(self, request, response):
        _, peak_memory = tracemalloc.get_traced_memory()
        peak_memory_mb = peak_memory / 1024 / 1024  # Convert to MB
        tracemalloc.stop()  # Stop tracking

        logger.info(
            f"[{request.method}] {request.path} - Peak Memory Used: {peak_memory_mb:.2f} MB"
        )
        return response


class ResponseTimeLoggingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.logger = logging.getLogger("metric_logger")
        if not self.logger.handlers:
            handler = RotatingFileHandler(
                "metric.log", maxBytes=5 * 1024 * 1024, backupCount=3
            )
            formatter = logging.Formatter(
                "%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
            )
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)
            self.logger.setLevel(logging.INFO)

    def __call__(self, request):
        start = time.time()
        response = self.get_response(request)
        duration_ms = int((time.time() - start) * 1000)
        path = request.path
        method = request.method
        self.logger.info(
            f"Method: {method} | Path: {path} | Response Time: {duration_ms} ms"
        )
        return response


class QueryLoggingMiddleware:
    """
    Django middleware for logging all SQL queries executed during the processing of each HTTP request.

    This middleware captures all database queries made during the lifecycle of a request,
    logs the total number of queries, their execution time, and details about each query
    (including the SQL statement and its duration) to a rotating log file ("query.log").
    The log entry also includes the HTTP method, request path, user (if authenticated), and
    the total duration of the request.

    Attributes:
        get_response (callable): The next middleware or view in the Django request/response cycle.
        logger (logging.Logger): Logger instance for writing query logs to file.

        Initialize the QueryLoggingMiddleware.

        Sets up the logger with a rotating file handler if it hasn't been set up already.

        Args:
            get_response (callable): The next middleware or view in the Django request/response cycle.

        # ...


        Process the incoming HTTP request, capture and log all SQL queries executed.

        Args:
            request (HttpRequest): The incoming HTTP request object.

        Returns:
            HttpResponse: The response generated by the next middleware or view.
    """

    def __init__(self, get_response):
        self.get_response = get_response
        self.logger = logging.getLogger("query_logger")
        if not self.logger.handlers:
            handler = RotatingFileHandler(
                "query.log", maxBytes=5 * 1024 * 1024, backupCount=3
            )
            formatter = logging.Formatter(
                "%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
            )
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)
            self.logger.setLevel(logging.INFO)

    def __call__(self, request):
        start_time = time.time()
        with CaptureQueriesContext(connection) as queries:
            response = self.get_response(request)
        duration = time.time() - start_time
        path = request.path
        method = request.method
        user = getattr(request, "user", None)
        user_repr = str(user) if user and user.is_authenticated else "Anonymous"
        total_queries = len(queries.captured_queries)
        if queries.captured_queries:
            log_lines = [
                f"Method: {method} | Path: {path} | User: {user_repr} | "
                f"Total Queries: {total_queries} | Total Duration: {duration:.4f} sec | Queries:"
            ]
            for q in queries.captured_queries:
                sql = q.get("sql")
                q_duration = q.get("time")
                log_lines.append(f"    Query: {sql} | Duration: {q_duration} sec")
            log_message = "\n".join(log_lines)
            self.logger.info(log_message)
        return response


def get_client_ip(request):
    x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "").strip()
    if x_forwarded_for and x_forwarded_for.split(","):
        ip = x_forwarded_for.split(",")[0].strip()  # Take the first IP in the list
    else:
        ip = request.META.get("REMOTE_ADDR", "").strip()
    return ip


class BlockBlackListedIPMiddleware:
    """
    Django middleware for blocking requests from blacklisted IP addresses and network ranges.

    This middleware checks the client's IP address against:
    1. Static blocked IPs from environment (BLOCKED_IPS)
    2. Static blocked network ranges from environment (BLOCKED_NETWORKS)
    3. Dynamic blocked IPs from Redis cache (added by CustomBrokenLinkEmailsMiddleware)

    If the client's IP matches any blocked IP or falls within a blocked network range,
    the request is denied with an HTTP 403 Forbidden response.

    Attributes:
        get_response (callable): The next middleware or view in the Django request/response cycle.

    Args:
        get_response (callable): The next middleware or view in the Django request/response cycle.

    Returns:
        HttpResponse: Either an HTTP 403 Forbidden response for blocked IPs, or the response
                     from the next middleware/view in the chain.
    """

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        ip = get_client_ip(request)
        if ip:
            client_ip = ipaddress.ip_address(ip)

            # Check individual IPs from environment
            if BLOCKED_IPS and ip in BLOCKED_IPS:
                logger.info(
                    f"[BlockBlackListedIPMiddleware] Blocked request from IP: {ip} - IP found in static blocked list"
                )
                return HttpResponseForbidden("Access Denied: Your IP is blocked.")

            # Check IP ranges from environment
            if BLOCKED_NETWORKS:
                for network_str in BLOCKED_NETWORKS:
                    network = ipaddress.ip_network(network_str)
                    if client_ip in network:
                        logger.info(
                            f"[BlockBlackListedIPMiddleware] Blocked request from IP: {ip} - IP "
                            f"falls within blocked network range: {network_str}"
                        )
                        return HttpResponseForbidden(
                            "Access Denied: Your IP range is blocked."
                        )

            # Check dynamic block list from Redis cache (added by CustomBrokenLinkEmailsMiddleware)
            block_info = cache.get(f"broken_links:blocked_ips:{ip}")
            if block_info:
                logger.info(
                    f"[BlockBlackListedIPMiddleware] Blocked request from IP: {ip} - "
                    f"IP in dynamic blocked list. Reason: {block_info.get('reason')}"
                )
                return HttpResponseForbidden(
                    "Access Denied: You have been blocked from accessing this server."
                )

        response = self.get_response(request)
        return response


class FunctionDurationTrackingMiddleware:
    """
    Django middleware for profiling application code execution during request processing.

    This middleware uses cProfile to capture detailed profiling information similar to the
    profile_and_timeit decorator but at the middleware level for all requests. It focuses on
    application code and provides meaningful profiling data like a traditional profiler.

    The middleware tracks:
    - Application function calls with cumulative and per-call times
    - Function call counts and time percentages
    - Detailed profiling statistics
    - Request context (method, path, user)

    Logs are written to "function_durations.log" with rotation for file size management.
    """

    def __init__(self, get_response):

        self.get_response = get_response
        self.logger = logging.getLogger("function_duration_logger")
        if not self.logger.handlers:
            handler = RotatingFileHandler(
                "function_durations.log", maxBytes=10 * 1024 * 1024, backupCount=5
            )
            formatter = logging.Formatter(
                "%(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
            )
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)
            self.logger.setLevel(logging.INFO)

    def __call__(self, request):

        # Start profiling
        pr = cProfile.Profile()
        pr.enable()

        start_time = time.perf_counter()
        response = self.get_response(request)
        end_time = time.perf_counter()

        pr.disable()

        # Generate profiling report
        self._log_profiling_results(request, pr, end_time - start_time)

        return response

    def _log_profiling_results(self, request, profiler, total_time):
        """Generate and log profiling results showing ALL function calls with durations."""
        import io
        import pstats

        method = request.method
        path = request.path
        user = getattr(request, "user", None)
        user_repr = str(user) if user and user.is_authenticated else "Anonymous"

        # Create stats object
        s = io.StringIO()
        ps = pstats.Stats(profiler, stream=s)

        # Sort by cumulative time
        ps.sort_stats("cumulative")

        # Get stats data for custom formatting
        stats = ps.stats
        total_calls = ps.total_calls

        # Prepare log message header
        log_lines = [
            "COMPREHENSIVE PROFILING REPORT",
            f"Method: {method} | Path: {path} | User: {user_repr}",
            f"Total Time: {total_time:.6f}s | Total Function Calls: {total_calls}",
            "",
            f"{'ncalls':<10} {'tottime':<10} {'percall':<10} {'cumtime':<10} "
            f"{'percall':<10} {'filename:lineno(function)'}",
            f"{'-'*120}",
        ]

        # Sort all functions by cumulative time (most expensive first)
        sorted_stats = sorted(
            [(key, value) for key, value in stats.items()],
            key=lambda x: x[1][3],  # Sort by cumtime
            reverse=True,
        )

        # No categorization needed - just prepare for top 30 display

        # Helper function to format function info
        def format_function_line(filename, lineno, funcname, cc, nc, tt, ct):
            percall_tt = tt / cc if cc > 0 else 0
            percall_ct = ct / cc if cc > 0 else 0

            # Create a more readable function location
            if "site-packages" in filename:
                # For third-party packages, show package/module
                parts = filename.split("site-packages/")
                if len(parts) > 1:
                    package_path = parts[1]
                    if "django/" in package_path:
                        short_path = "django/" + package_path.split("django/")[-1]
                    else:
                        short_path = "/".join(
                            package_path.split("/")[:2]
                        )  # Show package/module
                else:
                    short_path = filename.split("/")[-1]
            else:
                # For application code, show relative path
                if "/iam_service/" in filename:
                    short_path = "iam_service/" + filename.split("/iam_service/")[-1]
                elif "/config/" in filename:
                    short_path = "config/" + filename.split("/config/")[-1]
                else:
                    short_path = (
                        "/".join(filename.split("/")[-2:])
                        if "/" in filename
                        else filename
                    )

            func_location = f"{short_path}:{lineno}({funcname})"

            return f"{cc:<10} {tt:<10.6f} {percall_tt:<10.6f} {ct:<10.6f} {percall_ct:<10.6f} {func_location}"

        # Show top 30 slowest functions only
        log_lines.append("")
        log_lines.append("TOP 30 SLOWEST FUNCTIONS:")

        for i, ((filename, lineno, funcname), stat_tuple) in enumerate(
            sorted_stats[:30]
        ):
            # Handle different stat tuple formats
            if len(stat_tuple) >= 4:
                cc, nc, tt, ct = stat_tuple[:4]
            else:
                continue  # Skip malformed entries

            log_lines.append(
                f"{i+1:2d}. {format_function_line(filename, lineno, funcname, cc, nc, tt, ct)}"
            )

        # Simple performance summary
        log_lines.extend(
            [
                "",
                "PERFORMANCE SUMMARY:",
                f"  Total Request Time: {total_time:.6f}s",
                f"  Total Functions Analyzed: {len(sorted_stats)}",
                "  Showing Top 30 Slowest Functions",
                "",
            ]
        )

        # Log everything as a single message
        log_message = "\n".join(log_lines)
        self.logger.info(log_message)


# ============================================================================
# BROKEN LINK TRACKING & IP BLOCKING MIDDLEWARE
# ============================================================================


def _get_broken_link_config(key, default=None):
    """Get config value from Django settings, environment, or defaults."""
    config_dict = getattr(settings, "BROKEN_LINK_CONFIG", {})
    if key in config_dict:
        return config_dict[key]

    env_key = f"BROKEN_LINK_{key}"
    env_value = os.getenv(env_key)
    if env_value is not None:
        if env_value.lower() in ("true", "1", "yes"):
            return True
        elif env_value.lower() in ("false", "0", "no"):
            return False
        try:
            return int(env_value)
        except ValueError:
            return env_value

    return default


def _get_allowed_patterns():
    """Get compiled regex patterns for allowed/ignored URLs."""
    default_patterns = [
        r"^/static/.*",
        r"^/media/.*",
        r"\.js$",
        r"\.css$",
        r"\.png$",
        r"\.jpg$",
        r"\.jpeg$",
        r"\.gif$",
        r"\.svg$",
        r"\.woff.*$",
        r"\.ttf$",
        r"/favicon\.ico$",
    ]
    patterns = _get_broken_link_config("ALLOW_PATTERNS", default_patterns)
    return [re.compile(p) for p in patterns] if patterns else []


def _get_exempt_ips():
    """Get list of IPs that should never be blocked."""
    return _get_broken_link_config("EXEMPT_IPS", [])


def _should_ignore_broken_link(path):
    """Check if path matches allowed patterns (should be ignored)."""
    for pattern in _get_allowed_patterns():
        if pattern.search(path):
            return True
    return False


def _increment_ip_counter(ip):
    """Increment broken link counter for IP in cache."""
    threshold_period = _get_broken_link_config("THRESHOLD_PERIOD", 60)
    counter_key = f"broken_links:counter:{ip}"
    current_count = cache.get(counter_key, 0)
    cache.set(counter_key, current_count + 1, threshold_period)
    return current_count + 1


def _check_threshold_exceeded(ip):
    """Check if IP exceeded broken link threshold."""
    threshold = _get_broken_link_config("THRESHOLD", 10)
    counter_key = f"broken_links:counter:{ip}"
    current_count = cache.get(counter_key, 0)
    return current_count >= threshold


def _block_ip(ip, reason, counter):
    """Add IP to block list in cache."""
    block_duration = _get_broken_link_config("BLOCK_DURATION", 2592000)  # 30 days
    block_key = f"broken_links:blocked_ips:{ip}"

    block_info = {
        "blocked_at": timezone.now().isoformat(),
        "reason": reason,
        "counter": counter,
        "duration": block_duration,
    }

    cache.set(block_key, block_info, block_duration)


def _log_malicious_request(ip, path, referer, user_agent, counter, threshold):
    """Log malicious request with all details."""
    logger_name = _get_broken_link_config("LOGGER_NAME", "Malicious Tracker")
    malicious_logger = getLogger(logger_name)

    log_msg = (
        f"ip={ip} | path={path} | referer={referer} | "
        f"user_agent={user_agent} | timestamp={timezone.now().isoformat()} | "
        f"threshold={threshold}/s | counter={counter}"
    )
    malicious_logger.warning(log_msg)


def _log_blocked_request(ip, reason, counter, block_duration):
    """Log when IP is blocked."""
    logger_name = _get_broken_link_config("LOGGER_NAME", "Malicious Tracker")
    malicious_logger = getLogger(logger_name)

    log_msg = (
        f"BLOCKED ip={ip} | reason={reason} | counter={counter} | "
        f"block_duration={block_duration}s | timestamp={timezone.now().isoformat()}"
    )
    malicious_logger.error(log_msg)


def _send_block_alert_email(ip, reason, counter, block_duration):
    """Send email alert to managers when IP is blocked."""
    managers = getattr(settings, "BROKEN_LINK_MANAGERS", None)
    if not managers:
        managers = getattr(settings, "MANAGERS", [])

    if not managers:
        return

    subject = f"[SECURITY] IP Address Blocked: {ip}"
    message = (
        f"An IP address has been blocked due to excessive broken link requests.\n\n"
        f"IP Address: {ip}\n"
        f"Reason: {reason}\n"
        f"Broken Links Count: {counter}\n"
        f"Block Duration: {block_duration} seconds ({block_duration / 86400:.1f} days)\n"
        f"Blocked At: {timezone.now().isoformat()}\n"
        f"Block Expires At: {(timezone.now() + timezone.timedelta(seconds=block_duration)).isoformat()}\n"
    )

    try:
        mail_managers(subject, message, fail_silently=True)
    except Exception as e:
        logger.exception(
            f"[CustomBrokenLinkEmailsMiddleware] Failed to send alert email: {e}"
        )


def _send_to_sentry(ip, reason, counter):
    """Send alert to Sentry."""
    sentry_enabled = _get_broken_link_config("SENTRY_ENABLED", True)
    if not sentry_enabled:
        return

    try:
        sentry_sdk.capture_message(
            f"IP {ip} blocked: {reason} (counter: {counter})",
            level="warning",
        )
    except Exception as e:
        logger.exception(
            f"[CustomBrokenLinkEmailsMiddleware] Failed to send to Sentry: {e}"
        )


class CustomBrokenLinkEmailsMiddleware(BrokenLinkEmailsMiddleware):
    """
    Custom broken link middleware that inherits from Django's BrokenLinkEmailsMiddleware.

    Features:
    - Logs all broken link attempts to logger with [Malicious Tracker] prefix
    - Tracks IP addresses using Django cache (Redis)
    - Blocks IPs after exceeding threshold (e.g., 10 broken links/minute)
    - Sends email alerts and Sentry notifications on IP block
    - Supports allow patterns to whitelist certain URLs (frontend errors, etc.)
    - Respects IP exemptions from config

    Configuration is read from:
    1. Django settings (BROKEN_LINK_CONFIG dict)
    2. Environment variables (BROKEN_LINK_* prefix)
    3. Sensible defaults
    """

    def process_response(self, request, response):
        """Log broken links and track abusive IPs."""
        if response.status_code != 404 or settings.DEBUG:
            return response

        domain = request.get_host()
        path = request.get_full_path()
        referer = request.META.get("HTTP_REFERER", "")
        ip = get_client_ip(request)
        user_agent = request.META.get("HTTP_USER_AGENT", "<none>")

        # Skip if request is ignorable (Django's logic)
        if self.is_ignorable_request(request, path, domain, referer):
            return response

        # Skip if URL matches allow patterns
        if _should_ignore_broken_link(path):
            return response

        # Skip if IP is exempt
        if ip in _get_exempt_ips():
            return response

        # Rate limiting is disabled, just log and return
        if not _get_broken_link_config("ENABLE_RATE_LIMITING", True):
            threshold_period = _get_broken_link_config("THRESHOLD_PERIOD", 60)
            _log_malicious_request(ip, path, referer, user_agent, 1, threshold_period)
            return response

        # Increment IP counter
        counter = _increment_ip_counter(ip)
        threshold = _get_broken_link_config("THRESHOLD", 10)
        threshold_period = _get_broken_link_config("THRESHOLD_PERIOD", 60)

        # Log the broken link attempt
        _log_malicious_request(
            ip, path, referer, user_agent, counter, f"{threshold}/{threshold_period}"
        )

        # Check if IP exceeded threshold
        if _check_threshold_exceeded(ip):
            auto_block = _get_broken_link_config("AUTO_BLOCK_ENABLED", True)
            if auto_block:
                block_duration = _get_broken_link_config("BLOCK_DURATION", 2592000)
                _block_ip(ip, "THRESHOLD_EXCEEDED", counter)
                _log_blocked_request(ip, "THRESHOLD_EXCEEDED", counter, block_duration)

                # Send alerts
                if _get_broken_link_config("SEND_EMAIL_ON_THRESHOLD_EXCEEDED", True):
                    _send_block_alert_email(
                        ip, "THRESHOLD_EXCEEDED", counter, block_duration
                    )

                _send_to_sentry(ip, "THRESHOLD_EXCEEDED", counter)

        return response
