"""
Endobest Dashboard - Utility Functions Module

This module contains generic utility functions used throughout the Endobest Dashboard:
- HTTP client management (thread-safe)
- Nested data structure navigation with wildcard support
- Configuration path resolution (script vs PyInstaller)
- Thread position management for progress bars
- Filename generation utilities
"""

import os
import sys
import threading

import httpx

from eb_dashboard_constants import CONFIG_FOLDER_NAME


# ============================================================================
# GLOBAL VARIABLES (managed by main module)
# ============================================================================
thread_local_storage = threading.local()


def run_with_context(func, context, *args, **kwargs):
    """
    Wrapper to set thread-local context before running a function in a new thread.
    Useful for ThreadPoolExecutor where context is lost.
    """
    thread_local_storage.current_patient_context = context
    return func(*args, **kwargs)


# These will be set/accessed from the main module
httpx_clients = {}
_clients_lock = threading.Lock()
threads_list = []
_threads_list_lock = threading.Lock()


# ============================================================================
# HTTP CLIENT MANAGEMENT
# ============================================================================

def get_httpx_client() -> httpx.Client:
    """
    Get or create thread-local HTTP client.
    Keep-alive is disabled to avoid stale connections with load balancers.
    """
    global httpx_clients
    thread_id = threading.get_ident()
    
    with _clients_lock:
        if thread_id not in httpx_clients:
            # Create client with keep-alive disabled
            httpx_clients[thread_id] = httpx.Client(
                headers={"Connection": "close"}, # Explicitly request closing
                limits=httpx.Limits(max_keepalive_connections=0, max_connections=100)
            )
        return httpx_clients[thread_id]


def clear_httpx_client():
    """
    Removes the current thread's client from the cache.
    Ensures a fresh client (and socket pool) will be created on the next call.
    """
    global httpx_clients
    thread_id = threading.get_ident()
    with _clients_lock:
        if thread_id in httpx_clients:
            try:
                # Close the client before removing it
                httpx_clients[thread_id].close()
            except:
                pass
            del httpx_clients[thread_id]


def get_thread_position():
    """
    Get the position of the current thread in the threads list.
    Used for managing progress bar positions in multithreaded environment.
    """
    global threads_list
    thread_id = threading.get_ident()
    with _threads_list_lock:
        if thread_id not in threads_list:
            threads_list.append(thread_id)
            return len(threads_list) - 1
        else:
            return threads_list.index(thread_id)


# ============================================================================
# NESTED DATA NAVIGATION
# ============================================================================

def get_nested_value(data_structure, path, default=None):
    """
    Extracts a value from a nested structure of dictionaries and lists.
    Supports a wildcard '*' in the path to retrieve all elements from a list.

    Args:
        data_structure: The nested dict/list structure to navigate
        path: List of keys/indices to follow. Use '*' for list wildcard.
        default: Value to return if path not found

    Returns:
        The value at the end of the path, or default if not found

    Examples:
        get_nested_value({"a": {"b": 1}}, ["a", "b"]) -> 1
        get_nested_value({"items": [{"x": 1}, {"x": 2}]}, ["items", "*", "x"]) -> [1, 2]
    """
    if data_structure is None:
        return "$$$$ No Data"
    if not path:
        return default

    if "*" in path:
        wildcard_index = path.index("*")
        path_before = path[:wildcard_index]
        path_after = path[wildcard_index+1:]

        # Create a temporary function for non-wildcard path resolution
        def _get_simple_nested_value(ds, p, d):
            cl = ds
            for k in p:
                if isinstance(cl, dict):
                    cl = cl.get(k)
                elif isinstance(cl, list):
                    try:
                        if isinstance(k, int) and -len(cl) <= k < len(cl):
                            cl = cl[k]
                        else: return d
                    except (IndexError, TypeError): return d
                else: return d
                if cl is None: return d
            return cl

        base_level = _get_simple_nested_value(data_structure, path_before, default)

        if not isinstance(base_level, list):
            return default

        results = []
        for item in base_level:
            # For each item, recursively call to resolve the rest of the path
            value = get_nested_value(item, path_after, default)
            if value is not default and value != "$$$$ No Data":
                results.append(value)

        # Flatten the results by one level to handle multiple wildcards
        final_results = []
        for res in results:
            if isinstance(res, list):
                final_results.extend(res)
            else:
                final_results.append(res)

        return final_results

    # No wildcard, original logic (iterative)
    current_level = data_structure
    for key_or_index in path:
        if isinstance(current_level, dict):
            current_level = current_level.get(key_or_index)
            if current_level is None:
                return default
        elif isinstance(current_level, list):
            try:
                if isinstance(key_or_index, int) and -len(current_level) <= key_or_index < len(current_level):
                    current_level = current_level[key_or_index]
                else:
                    return default
            except (IndexError, TypeError):
                return default
        else:
            return default
    return current_level


# ============================================================================
# CONFIGURATION UTILITIES
# ============================================================================

def get_config_path():
    """
    Gets the correct path to the config folder.
    Works for both script execution and PyInstaller executable.

    Returns:
        Path to config folder
    """
    if getattr(sys, 'frozen', False):
        # Running as a PyInstaller bundle
        config_folder = CONFIG_FOLDER_NAME
        return os.path.join(sys._MEIPASS, config_folder)
    else:
        # Running as a script
        return CONFIG_FOLDER_NAME


def get_old_filename(current_filename, old_suffix="_old"):
    """Generate old backup filename from current filename.

    Example: "endobest_inclusions.json" → "endobest_inclusions_old.json"

    Args:
        current_filename: Current file name (e.g., "endobest_inclusions.json")
        old_suffix: Suffix to append before file extension (default: "_old")

    Returns:
        Old backup filename with suffix before extension
    """
    name, ext = os.path.splitext(current_filename)
    return f"{name}{old_suffix}{ext}"
