"""
Unified cache invalidation registry.

This module handles registration and execution of both local and cross-service cache invalidation.
It provides a unified registry and handler for all cache invalidation events.
"""

import json
from sqlalchemy import event, inspect
from concurrent.futures import ThreadPoolExecutor
from wedeliver_core_plus.helpers.caching.cache_logger import (
    cache_debug, cache_info, cache_warning, cache_error
)


# ============================================================================
# GLOBAL CACHE ENABLE CHECK
# ============================================================================

def is_cache_enabled():
    """
    Check if cache system is enabled globally.

    This is the single source of truth for cache enabled/disabled state.
    When ENABLE_REDIS=False, all cache operations are skipped for zero overhead.

    Returns:
        bool: True if CACHE_ENABLE=True, False otherwise
    """
    try:
        from flask import current_app
        return current_app.config.get("CACHE_ENABLE", False)
    except RuntimeError:
        # Outside app context (e.g., during imports)
        return False


# ============================================================================
# UNIFIED REGISTRY - Handles both local and cross-service invalidation
# ============================================================================

# Unified registry structure:
# {
#     model_class: {
#         "local_rules": [cache_rule_instance1, cache_rule_instance2, ...],
#         "cross_service_rules": [
#             {
#                 "source_service": "thrivve-service",
#                 "api_path": "/finance/api/v1/me/balance",
#                 "scoped_cache_keys": {...},
#                 "conditions": {...}
#             },
#             ...
#         ]
#     }
# }
_unified_registry = {}
_listeners_registered = set()  # {(model_class, tuple(events))}

# Global thread pool for event listeners
# Handles async processing of cache invalidation events
# Prevents blocking database transactions
_event_pool = None
_event_pool_initialized = False


# ============================================================================
# DEPRECATED - Will be removed after migration
# ============================================================================
_cross_service_registry = {}
_cross_service_listeners_registered = set()


# ============================================================================
# INITIALIZATION FUNCTION
# ============================================================================

def initialize_cache_system(app):
    """
    Initialize the unified cache invalidation system on app startup.

    This function:
    1. Checks if cache is enabled (ENABLE_REDIS flag)
    2. Flushes all cache (optional, based on config)
    3. Initializes the unified registry
    4. Eagerly initializes ModelDiscovery (scans all models at startup)

    Args:
        app: Flask application instance

    Returns:
        bool: True if initialization successful, False if disabled or failed
    """
    try:
        with app.app_context():
            # Check if cache is enabled
            if not is_cache_enabled():
                app.logger.info("[Cache] Cache system disabled (ENABLE_REDIS=False)")
                return False

            # Flush all cache on startup (optional)
            flush_on_startup = app.config.get('CACHE_FLUSH_ON_STARTUP', True)
            if flush_on_startup:
                from wedeliver_core_plus import BaseCacheRule
                success = BaseCacheRule.flush_all_cache()
                if success:
                    app.logger.info("[Cache] All cache cleared on startup")

            # Initialize unified registry (empty, will be populated lazily)
            global _unified_registry, _listeners_registered
            _unified_registry = {}
            _listeners_registered = set()

            # Eagerly initialize ModelDiscovery
            from wedeliver_core_plus.helpers.model_discovery import ModelDiscovery
            discovery = ModelDiscovery()  # Scans app/models directory NOW
            model_count = len(discovery.list_models())
            app.logger.info(f"[Cache] ModelDiscovery initialized: {model_count} models discovered")

            app.logger.info("[Cache] Unified cache system initialized")
            return True

    except Exception as e:
        app.logger.error(f"[Cache] Error during cache system initialization: {e}")
        return False


# ============================================================================
# EVENT LISTENER THREAD POOL
# ============================================================================

def _get_event_pool():
    """
    Get or create the event listener thread pool.

    This pool handles async processing of cache invalidation events
    to prevent blocking database transactions.

    Returns:
        ThreadPoolExecutor: Thread pool for event listeners
    """
    global _event_pool, _event_pool_initialized

    if not _event_pool_initialized:
        try:
            from flask import current_app
            pool_size = current_app.config.get('CACHE_EVENT_POOL_SIZE', 5)
        except:
            pool_size = 5

        _event_pool = ThreadPoolExecutor(
            max_workers=pool_size,
            thread_name_prefix="cache-event"
        )
        _event_pool_initialized = True
        cache_debug(f"Initialized event pool with {pool_size} workers")

    return _event_pool


# ============================================================================
# UNIFIED REGISTRATION FUNCTIONS
# ============================================================================

def register_model_invalidation(
    model_name: str,
    conditions: dict,
    cache_rule_instance=None,
    source_service: str = None,
    api_path: str = None,
    scoped_cache_keys: dict = None
):
    """
    Unified registration for both local and cross-service models.

    Args:
        model_name: Model name (e.g., "Customer", "CustomerTransactionEntry")
        conditions: Invalidation conditions (events, filters, etc.)
        cache_rule_instance: For local models, the cache rule instance
        source_service: For cross-service models, the source service name
        api_path: For cross-service models, the API path to invalidate
        scoped_cache_keys: For cross-service models, cache key structure

    Flow:
        1. Check if model exists in THIS service using ModelDiscovery
        2. If not found, skip registration (model not in this service)
        3. If found, get model class from ModelDiscovery
        4. Determine if this is local or cross-service registration
        5. Add to appropriate registry
        6. Register SQLAlchemy event listener (once per model+events)

    Returns:
        bool: True if registered, False if skipped
    """
    from wedeliver_core_plus.helpers.model_discovery import ModelDiscovery

    discovery = ModelDiscovery()

    # Check if model exists in THIS service
    if not discovery.has_model(model_name):
        cache_debug(f"Model '{model_name}' not found in this service, skipping registration")
        return False

    # Get model class
    try:
        model_class = discovery.get_model_class(model_name)
    except ValueError as e:
        cache_error(f"Failed to get model class for '{model_name}': {e}")
        return False

    events = conditions.get("events", [])
    is_local = (cache_rule_instance is not None)

    # Add to appropriate registry
    if is_local:
        # Local registration (from BaseCacheRule)
        _register_local_model(model_class, events, cache_rule_instance, conditions)
    else:
        # Cross-service registration (from MicroFetcher metadata)
        _register_cross_service_model(
            model_class,
            events,
            source_service,
            api_path,
            scoped_cache_keys,
            conditions
        )

    # Register SQLAlchemy listener (once per model+events)
    _register_sqlalchemy_listener(model_class, events)

    return True


def _register_local_model(model_class, events, cache_rule_instance, conditions):
    """Add local cache rule to unified registry."""
    if model_class not in _unified_registry:
        _unified_registry[model_class] = {
            "local_rules": [],
            "cross_service_rules": []
        }

    # Check for duplicates
    existing_paths = [rule.path for rule in _unified_registry[model_class]["local_rules"]]
    if cache_rule_instance.path not in existing_paths:
        _unified_registry[model_class]["local_rules"].append(cache_rule_instance)
        cache_debug(f"Registered local rule: {cache_rule_instance.path} → {model_class.__name__}")


def _register_cross_service_model(model_class, events, source_service, api_path, scoped_cache_keys, conditions):
    """Add cross-service rule to unified registry."""
    if model_class not in _unified_registry:
        _unified_registry[model_class] = {
            "local_rules": [],
            "cross_service_rules": []
        }

    # Check for duplicates
    existing = [
        r for r in _unified_registry[model_class]["cross_service_rules"]
        if r["source_service"] == source_service and r["api_path"] == api_path
    ]

    if not existing:
        _unified_registry[model_class]["cross_service_rules"].append({
            "source_service": source_service,
            "api_path": api_path,
            "scoped_cache_keys": scoped_cache_keys,
            "conditions": conditions
        })
        cache_debug(f"Registered cross-service rule: {source_service}:{api_path} → {model_class.__name__}")


def _register_sqlalchemy_listener(model_class, events):
    """Register SQLAlchemy event listener (once per model+events)."""
    registry_key = (model_class, tuple(events))

    if registry_key not in _listeners_registered:
        for ev in events:
            event.listen(model_class, ev, unified_invalidation_handler)

        _listeners_registered.add(registry_key)
        cache_debug(f"Registered SQLAlchemy listener: {model_class.__name__} → {events}")


# ============================================================================
# UNIFIED INVALIDATION HANDLER
# ============================================================================

def unified_invalidation_handler(mapper, connection, target):
    """
    Unified handler for both local and cross-service cache invalidation.

    This handler is called by SQLAlchemy events but immediately submits
    work to a background thread pool to avoid blocking the database transaction.

    Flow:
        1. SQLAlchemy event fires (after_update, after_insert, etc.)
        2. This handler is called synchronously
        3. Work is submitted to thread pool (returns immediately)
        4. Database transaction continues without blocking
        5. Background thread processes invalidation asynchronously

    Args:
        mapper: SQLAlchemy mapper
        connection: Database connection
        target: Model instance that triggered the event
    """
    # Check if cache is enabled - skip if disabled
    if not is_cache_enabled():
        return

    model_class = target.__class__

    if model_class not in _unified_registry:
        return  # No rules registered for this model

    # Submit to thread pool for async processing (non-blocking)
    # This returns immediately - database transaction continues
    pool = _get_event_pool()
    pool.submit(_async_invalidation_worker, model_class, target)


def _async_invalidation_worker(model_class, target):
    """
    Background worker that processes cache invalidation asynchronously.

    This runs in a background thread from the event pool.
    All exceptions are caught to prevent affecting the database transaction.

    Args:
        model_class: Model class that triggered the event
        target: Model instance that triggered the event
    """
    try:
        registry = _unified_registry[model_class]

        # Handle local cache rules
        for cache_rule in registry["local_rules"]:
            try:
                _handle_local_invalidation(cache_rule, target)
            except Exception as e:
                # Catch exceptions per cache rule to continue processing other rules
                cache_error(f"Local invalidation failed for {cache_rule.path}: {e}")

        # Handle cross-service cache rules
        for cross_rule in registry["cross_service_rules"]:
            try:
                _handle_cross_service_invalidation(cross_rule, target)
            except Exception as e:
                # Catch exceptions per cross-service rule
                cache_error(f"Cross-service invalidation failed: {e}")

    except Exception as e:
        # Catch all exceptions to prevent affecting DB transaction
        cache_error(f"Invalidation worker failed for {model_class.__name__}: {e}")


def _handle_local_invalidation(cache_rule, target):
    """Handle invalidation for local cache rule."""
    model_name = target.__class__.__name__

    # Find conditions for this model
    conditions = None
    for key, config in cache_rule.model_invalidation_conditions.items():
        if key == model_name:
            conditions = config
            break

    if not conditions:
        return  # This cache rule doesn't use this model

    cache_debug(f"Evaluating local invalidation for {cache_rule.path} triggered by {model_name}")

    # Evaluate conditions (shared logic)
    result = evaluate_invalidation_conditions(
        target,
        conditions,
        cache_rule.scoped_cache_keys,
        service_name=cache_rule._service_name,
        api_path=cache_rule.path
    )

    if result is None:
        return  # Conditions not met

    # Invalidate cache
    if result.get("__invalidate_all__"):
        cache_debug(f"Invalidating all cache for {cache_rule.path} (scope: *)")
        cache_rule.invalidate_by_path()
    else:
        main_keys = result.get("main_cache_keys", [])
        func_keys = result.get("function_cache_keys", [])

        if main_keys or func_keys:
            all_keys = main_keys + func_keys
            cache_debug(f"Invalidating {len(main_keys)} main + {len(func_keys)} function cache keys")

            # Check if async invalidation is enabled
            try:
                from flask import current_app
                async_invalidation = current_app.config.get('CACHE_ASYNC_INVALIDATION', True)
            except RuntimeError:
                async_invalidation = True

            if async_invalidation:
                cache_rule._invalidate_async(all_keys)
            else:
                cache_rule._batch_delete_keys(all_keys)
        else:
            # No specific keys found, fallback to path invalidation
            cache_debug(f"No specific cache keys found, invalidating all cache for {cache_rule.path}")
            cache_rule.invalidate_by_path()


def _handle_cross_service_invalidation(cross_rule, target):
    """Handle invalidation for cross-service cache rule."""
    source_service = cross_rule["source_service"]
    api_path = cross_rule["api_path"]
    scoped_cache_keys = cross_rule["scoped_cache_keys"]
    conditions = cross_rule["conditions"]

    cache_debug(f"Evaluating cross-service invalidation for {source_service}:{api_path}")

    # Evaluate conditions (shared logic)
    result = evaluate_invalidation_conditions(
        target,
        conditions,
        scoped_cache_keys,
        service_name=source_service,
        api_path=api_path
    )

    if result is None:
        return  # Conditions not met

    # Invalidate remote cache
    if result.get("__invalidate_all__"):
        _invalidate_remote_cache_by_path(source_service, api_path)
    else:
        main_keys = result.get("main_cache_keys", [])
        func_keys = result.get("function_cache_keys", [])

        if main_keys or func_keys:
            all_keys = main_keys + func_keys
            _invalidate_remote_cache_keys(all_keys)


def _invalidate_remote_cache_by_path(service_name, api_path):
    """Invalidate all cache for a specific path in remote service."""
    from wedeliver_core_plus.helpers.caching.cache_invalidation_registry import invalidate_cache_by_pattern

    pattern = f"{service_name}:{api_path}:*"
    cache_debug(f"Invalidating remote cache by pattern: {pattern}")
    invalidate_cache_by_pattern(pattern)


def _invalidate_remote_cache_keys(cache_keys):
    """Invalidate specific cache keys in remote service."""
    from wedeliver_core_plus.helpers.caching.valkey_redis_utils import BaseCacheRule

    if not BaseCacheRule._redis_client:
        cache_warning("Redis client not available for cross-service invalidation")
        return

    cache_debug(f"Invalidating {len(cache_keys)} remote cache keys")

    try:
        pipeline = BaseCacheRule._redis_client.pipeline()
        for key in cache_keys:
            pipeline.delete(key)
        pipeline.execute()
        cache_debug(f"Successfully invalidated {len(cache_keys)} remote cache keys")
    except Exception as e:
        cache_warning(f"Error invalidating remote cache keys: {e}")


def _extract_scoped_key_values_from_redis(service_name, api_path, scoped_key):
    """
    Scan Redis cache keys and extract all values for a specific scoped key along with their cache keys.

    Args:
        service_name: Service name (e.g., "thrivve-service")
        api_path: API path (e.g., "/core/api/v1/mobile/home/messages")
        scoped_key: The scoped key to extract (e.g., "customer_id")

    Returns:
        list: List of (value, cache_key) tuples

    Example:
        Redis keys:
            thrivve-service/core/api/v1/mobile/home/messages:customer_id=123:app_version=app_version:hash1
            thrivve-service/core/api/v1/mobile/home/messages:customer_id=456:app_version=app_version:hash2

        _extract_scoped_key_values_from_redis("thrivve-service", "/core/api/v1/mobile/home/messages", "customer_id")
        → [(123, "thrivve-service/core/.../messages:customer_id=123:..."),
            (456, "thrivve-service/core/.../messages:customer_id=456:...")]
    """
    from wedeliver_core_plus.helpers.caching.valkey_redis_utils import BaseCacheRule
    import re

    if not BaseCacheRule._redis_client:
        cache_warning("Redis client not available for scanning keys")
        return []

    # Build pattern to match all cache keys for this endpoint
    pattern = f"{service_name}:{api_path}:*"

    results = []

    try:
        # Scan for all cache keys matching the pattern
        for key in BaseCacheRule._redis_client.scan_iter(match=pattern):
            try:
                # Decode key if it's bytes
                cache_key = key.decode('utf-8') if isinstance(key, bytes) else key

                # Parse the key to extract scoped key values
                # Format: service_name/path:key1=val1:key2=val2:hash
                # Extract the tags portion (between path and final hash)
                parts = cache_key.split(':')

                # Find the scoped key in the tags
                for part in parts:
                    if '=' in part and part.startswith(f"{scoped_key}="):
                        # Extract value after the equals sign
                        value_str = part.split('=', 1)[1]

                        # Try to parse the value (handle different types)
                        try:
                            # Try to parse as JSON (handles lists, numbers, etc.)
                            value = json.loads(value_str)
                        except (json.JSONDecodeError, ValueError):
                            # If not JSON, use as string
                            value = value_str

                        # Store (value, cache_key) tuple
                        results.append((value, cache_key))
                        break

            except Exception as e:
                cache_warning(f"Error parsing cache key {key}: {e}")
                continue

    except Exception as e:
        cache_warning(f"Error scanning Redis keys: {e}")

    cache_debug(f"Extracted {len(results)} cache keys for '{scoped_key}' from Redis")
    return results


def _apply_operator(actual_value, operator, expected_value, service_name=None, api_path=None, scoped_key=None):
    """
    Apply comparison operator between actual and expected values.

    Args:
        actual_value: Value from the model instance
        operator: Comparison operator (=, !=, in, >, <)
        expected_value: Expected value to compare against
        service_name: Service name (for Redis-aware operators)
        api_path: API path (for Redis-aware operators)
        scoped_key: Scoped key name (for Redis-aware operators)

    Returns:
        tuple: (bool, list) - (condition_passed, matching_cache_keys)
            - condition_passed: True if condition passes, False otherwise
            - matching_cache_keys: List of cache keys that match (for Redis-aware operators)

    Examples:
        _apply_operator("app_version", "=", "app_version") → (True, [])
        _apply_operator(123, "in", "customer_id", "thrivve-service", "/api/path", "customer_id")
        → (True, ["service/path:customer_id=123:...", ...])
    """
    matching_keys = []

    if operator == "=":
        # For equality with Redis context, find matching cache keys
        if service_name and api_path and scoped_key:
            cached_items = _extract_scoped_key_values_from_redis(service_name, api_path, scoped_key)
            for value, cache_key in cached_items:
                if value == actual_value:
                    matching_keys.append(cache_key)
            return (len(matching_keys) > 0, matching_keys)
        return (actual_value == expected_value, [])

    elif operator == "!=":
        return (actual_value != expected_value, [])

    elif operator == "in":
        # Special handling for 'in' operator
        if isinstance(expected_value, str) and service_name and api_path and scoped_key:
            # Redis-aware: scan cache keys and extract values
            cached_items = _extract_scoped_key_values_from_redis(service_name, api_path, scoped_key)

            # Check if actual_value is in any of the cached values
            # Handle both scalar values and list values (flatten lists)
            for value, cache_key in cached_items:
                if isinstance(value, list):
                    # Flatten list and check membership
                    if actual_value in value:
                        matching_keys.append(cache_key)
                else:
                    # Direct comparison
                    if actual_value == value:
                        matching_keys.append(cache_key)

            passed = len(matching_keys) > 0
            cache_debug(f"'in' operator: found {len(matching_keys)} matching cache keys")
            return (passed, matching_keys)

        elif isinstance(expected_value, (list, tuple, set)):
            # Traditional list check (no cache keys)
            return (actual_value in expected_value, [])
        else:
            cache_warning(f"'in' operator requires list or Redis context. Got: {type(expected_value)}")
            return (False, [])

    elif operator == ">":
        return (actual_value > expected_value, [])
    elif operator == "<":
        return (actual_value < expected_value, [])
    else:
        cache_warning(f"Unknown operator: {operator}")
        return (False, [])


def _resolve_scoped_value(scoped_key, scoped_cache_keys, api_params=None):
    """
    Resolve a scoped key to its actual value.

    Args:
        scoped_key: Key from filter (e.g., "customer_id", "app_version")
        scoped_cache_keys: Dict defining cache key structure
        api_params: Optional dict of API parameters (for runtime resolution)

    Returns:
        Resolved value or the scoped_key itself if not found

    Examples:
        scoped_cache_keys = {
            "customer_id": "api_params",
            "app_version": "__static_value__('app_version')"
        }

        _resolve_scoped_value("app_version", scoped_cache_keys) → "app_version"
        _resolve_scoped_value("customer_id", scoped_cache_keys, {"customer_id": 123}) → 123
    """
    import re

    if scoped_key not in scoped_cache_keys:
        # Not in scoped keys, return as-is (might be a literal value)
        return scoped_key

    scoped_value = scoped_cache_keys[scoped_key]

    # Handle static value pattern
    if isinstance(scoped_value, str) and scoped_value.startswith("__static_value__"):
        match = re.match(r"__static_value__\(['\"](.+?)['\"]\)", scoped_value)
        if match:
            return match.group(1)

    # Handle api_params
    if scoped_value == "api_params" and api_params:
        return api_params.get(scoped_key)

    # Handle function call (return the scoped_key for now, will be resolved later)
    if isinstance(scoped_value, str) and "__function_call__" in scoped_value:
        return scoped_key  # Will be resolved during cache key generation

    return scoped_value


def _evaluate_filter(target, filter_tuple, scoped_cache_keys, api_params=None, service_name=None, api_path=None):
    """
    Evaluate a single filter condition.

    Args:
        target: SQLAlchemy model instance
        filter_tuple: Tuple of (field, operator, scoped_key)
        scoped_cache_keys: Dict defining cache key structure
        api_params: Optional dict of API parameters
        service_name: Service name (for Redis-aware operators)
        api_path: API path (for Redis-aware operators)

    Returns:
        tuple: (bool, list) - (filter_passed, matching_cache_keys)

    Examples:
        filter_tuple = ("group_type", "=", "app_version")
        scoped_cache_keys = {"app_version": "__static_value__('app_version')"}
        target.group_type = "app_version"
        → Returns (True, ["service/path:app_version=app_version:..."])
    """
    field, operator, scoped_key = filter_tuple

    # Get actual value from target
    if not hasattr(target, field):
        cache_warning(
            f"Filter field '{field}' not found on "
            f"{target.__class__.__name__}. Filter fails."
        )
        return (False, [])

    actual_value = getattr(target, field)

    # Resolve expected value from scoped_cache_keys
    expected_value = _resolve_scoped_value(scoped_key, scoped_cache_keys, api_params)

    # Apply operator (pass Redis context)
    passed, matching_keys = _apply_operator(
        actual_value,
        operator,
        expected_value,
        service_name=service_name,
        api_path=api_path,
        scoped_key=scoped_key
    )

    if passed:
        cache_debug(
            f"Filter passed for {target.__class__.__name__}: "
            f"{field} {operator} {expected_value} (actual: {actual_value}), "
            f"matched {len(matching_keys)} cache keys"
        )
    else:
        cache_debug(
            f"Filter failed for {target.__class__.__name__}: "
            f"{field} {operator} {expected_value} (actual: {actual_value})"
        )

    return (passed, matching_keys)


def _evaluate_filters_and(target, filters, scoped_cache_keys, api_params=None, service_name=None, api_path=None):
    """
    Evaluate multiple filters with AND logic (all must pass).

    Args:
        target: SQLAlchemy model instance
        filters: List of filter tuples [(field, operator, scoped_key), ...]
        scoped_cache_keys: Dict defining cache key structure
        api_params: Optional dict of API parameters
        service_name: Service name (for Redis-aware operators)
        api_path: API path (for Redis-aware operators)

    Returns:
        tuple: (bool, list) - (all_passed, matching_cache_keys)
            Returns intersection of cache keys (keys that match ALL filters)

    Examples:
        filters = [
            ("group_type", "=", "app_version"),
            ("status", "=", "active")
        ]
        → Returns (True, [matching_keys]) only if both conditions are met
    """
    if not filters:
        return (True, [])  # No filters means pass

    all_matching_keys = None  # Will hold intersection of keys

    for filter_tuple in filters:
        passed, matching_keys = _evaluate_filter(target, filter_tuple, scoped_cache_keys, api_params, service_name, api_path)

        if not passed:
            return (False, [])  # One filter failed, AND logic fails

        # Collect intersection of matching keys
        if all_matching_keys is None:
            all_matching_keys = set(matching_keys)
        else:
            all_matching_keys = all_matching_keys.intersection(set(matching_keys))

    return (True, list(all_matching_keys) if all_matching_keys else [])


def _evaluate_filters_or(target, or_filters, scoped_cache_keys, api_params=None, service_name=None, api_path=None):
    """
    Evaluate multiple filters with OR logic (at least one must pass).

    Args:
        target: SQLAlchemy model instance
        or_filters: List of filter tuples [(field, operator, scoped_key), ...]
        scoped_cache_keys: Dict defining cache key structure
        api_params: Optional dict of API parameters
        service_name: Service name (for Redis-aware operators)
        api_path: API path (for Redis-aware operators)

    Returns:
        tuple: (bool, list) - (any_passed, matching_cache_keys)
            Returns union of cache keys (keys that match ANY filter)

    Examples:
        or_filters = [
            ("status", "=", "active"),
            ("status", "=", "pending")
        ]
        → Returns (True, [matching_keys]) if status is either "active" or "pending"
    """
    if not or_filters:
        return (True, [])  # No filters means pass

    all_matching_keys = set()
    any_passed = False

    for filter_tuple in or_filters:
        passed, matching_keys = _evaluate_filter(target, filter_tuple, scoped_cache_keys, api_params, service_name, api_path)

        if passed:
            any_passed = True
            all_matching_keys.update(matching_keys)

    return (any_passed, list(all_matching_keys))


def evaluate_invalidation_conditions(target, conditions, scoped_cache_keys, api_params=None, service_name=None, api_path=None):
    """
    Evaluate invalidation conditions and return actual cache keys to invalidate.

    This is the main function used by both local and cross-service invalidation.

    Args:
        target: SQLAlchemy model instance that triggered the event
        conditions: Dict with filter configuration
            {
                "filters": [(field, operator, scoped_key), ...],  # AND logic
                "or_filters": [(field, operator, scoped_key), ...],  # OR logic
                "invalidate_scope": "*" or None  # "*" = invalidate all (path-based)
            }
        scoped_cache_keys: Dict defining cache key structure
            {
                "customer_id": "api_params",
                "app_version": "__static_value__('app_version')",
                "contract_ids": "__function_call__(get_contract_ids, [customer_id:scoped_key])"
            }
        api_params: Optional dict of API parameters (for runtime resolution)
        service_name: Service name (for Redis scanning)
        api_path: API path (for Redis scanning)

    Returns:
        dict: {"main_cache_keys": [...], "function_cache_keys": [...]}
        dict: {"__invalidate_all__": True} if invalidate_scope = "*"
        None: If conditions not met (skip invalidation)

    Examples:
        # Returns actual cache keys to invalidate
        conditions = {"filters": [("id", "=", "customer_id")]}
        → Returns {
            "main_cache_keys": ["service/path:customer_id=123:..."],
            "function_cache_keys": ["service:function_cache:get_contract_ids:customer_id=123"]
        }

        # Invalidate all (path-based)
        conditions = {"invalidate_scope": "*"}
        → Returns {"__invalidate_all__": True}
    """
    import re

    # Step 1: Evaluate filters (AND logic)
    filters = conditions.get("filters", [])
    and_passed, and_matching_keys = _evaluate_filters_and(target, filters, scoped_cache_keys, api_params, service_name, api_path)

    if not and_passed:
        cache_debug(f"AND filters failed for {target.__class__.__name__}, skipping invalidation")
        return None  # Conditions not met

    # Step 2: Evaluate or_filters (OR logic)
    or_filters = conditions.get("or_filters", [])
    or_matching_keys = []

    if or_filters:
        or_passed, or_matching_keys = _evaluate_filters_or(target, or_filters, scoped_cache_keys, api_params, service_name, api_path)
        if not or_passed:
            cache_debug(f"OR filters failed for {target.__class__.__name__}, skipping invalidation")
            return None  # Conditions not met

    # Step 3: Check invalidate_scope
    invalidate_scope = conditions.get("invalidate_scope")
    if invalidate_scope == "*":
        cache_debug(f"Invalidate scope is '*', will invalidate all cache for path")
        return {"__invalidate_all__": True}

    # Step 4: Collect all matching cache keys
    # Combine AND and OR matching keys (intersection for AND, union for OR)
    if and_matching_keys and or_matching_keys:
        # Both AND and OR filters exist - take intersection
        main_cache_keys = list(set(and_matching_keys).intersection(set(or_matching_keys)))
    elif and_matching_keys:
        main_cache_keys = and_matching_keys
    elif or_matching_keys:
        main_cache_keys = or_matching_keys
    else:
        main_cache_keys = []

    # Step 5: Find related function cache keys
    # For each main cache key, extract scoped params and find matching function caches
    function_cache_keys = []

    if main_cache_keys and service_name:
        for cache_key in main_cache_keys:
            # Parse cache key to extract scoped params
            # Format: service_name/path:key1=val1:key2=val2:hash
            parts = cache_key.split(':')
            cache_params = {}

            for part in parts:
                if '=' in part:
                    key, value_str = part.split('=', 1)
                    try:
                        value = json.loads(value_str)
                    except (json.JSONDecodeError, ValueError):
                        value = value_str
                    cache_params[key] = value

            # Find function calls in scoped_cache_keys and build their cache keys
            for scoped_key, source in scoped_cache_keys.items():
                if isinstance(source, str) and "__function_call__" in source:
                    # Parse function call
                    match = re.match(r"__function_call__\((\w+),\s*\[([^\]]+)\]\)", source)
                    if match:
                        func_name = match.group(1)
                        param_specs = [p.strip() for p in match.group(2).split(",")]

                        # Extract only scoped params
                        func_params = {}
                        for param_spec in param_specs:
                            if ":scoped_key" in param_spec:
                                param_name = param_spec.replace(":scoped_key", "").strip()
                                if param_name in cache_params:
                                    func_params[param_name] = cache_params[param_name]

                        # Build function cache key
                        if func_params:
                            from wedeliver_core_plus.helpers.caching.cache_invalidation_registry import format_value_for_cache_key
                            param_parts = []
                            for key, value in sorted(func_params.items()):
                                formatted_value = format_value_for_cache_key(value)
                                param_parts.append(f"{key}={formatted_value}")

                            param_str = ":".join(param_parts)
                            func_cache_key = f"{service_name}:function_cache:{func_name}:{param_str}"
                            function_cache_keys.append(func_cache_key)

    cache_debug(f"Found {len(main_cache_keys)} main cache keys and {len(function_cache_keys)} function cache keys to invalidate")

    return {
        "main_cache_keys": main_cache_keys,
        "function_cache_keys": function_cache_keys
    }


def _find_matching_function_cache_keys(func_name, target_field_value, service_name):
    """
    Find function cache keys where the cached result contains target_field_value.

    This function scans Redis for all function cache keys matching the pattern
    and checks if the cached result contains the target field value.

    Args:
        func_name: Function name (e.g., "get_contract_ids")
        target_field_value: Value to search for (e.g., 101)
        service_name: Service name for cache key prefix

    Returns:
        list: List of (cache_key, api_params_dict) tuples that match

    Example:
        func_name = "get_contract_ids"
        target_field_value = 101
        service_name = "thrivve-service"

        Scans: thrivve-service:function_cache:get_contract_ids:*
        Finds: thrivve-service:function_cache:get_contract_ids:customer_id=123
        Cached result: [101, 102, 103]
        Match: 101 in [101, 102, 103] ✓
        Returns: [("thrivve-service:function_cache:get_contract_ids:customer_id=123", {"customer_id": 123})]
    """
    from wedeliver_core_plus.helpers.caching.valkey_redis_utils import BaseCacheRule
    import json

    if not BaseCacheRule._redis_client:
        return []

    # Pattern: {service_name}:function_cache:{func_name}:*
    pattern = f"{service_name}:function_cache:{func_name}:*"

    matching_keys = []

    try:
        # Scan for all function cache keys
        for key in BaseCacheRule._redis_client.scan_iter(match=pattern):
            try:
                # Decode key if it's bytes
                if isinstance(key, bytes):
                    key = key.decode('utf-8')

                # Get cached result
                data = BaseCacheRule._redis_client.get(key)
                if not data:
                    continue

                # Decode data if it's bytes
                if isinstance(data, bytes):
                    data = data.decode('utf-8')

                result = json.loads(data)

                # Check if target_field_value is in result
                match_found = False
                if isinstance(result, list):
                    if target_field_value in result:
                        match_found = True
                else:
                    if target_field_value == result:
                        match_found = True

                if match_found:
                    # Extract api_params from key
                    api_params = _parse_api_params_from_cache_key(key, func_name, service_name)
                    matching_keys.append((key, api_params))
                    cache_debug(f"Found matching function cache: {key} with result containing {target_field_value}")

            except Exception as e:
                cache_warning(f"Error checking function cache key {key}: {e}")
                continue
    except Exception as e:
        cache_warning(f"Error scanning function cache keys: {e}")

    return matching_keys


def _parse_api_params_from_cache_key(cache_key, func_name, service_name):
    """
    Parse api_params from function cache key.

    Args:
        cache_key: Function cache key
        func_name: Function name
        service_name: Service name

    Returns:
        dict: Parsed api_params

    Example:
        Key: "thrivve-service:function_cache:get_contract_ids:customer_id=123:country=sa"
        Returns: {"customer_id": 123, "country": "sa"}
    """
    # Remove prefix: service:function_cache:func_name:
    prefix = f"{service_name}:function_cache:{func_name}:"
    if not cache_key.startswith(prefix):
        return {}

    params_str = cache_key[len(prefix):]

    # Parse param1=value1:param2=value2
    params = {}
    for part in params_str.split(":"):
        if "=" in part:
            key, value = part.split("=", 1)
            # Try to convert to int if possible
            try:
                params[key] = int(value)
            except ValueError:
                params[key] = value

    return params


def format_value_for_cache_key(value):
    """
    Format a value for use in cache keys.
    
    Shared utility for consistent value formatting across local and cross-service invalidation.
    
    Args:
        value: The value to format (int, str, bool, dict, list, etc.)
    
    Returns:
        str: Formatted value suitable for cache key
    """
    if isinstance(value, (int, str)):
        return str(value)
    else:
        return json.dumps(value)


def build_cache_key_pattern(service_name, api_path, invalidation_params):
    """
    Build a cache key pattern for invalidation.
    
    Shared utility to ensure consistent pattern building.
    
    Args:
        service_name: Service name (e.g., "thrivve-service")
        api_path: API path (e.g., "/finance/api/v1/me/balance")
        invalidation_params: Parameters for granular invalidation (e.g., {"customer_id": 123})
    
    Returns:
        str: Cache key pattern for Redis SCAN
    
    Examples:
        build_cache_key_pattern("thrivve-service", "/api/balance", {"customer_id": 123})
        Returns: "thrivve-service:/api/balance:customer_id=123:*"
        
        build_cache_key_pattern("thrivve-service", "/api/balance", {})
        Returns: "thrivve-service:/api/balance:*"
    """
    if invalidation_params:
        tags = []
        for api_param, value in invalidation_params.items():
            formatted_value = format_value_for_cache_key(value)
            tags.append(f"{api_param}={formatted_value}")
        
        tag_str = ":".join(tags)
        return f"{service_name}:{api_path}:{tag_str}:*"
    else:
        # Path-based invalidation (all keys for this endpoint)
        return f"{service_name}:{api_path}:*"


def invalidate_cache_by_pattern(redis_client, pattern):
    """
    Invalidate cache entries matching a pattern.
    
    Shared utility for cache invalidation via Redis SCAN.
    
    Args:
        redis_client: Redis/Valkey client instance
        pattern: Redis key pattern (e.g., "service:/path:customer_id=123:*")
    
    Returns:
        int: Number of keys deleted
    """
    if not redis_client:
        cache_warning("Redis client not available for invalidation")
        return 0
    
    try:
        deleted = 0
        for key in redis_client.scan_iter(pattern):
            redis_client.delete(key)
            deleted += 1
        
        if deleted > 0:
            cache_debug(f"Invalidated {deleted} key(s) matching pattern: {pattern}")
        
        return deleted
        
    except Exception as e:
        cache_warning(f"Cache invalidation failed for pattern {pattern}: {e}")
        return 0


def _model_exists(model_path):
    """
    Check if a model path exists before attempting to import.

    This allows graceful degradation when cache metadata references models
    that don't exist in the current service.

    Args:
        model_path: Full model path (e.g., "app.models.core.Customer")

    Returns:
        bool: True if model can be imported, False otherwise
    """
    import importlib

    try:
        module_path, class_name = model_path.rsplit(".", 1)
        module = importlib.import_module(module_path)
        return hasattr(module, class_name)
    except (ImportError, ValueError, AttributeError):
        return False


# ============================================================================
# DEPRECATED FUNCTIONS - Kept for backward compatibility, will be removed
# ============================================================================

def register_cross_service_invalidation_batch(
    source_service,
    api_path,
    scoped_cache_keys,
    models
):
    """
    DEPRECATED: Use register_model_invalidation() instead.

    This function is kept for backward compatibility only.
    It will be removed in a future version.

    Register cross-service cache invalidation for multiple models.
    Called automatically when MicroFetcher receives cache metadata.

    Args:
        source_service: Service that owns the cache (e.g., "thrivve-service")
        api_path: API path to invalidate (e.g., "/finance/api/v1/me/balance")
        scoped_cache_keys: Cache key structure (e.g., {"customer_id": "api_params"})
        models: Dict of model paths and their invalidation conditions

    Example models structure:
        {
            "app.models.core.Customer": {
                "events": ["after_update"],
                "filters": [("id", "=", "customer_id")]
            },
            "app.models.settings.WalletSettings": {
                "events": ["after_update", "after_insert"],
                "filters": [("customer_id", "=", "customer_id")],
                "invalidate_scope": "*"
            }
        }

    Returns:
        int: Number of models successfully registered
    """
    cache_warning("register_cross_service_invalidation_batch() is deprecated, use register_model_invalidation() instead")
    import importlib

    registered_count = 0
    skipped_count = 0

    for model_path, conditions in models.items():
        # Check if model exists before importing
        if not _model_exists(model_path):
            cache_debug(f"Skipping cross-service registration: Model '{model_path}' not found in this service")
            skipped_count += 1
            continue

        try:
            # Import the model class
            module_path, class_name = model_path.rsplit(".", 1)
            module = importlib.import_module(module_path)
            model_class = getattr(module, class_name)

            events = conditions.get("events", [])
            filters = conditions.get("filters", [])
            or_filters = conditions.get("or_filters", [])
            invalidate_scope = conditions.get("invalidate_scope")

            # Add to registry
            if model_path not in _cross_service_registry:
                _cross_service_registry[model_path] = []

            # Check if already registered (avoid duplicates)
            existing = [
                r for r in _cross_service_registry[model_path]
                if r["source_service"] == source_service and r["api_path"] == api_path
            ]

            if existing:
                # print(f"[Cache] Cross-service rule already registered: {source_service}:{api_path} → {model_path}")
                continue

            # Add new rule
            _cross_service_registry[model_path].append({
                "source_service": source_service,
                "api_path": api_path,
                "scoped_cache_keys": scoped_cache_keys,
                "conditions": {
                    "filters": filters,
                    "or_filters": or_filters,
                    "invalidate_scope": invalidate_scope
                },
                "events": events
            })

            # Register SQLAlchemy listener (once per model+events)
            registry_key = (model_class, tuple(events))
            if registry_key not in _cross_service_listeners_registered:
                for ev in events:
                    event.listen(model_class, ev, _cross_service_invalidation_handler)

                _cross_service_listeners_registered.add(registry_key)
                cache_debug(f"Registered cross-service listener: {model_path} → {events}")

            registered_count += 1

        except Exception as e:
            cache_warning(f"Failed to register cross-service invalidation for {model_path}: {e}")
            skipped_count += 1
            continue

    if registered_count > 0:
        cache_debug(f"Registered {registered_count} cross-service invalidation rule(s) for {source_service}:{api_path}")
    if skipped_count > 0:
        cache_debug(f"Skipped {skipped_count} model(s) not present in this service")

    return registered_count


def _cross_service_invalidation_handler(mapper, connection, target):
    """
    DEPRECATED: Use unified_invalidation_handler() instead.

    Global handler for cross-service cache invalidation.
    Triggered when a model registered for cross-service invalidation changes.

    Args:
        mapper: SQLAlchemy mapper
        connection: Database connection
        target: Model instance that triggered the event
    """
    model_path = f"{target.__class__.__module__}.{target.__class__.__name__}"

    if model_path not in _cross_service_registry:
        return

    # Get all cache rules registered for this model
    for rule_config in _cross_service_registry[model_path]:
        source_service = rule_config["source_service"]
        api_path = rule_config["api_path"]
        scoped_cache_keys = rule_config["scoped_cache_keys"]
        conditions = rule_config["conditions"]

        cache_debug(f"Evaluating cross-service invalidation for {source_service}:{api_path}")

        # Evaluate invalidation conditions
        result = evaluate_invalidation_conditions(
            target,
            conditions,
            scoped_cache_keys,
            service_name=source_service,
            api_path=api_path
        )

        if result is None:
            # Conditions not met, skip invalidation
            cache_debug(f"Conditions not met for {source_service}:{api_path}, skipping cross-service invalidation")
            continue

        if result.get("__invalidate_all__"):
            # Invalidate all cache for this path
            cache_debug(f"Invalidating all cache for {source_service}:{api_path} (scope: *)")
            _invalidate_remote_cache_by_path(source_service, api_path)
        else:
            # Invalidate specific cache keys
            main_cache_keys = result.get("main_cache_keys", [])
            function_cache_keys = result.get("function_cache_keys", [])

            if main_cache_keys or function_cache_keys:
                cache_debug(f"Invalidating {len(main_cache_keys)} main cache keys and {len(function_cache_keys)} function cache keys for {source_service}:{api_path}")
                _invalidate_remote_cache_keys(main_cache_keys + function_cache_keys)
            else:
                # No specific keys found, fallback to path invalidation
                cache_debug(f"No specific cache keys found, invalidating all cache for {source_service}:{api_path}")
                _invalidate_remote_cache_by_path(source_service, api_path)


def _invalidate_remote_cache_keys(cache_keys):
    """
    DEPRECATED: This is the old implementation, kept for backward compatibility.

    Invalidate specific cache keys in remote service by directly accessing Valkey.

    Since all services share the same Valkey instance, we can directly
    delete cache keys without making HTTP requests.

    Args:
        cache_keys: List of cache keys to delete
    """
    # Import here to avoid circular dependency
    from wedeliver_core_plus.helpers.caching.valkey_redis_utils import BaseCacheRule
    from redis import exceptions as redis_exceptions

    if not BaseCacheRule._redis_client:
        cache_warning("Redis client not available for cross-service invalidation")
        return

    deleted_count = 0
    for cache_key in cache_keys:
        try:
            deleted = BaseCacheRule._redis_client.delete(cache_key)
            if deleted > 0:
                deleted_count += 1
                cache_debug(f"Deleted cache key: {cache_key}")
        except redis_exceptions.RedisError as e:
            cache_warning(f"Failed to delete cache key {cache_key}: {e}")

    if deleted_count > 0:
        cache_debug(f"Cross-service invalidation: Deleted {deleted_count} cache key(s)")


def _invalidate_remote_cache_by_path(source_service, api_path):
    """
    DEPRECATED: This is the old implementation, kept for backward compatibility.

    Invalidate all cache for a path in remote service (path-based invalidation).

    Args:
        source_service: Service name that owns the cache (e.g., "thrivve-service")
        api_path: API path to invalidate (e.g., "/finance/api/v1/me/balance")
    """
    # Import here to avoid circular dependency
    from wedeliver_core_plus.helpers.caching.valkey_redis_utils import BaseCacheRule

    if not BaseCacheRule._redis_client:
        cache_warning("Redis client not available for cross-service invalidation")
        return

    # Build pattern for all keys on this path
    pattern = f"{source_service}/{api_path}:*"

    # Invalidate using shared utility
    deleted = invalidate_cache_by_pattern(BaseCacheRule._redis_client, pattern)

    if deleted > 0:
        cache_debug(f"Cross-service path invalidation: Deleted {deleted} key(s) for {source_service}:{api_path}")


def get_cross_service_registry():
    """
    Get the current cross-service registry for debugging/monitoring.
    
    Returns:
        dict: Current registry state with registry and listener count
    """
    return {
        "registry": _cross_service_registry,
        "listeners_count": len(_cross_service_listeners_registered),
        "registered_models": list(_cross_service_registry.keys())
    }

