from bfabric import Bfabric
from bfabric.rest.token_data import TokenData
import json
import datetime
from bfabric_web_apps.utils.get_logger import get_logger
import os
import bfabric_web_apps

from cachetools import TTLCache
from loguru import logger

from bfabric_web_apps.utils.config import settings
from bfabric_web_apps.utils.url_utils import normalize_url

# Cache for authenticated wrappers, keyed by session identifier (webbase:user:jobId)
# TTL of 1 hour, max 100 concurrent sessions
_wrapper_cache = TTLCache(maxsize=100, ttl=3600)

# Cache for token-to-data conversions, keyed by token string
# TTL of 1 hour (same as wrapper cache), max 100 concurrent tokens
_token_data_cache = TTLCache(maxsize=100, ttl=3600)


def _get_cache_key(token_data_dict: dict) -> str:
    """
    Generate a deterministic cache key from token_data fields.

    Args:
        token_data_dict: Dictionary containing webbase_data, user_data, and jobId

    Returns:
        str: Cache key in format "webbase:user:jobId"
    """
    webbase = normalize_url(token_data_dict.get("webbase_data", ""))
    user = token_data_dict.get("user_data", "")
    job_id = token_data_dict.get("jobId", "")
    return f"{webbase}:{user}:{job_id}"


class BfabricInterface( Bfabric ):
    _instance = None  # Singleton instance
    """
    A class to interface with the Bfabric API, providing methods to validate tokens,
    retrieve data, and send bug reports.
    """

    def __init__(self):
        """
        Initializes an instance of BfabricInterface.
        """
        pass

    def __new__(cls, *args, **kwargs):
        """Ensure only one instance exists (Singleton Pattern)."""
        if cls._instance is None:
            cls._instance = super(BfabricInterface, cls).__new__(cls)
        return cls._instance

    def _tokendata_to_legacy_dict(self, token_data: TokenData) -> dict:
        """
        Convert new TokenData Pydantic model to legacy token_data dict format.
        Provides backward compatibility for existing code.

        Args:
            token_data: TokenData from connect_webapp()

        Returns:
            dict: Legacy format token_data
        """
        return {
            'jobId': token_data.job_id,
            'user_data': token_data.user,
            'userWsPassword': token_data.user_ws_password.get_secret_value(),
            'environment': token_data.environment,
            'entityClass_data': token_data.entity_class,
            'entity_id_data': token_data.entity_id,
            'application_data': str(token_data.application_id),
            'token_expires': token_data.token_expires.strftime("%Y-%m-%d %H:%M:%S"),
            'webbase_data': token_data.caller,  # Use caller from token (already validated)
            'application_params_data': {},  # Legacy field, unused
            'webServiceUser': token_data.web_service_user,
        }

    def get_wrapper(self, token_data: dict = None):
        """
        Return a wrapper for the given token_data, using cache when available.

        Args:
            token_data: The token data dictionary. Looks up cached
                        wrapper by session key (webbase:user:jobId).

        Returns:
            Bfabric: The authenticated wrapper instance.

        Raises:
            RuntimeError: If no wrapper is available (token not validated or not in cache).
            ValueError: If token_data is not provided (deprecated - will be required in future).
        """
        if not token_data:
            import warnings
            warnings.warn(
                "Calling get_wrapper() without token_data is deprecated and will be removed in a future version. "
                "Please pass token_data to get_wrapper(token_data).",
                DeprecationWarning,
                stacklevel=2
            )
            raise ValueError(
                "token_data is required to get wrapper. "
                "Pass the token_data dict from process_url_and_token() or your dcc.Store."
            )

        cache_key = _get_cache_key(token_data)
        if cache_key in _wrapper_cache:
            return _wrapper_cache[cache_key]
        raise RuntimeError(
            f"No cached wrapper found for session key '{cache_key}'. "
            "Token validation must run first."
        )
    

    def token_to_data(self, token):
        """
        Validates the given token and retrieves its associated data using connect_webapp.
        Results are cached for 1 hour to avoid redundant validation calls.

        Args:
            token (str): The token to validate.

        Returns:
            str: A JSON string containing token data if valid.
            str: "EXPIRED" if the token is expired.
            None: If the token is invalid or validation fails.
        """

        if not token:
            return None

        # Check cache first
        if token in _token_data_cache:
            logger.debug(f"Token data cache hit for token {token[:20]}...")
            return _token_data_cache[token]

        try:
            # Use the new connect_webapp method with configurable validation URL
            wrapper, token_data = Bfabric.connect_webapp(
                token=token,
                validation_instance_url=settings.VALIDATION_INSTANCE_URL
            )

            # Validate that the token's instance is in the supported whitelist
            # Normalize URLs for comparison (removes trailing slashes)
            normalized_caller = normalize_url(token_data.caller)
            normalized_whitelist = [normalize_url(url) for url in settings.SUPPORTED_BFABRIC_INSTANCES]

            if normalized_caller not in normalized_whitelist:
                raise ValueError(
                    f"B-Fabric instance '{token_data.caller}' is not in the list of supported instances. "
                    f"Supported instances: {settings.SUPPORTED_BFABRIC_INSTANCES}"
                )

            # Check if token is expired (7 days grace period)
            current_time = datetime.datetime.now(tz=token_data.token_expires.tzinfo)
            if current_time > token_data.token_expires + datetime.timedelta(days=7):
                result = "EXPIRED"
                # Cache the EXPIRED result to avoid re-validating expired tokens
                _token_data_cache[token] = result
                return result

            # Convert TokenData to dict format for backward compatibility
            token_data_dict = self._tokendata_to_legacy_dict(token_data)

            # Cache the wrapper by session key (webbase:user:jobId)
            cache_key = _get_cache_key(token_data_dict)
            is_new = cache_key not in _wrapper_cache
            _wrapper_cache[cache_key] = wrapper

            if is_new:
                logger.info(f"Cached wrapper for {token_data_dict.get('user_data')}")

            # Log the token validation process
            L = get_logger(token_data_dict)
            L.log_operation(
                operation="Authentication Process",
                message=f"Token validated successfully. User {token_data_dict.get('user_data')} authenticated.",
                params=None,
                flush_logs=True
            )

            # Cache and return the result
            result = json.dumps(token_data_dict)
            _token_data_cache[token] = result
            logger.debug(f"Cached token data for token {token[:20]}...")
            return result

        except Exception as e:
            # Token validation failed
            print(f"Token validation failed: {e}")
            # Don't cache failed validations (None) as the issue might be temporary
            return None
        



    def entity_data(self, token_data: dict, return_dict: bool = False) -> str | dict:
        """
        Retrieves entity data associated with the provided token.

        Args:
            token_data (dict): The token data dictionary.
            return_dict (bool): If True, return dict directly. If False, return JSON string.

        Returns:
            str | dict: Entity data as JSON string or dict depending on return_dict.
            {}: If the retrieval fails or token_data is invalid.
        """

        if not token_data:
            return {} if return_dict else json.dumps({})

        wrapper = self.get_wrapper(token_data)
        entity_class = token_data.get('entityClass_data', None)
        # B-Fabric API accepts entity type names in lowercase (e.g., "Project" -> "project")
        endpoint = entity_class.lower() if entity_class else None
        entity_id = token_data.get('entity_id_data', None)
        jobId = token_data.get('jobId', None)
        username = token_data.get("user_data", "None")
        environment = token_data.get("environment", "None")

        if wrapper and entity_class and endpoint and entity_id and jobId:
            L = get_logger(token_data)
            
            # Log the read operation directly using Logger L
            entity_data_dict = L.logthis(
                api_call=wrapper.read,
                endpoint=endpoint,
                obj={"id": entity_id},
                max_results=None,
                params=None,
                flush_logs=False
            )[0]

            
            if entity_data_dict:
                result = {
                    "name": entity_data_dict.get("name", ""),
                    "createdby": entity_data_dict.get("createdby"),
                    "created": entity_data_dict.get("created"),
                    "modified": entity_data_dict.get("modified"),
                    "full_api_response": entity_data_dict,
                }
                return result if return_dict else json.dumps(result)
            else:
                L.log_operation(
                    operation="entity_data",
                    message="Entity data retrieval failed or returned None.",
                    params=None,
                    flush_logs=True
                )
                print("entity_data_dict is empty or None")
                return {} if return_dict else json.dumps({})

        else:
            print("Invalid input or entity information")
            return {} if return_dict else json.dumps({})
        

    def app_data(self, token_data: dict, return_dict: bool = False) -> str | dict:
        """
        Retrieves application data (App Name and Description) associated with the provided token.

        Args:
            token_data (dict): The token data dictionary.
            return_dict (bool): If True, return dict directly. If False, return JSON string.

        Returns:
            str | dict: Application data as JSON string or dict depending on return_dict.
            {}: If retrieval fails or token_data is invalid.
        """

        if not token_data:
            return {} if return_dict else json.dumps({})

        # Extract App ID from token
        app_data_raw = token_data.get("application_data", None)

        try:
            app_id = int(app_data_raw)
        except:
            print("Invalid application_data format in token_data")
            return {} if return_dict else json.dumps({})

        # Define API endpoint
        endpoint = "application"

        # Initialize Logger
        L = get_logger(token_data)

        # Get API wrapper
        wrapper = self.get_wrapper(token_data)
        if not wrapper:
            print("Failed to get Bfabric API wrapper")
            return {} if return_dict else json.dumps({})

        # Make API Call
        app_data_dict = L.logthis(
            api_call=wrapper.read,
            endpoint=endpoint,
            obj={"id": app_id},  # Query using the App ID
            max_results=None,
            params=None,
            flush_logs=False
        )

        # If API call fails, return empty
        if not app_data_dict or len(app_data_dict) == 0:
            L.log_operation(
                operation="app_data",
                message=f"Failed to retrieve application data for App ID {app_id}",
                params=None,
                flush_logs=True
            )
            return {} if return_dict else json.dumps({})

        # Extract App ID, Name, and Description
        app_info = app_data_dict[0]  # First (and only) result

        result = {
            "id": app_info.get("id", "Unknown"),
            "name": app_info.get("name", "Unknown"),
            "description": app_info.get("description", "No description available")
        }

        return result if return_dict else json.dumps(result)
     
    
    def send_bug_report(self, token_data = None, entity_data = None, description = None):
        """
        Sends a bug report via email.

        Args:
            token_data (dict): Token data to include in the report.
            entity_data (dict): Entity data to include in the report.
            description (str): A description of the bug.

        Returns:
            bool: True if the report is sent successfully, False otherwise.
        """

        mail_string = f"""
        BUG REPORT FROM QC-UPLOADER
            \n\n
            token_data: {token_data} \n\n 
            entity_data: {entity_data} \n\n
            description: {description} \n\n
            sent_at: {datetime.datetime.now()} \n\n
        """

        mail = f"""
            echo "{mail_string}" | mail -s "Bug Report" {bfabric_web_apps.BUG_REPORT_EMAIL_ADDRESS}
        """

        print("MAIL STRING:")
        print(mail_string)

        print("MAIL:")
        print(mail)

        os.system(mail)

        return True
    

    
# Create a globally accessible instance
bfabric_interface = BfabricInterface()


