#  -*- coding: utf-8 -*-
#
#  Copyright (c) 2025 Featrix, Inc, All Rights Reserved
#
#  Proprietary and Confidential.  Unauthorized use, copying or dissemination
#  of these materials is strictly prohibited.
#
import os
import pickle
import sys
import json
from pathlib import Path
from datetime import datetime
from typing import Optional

def load_embedded_space(local_path: str, force_cpu: bool = True):
    """
    Load a pickled embedding space from disk.
    
    Args:
        local_path: Path to the pickled embedding space file
        force_cpu: If True, force sentence transformer to load on CPU (saves GPU memory)
                   Set to False if you need GPU encoding during inference
    """
    if not os.path.exists(local_path):
        raise FileNotFoundError(f"Embedding space file not found: {local_path}")

    # Force CPU loading for sentence transformer to save GPU memory
    # Set this FIRST before any imports that might trigger model loading
    # This is especially important when just loading for inspection
    if force_cpu:
        os.environ['FEATRIX_FORCE_CPU_SENTENCE_MODEL'] = '1'
        # Verify it was set
        if os.environ.get('FEATRIX_FORCE_CPU_SENTENCE_MODEL') != '1':
            import warnings
            warnings.warn("Failed to set FEATRIX_FORCE_CPU_SENTENCE_MODEL environment variable")

    # Ensure necessary modules are importable for unpickling
    # The pickled object may reference 'version' module or other modules
    try:
        current_file = Path(__file__)
        
        # Add src/ to path for version module (go up from lib/utils.py -> src/)
        src_path = current_file.parent.parent
        if str(src_path.resolve()) not in sys.path:
            sys.path.insert(0, str(src_path.resolve()))
        
        # Add lib/ to path for featrix.neural modules
        lib_path = current_file.parent
        if str(lib_path.resolve()) not in sys.path:
            sys.path.insert(0, str(lib_path.resolve()))
    except (NameError, AttributeError):
        # __file__ not available (e.g., in Jupyter/interactive session)
        # Try common paths
        common_paths = [
            Path("/sphere/app"),
            Path("/sphere/app/src"),
            Path("/sphere/app/lib"),
            Path.cwd() / "src",
            Path.cwd() / "lib",
        ]
        for path in common_paths:
            if path.exists() and str(path.resolve()) not in sys.path:
                sys.path.insert(0, str(path.resolve()))

    try:
        with open(local_path, "rb") as fp:
            try:
                # Try standard pickle.load first
                p = pickle.load(fp)
                return p
            except (AttributeError, pickle.UnpicklingError) as e:
                error_msg = str(e).lower()
                if "persistent_load" in error_msg or "persistent id" in error_msg:
                    # If we get a persistent_load error, try with Unpickler
                    fp.seek(0)  # Reset file pointer
                    unpickler = pickle.Unpickler(fp)
                    # Provide a handler for unknown persistent IDs
                    # Protocol 0 requires ASCII strings, so return empty string instead of None
                    def persistent_load(saved_id):
                        import logging
                        logger = logging.getLogger(__name__)
                        # Convert saved_id to string if it's not already
                        saved_id_str = str(saved_id) if saved_id is not None else "unknown"
                        logger.warning(f"⚠️  Encountered persistent_id {saved_id_str} in pickle file - returning empty string. This may cause issues if the ID is required.")
                        # Protocol 0 requires ASCII strings, not None
                        return ""
                    unpickler.persistent_load = persistent_load
                    p = unpickler.load()
                    return p
                raise
    finally:
        # Clean up environment variable
        if force_cpu:
            os.environ.pop('FEATRIX_FORCE_CPU_SENTENCE_MODEL', None)

    return None

def convert_to_iso(timestamp: datetime | None) -> str | None:
    if timestamp is None:
        return None
    
    return timestamp.isoformat()


def convert_from_iso(timestamp: str | None) -> datetime | None:
    if timestamp is None:
        return None
    
    return datetime.fromisoformat(timestamp)

import logging
import sys

def reset_logger():
    """Removes all handlers and resets logging to default stdout/stderr."""
    logging.getLogger().handlers.clear()  # Remove all handlers
    logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(asctime)s [%(levelname)-8s] %(name)-45s: %(message)s")


def validate_user_metadata(user_metadata: Optional[dict], max_size_kb: int = 32) -> Optional[dict]:
    """
    Validate user_metadata size. Returns the metadata if valid, raises ValueError if too large.
    
    Args:
        user_metadata: Dictionary to validate
        max_size_kb: Maximum size in KB (default 32KB)
    
    Returns:
        The validated metadata dict
    
    Raises:
        ValueError: If metadata exceeds max_size_kb when serialized to JSON
    """
    if user_metadata is None:
        return None
    
    if not isinstance(user_metadata, dict):
        raise ValueError(f"user_metadata must be a dict, got {type(user_metadata)}")
    
    # Serialize to JSON to check size
    try:
        json_str = json.dumps(user_metadata)
        size_bytes = len(json_str.encode('utf-8'))
        size_kb = size_bytes / 1024
        
        if size_kb > max_size_kb:
            raise ValueError(
                f"user_metadata exceeds maximum size of {max_size_kb}KB "
                f"(actual: {size_kb:.2f}KB). Please reduce the size of your metadata."
            )
        
        return user_metadata
    except (TypeError, ValueError) as e:
        if isinstance(e, ValueError) and "exceeds maximum size" in str(e):
            raise
        raise ValueError(f"user_metadata must be JSON-serializable: {e}")
