"""Cache key generation and management for device import operations."""

import hashlib
import json
import logging

from django.core.cache import cache

logger = logging.getLogger(__name__)


def _build_filter_hash(filters: dict) -> str:
    """
    Build a stable, collision-free hash from a filter dict.

    Removes None values (preserves valid falsy values like 0 and False),
    sorts by key, and returns the first 16 hex characters of the SHA-256
    digest of the JSON-serialized result.
    """
    return hashlib.sha256(
        json.dumps({k: v for k, v in filters.items() if v is not None}, sort_keys=True, separators=(",", ":")).encode()
    ).hexdigest()[:16]


def get_location_choices_cache_key(server_key: str) -> str:
    """Return the cache key for LibreNMS location choices for a given server."""
    return f"librenms_locations_choices:{server_key}"


def get_cache_metadata_key(
    server_key: str, filters: dict, vc_enabled: bool, use_sysname: bool = True, strip_domain: bool = False
) -> str:
    """
    Generate a consistent cache metadata key from filter parameters.

    Args:
        server_key: LibreNMS server identifier
        filters: Filter dictionary
        vc_enabled: Whether VC detection is enabled
        use_sysname: Whether sysName is preferred over hostname for device naming
        strip_domain: Whether domain suffix is stripped from device names

    Returns:
        str: Consistent cache key for metadata
    """
    # Sort filter items to ensure consistent key generation; use "is not None" to preserve
    # valid falsy values like 0 and False (filtering only None/missing entries).
    # Use JSON serialization for a stable, collision-free hash (avoids issues with
    # values containing "=" or "_" that could collide with the key separators).
    filter_hash = _build_filter_hash(filters)
    return f"librenms_filter_cache_metadata_{server_key}_{filter_hash}_{vc_enabled}_sysname={use_sysname}_strip={strip_domain}"


def get_active_cached_searches(server_key: str) -> list[dict]:
    """
    Retrieve all active cached searches for a server and enrich with display-friendly values.

    Enriches raw filter IDs with human-readable names by looking up location names
    from cached choices and converting type codes to display names.

    Args:
        server_key: LibreNMS server identifier

    Returns:
        List of dicts containing cache metadata with enriched display_filters
    """
    from datetime import datetime, timezone

    cache_index_key = f"librenms_cache_index_{server_key}"
    cache_index = cache.get(cache_index_key, [])

    active_searches = []
    valid_cache_keys = []

    # Get location and type choices for enriching display
    location_choices = {}
    type_choices = {
        "": "All Types",
        "network": "Network",
        "server": "Server",
        "storage": "Storage",
        "wireless": "Wireless",
        "firewall": "Firewall",
        "power": "Power",
        "appliance": "Appliance",
        "printer": "Printer",
        "loadbalancer": "Load Balancer",
        "other": "Other",
    }

    # Get cached location choices for enrichment; scoped by server_key so labels
    # from different LibreNMS servers don't bleed into each other's filter summaries.
    location_cache_key = get_location_choices_cache_key(server_key)
    cached_locations = cache.get(location_cache_key)
    if cached_locations:
        location_choices = dict(cached_locations)

    for cache_key in cache_index:
        metadata = cache.get(cache_key)
        if metadata:
            # Cache still exists, calculate time remaining
            cache_timeout = metadata.get("cache_timeout", 300)
            now = datetime.now(timezone.utc)
            try:
                cached_at_raw = metadata.get("cached_at")
                if isinstance(cached_at_raw, datetime):
                    cached_at = cached_at_raw
                elif cached_at_raw:
                    cached_at = datetime.fromisoformat(cached_at_raw)
                else:
                    cached_at = datetime.fromtimestamp(0, timezone.utc)
                # Normalize naive datetimes (e.g., stored without tzinfo) to UTC
                if cached_at.tzinfo is None:
                    cached_at = cached_at.replace(tzinfo=timezone.utc)
            except (ValueError, TypeError):
                cached_at = datetime.fromtimestamp(0, timezone.utc)
            age_seconds = (now - cached_at).total_seconds()
            remaining_seconds = max(0, cache_timeout - age_seconds)

            if remaining_seconds > 0:
                # Add remaining time and cache key
                metadata["remaining_seconds"] = int(remaining_seconds)
                metadata["cache_key"] = cache_key
                # Store numeric sort key so the final sort is unambiguous
                metadata["cached_at_ts"] = cached_at.timestamp()

                # Enrich filters with human-readable display values
                if "filters" in metadata:
                    display_filters = metadata["filters"].copy()
                    # Convert location ID to location name
                    if "location" in display_filters and display_filters["location"] in location_choices:
                        display_filters["location"] = location_choices[display_filters["location"]]
                    # Convert type code to display name
                    if "type" in display_filters and display_filters["type"] in type_choices:
                        display_filters["type"] = type_choices[display_filters["type"]]
                    metadata["display_filters"] = display_filters
                else:
                    # Fallback if filters key missing
                    metadata["display_filters"] = {}

                active_searches.append(metadata)
                valid_cache_keys.append(cache_key)

    # Clean up index if any keys have expired
    if len(valid_cache_keys) < len(cache_index):
        cache.set(cache_index_key, valid_cache_keys, timeout=3600)

    # Sort by most recent first
    active_searches.sort(key=lambda x: x.get("cached_at_ts", 0.0), reverse=True)

    return active_searches


def get_validated_device_cache_key(
    server_key: str,
    filters: dict,
    device_id: int | str,
    vc_enabled: bool,
    use_sysname: bool = True,
    strip_domain: bool = False,
) -> str:
    """
    Generate a consistent cache key for validated device data.

    This ensures both synchronous and background job processing use the same
    cache keys, avoiding duplicate validation work and cache entries.

    Args:
        server_key: LibreNMS server key
        filters: Filter dict with location, type, os, hostname, sysname, hardware keys
        device_id: LibreNMS device ID
        vc_enabled: Whether virtual chassis detection was enabled
        use_sysname: Whether sysName is preferred over hostname for device naming
        strip_domain: Whether domain suffix is stripped from device names

    Returns:
        str: Cache key for the validated device

    Example:
        >>> key = get_validated_device_cache_key('default', {'location': 'NYC'}, 123, True)
        >>> key
        'validated_device_default_e3b0c44298fc1c14_123_vc'
    """
    # Sort filters for a deterministic, cross-process stable hash; None values are excluded
    # (consistent with get_cache_metadata_key).
    filter_hash = _build_filter_hash(filters)
    vc_part = "vc" if vc_enabled else "novc"
    return (
        f"validated_device_{server_key}_{filter_hash}_{device_id}_{vc_part}_sysname={use_sysname}_strip={strip_domain}"
    )


def get_import_device_cache_key(device_id: int | str, server_key: str = "default") -> str:
    """
    Generate cache key for raw LibreNMS device data.

    This key is used to cache raw device data (without validation metadata)
    to avoid redundant API calls when users interact with dropdowns during
    the import workflow.

    Args:
        device_id: LibreNMS device ID
        server_key: LibreNMS server identifier for multi-server setups. Defaults to "default" for backward compatibility.

    Returns:
        str: Cache key for the device data

    Example:
        >>> get_import_device_cache_key(123, "production")
        'import_device_data_production_123'
    """
    return f"import_device_data_{server_key}_{device_id}"


def get_import_search_cache_key(server_key: str, api_filters: dict, client_filters: dict) -> str:
    """
    Generate a deterministic cache key for a LibreNMS device search result.

    The key encodes the server, API-side filters, and client-side filters so
    that different filter combinations produce distinct cache entries.

    Args:
        server_key: Resolved LibreNMS server key (use ``api.server_key``).
        api_filters: Filters forwarded to the LibreNMS API.
        client_filters: Filters applied client-side after the API response.

    Returns:
        str: Cache key for the import search result.
    """
    return (
        f"librenms_devices_import_{server_key}_{_build_filter_hash(api_filters)}_{_build_filter_hash(client_filters)}"
    )
