"""Automation Handler Module.

This module provides the core automation management functionality for the JL Automation platform.
It includes an abstract base class for automation workflows and supporting data structures
for managing automation lifecycle, resource initialization, and cleanup operations.

The module implements lazy loading of dependencies, thread-safe resource management,
and proper cleanup mechanisms to ensure optimal resource utilization in serverless environments.

Key Features:
    - Thread-safe lazy initialization of dependencies
    - Resource management with automatic cleanup
    - Support for both HTTP and timer triggers
    - Comprehensive audit logging integration
    - Connection pooling and caching

Example:
    Basic usage by inheriting from AutomationManager:
        class MyAutomation(AutomationManager):
            async def start_processing(self, data):
                # Access pre-configured managers
                result = await self.db.execute_query("SELECT * FROM table")
                await self.audit.save_audit(audit_data)
                return result
"""

import os
import logging
from functools import lru_cache
from abc import ABC, abstractmethod
from typing import Any, Tuple
from .sql_handler import SqlManager
from requests.exceptions import RequestException
from .mainsubsysapi_handler import MainSubSysApiManager
from .sharepoint_handler import SharePointManager
from .appconfig_handler import Configurations
from .common_handler import CommonManager
from .audit_handler import AuditManager
from .graphql_handler import GraphQLServiceManager
from dataclasses import dataclass
from functools import cached_property
from threading import Lock
from typing import Optional
import asyncio
import weakref
from concurrent.futures import ThreadPoolExecutor
import gc

@dataclass
class AutomationDetails:
    """Data class to hold automation configuration details.
    
    This class encapsulates the essential information about an automation
    workflow including identification, tenant context, and deployment information.
    
    Attributes:
        code (str): Unique automation code identifier.
        tenant_id (str): Tenant identifier for multi-tenancy support.
        description (str): Human-readable description of the automation.
        deployment_id (str): Unique deployment identifier.
    """
    code: str
    tenant_id: str
    description: str
    deployment_id: str

class AutomationManager(ABC):
    """Abstract base class for automation workflow management.
    
    This class provides the foundation for all automation workflows in the JL platform.
    It handles resource initialization, dependency injection, and lifecycle management
    with thread-safe lazy loading and proper cleanup mechanisms.
    
    The class automatically fetches automation details from the database, initializes
    required managers, and provides a clean interface for automation implementations.
    
    Attributes:
        code (str): Unique automation code identifier.
        user_email (str): Email of the user triggering the automation.
        is_http_trigger (bool): Whether this is triggered by HTTP request.
        invocation_id (Optional[str]): Function invocation identifier.
        auto_id (str): Database automation ID.
        tenant_id (str): Tenant identifier.
        description (str): Automation description.
        deploymentid (str): Deployment unique identifier.
        automation_name (str): Name of the automation.
        env (str): Application environment.
        
    Note:
        This is an abstract class that must be inherited. Subclasses must
        implement the start_processing method.
    """

    def __init__(self, code: str, user_email: str, is_http_trigger: bool = False, 
                 invocation_id: str = None) -> None:
        """Initialize the AutomationManager with context and setup dependencies.
        
        Args:
            code (str): Unique automation code identifier.
            user_email (str): Email of the user triggering the automation.
            is_http_trigger (bool, optional): Whether this is HTTP triggered. Defaults to False.
            invocation_id (Optional[str], optional): Function invocation ID. Defaults to None.
            
        Raises:
            RequestException: If automation code is not found or disabled.
            Exception: If there are issues fetching automation details.
        """
        self.code = code
        self.user_email = user_email
        self.is_http_trigger = is_http_trigger
        self.invocation_id = invocation_id

        # Thread-safe locks for lazy initialization
        self._db_lock = Lock()
        self._api_lock = Lock()
        self._sharepoint_lock = Lock()
        self._common_lock = Lock()
        self._config_lock = Lock()
        self._audit_lock = Lock()
        self._graphql_lock = Lock()
        
        # Private instances for lazy loading
        self._db: Optional[SqlManager] = None
        self._api: Optional[MainSubSysApiManager] = None
        self._sharepoint: Optional[SharePointManager] = None
        self._common: Optional[CommonManager] = None
        self._config: Optional[Configurations] = None
        self._audit: Optional[AuditManager] = None
        self._graphql: Optional[GraphQLServiceManager] = None
        
        # Fetch automation details and populate instance variables
        auto_id, unique_id, fetched_tenant, fetched_desc = self._fetch_automation_details_sync(self.code)
        self.auto_id = auto_id
        self.tenant_id = fetched_tenant
        self.description = fetched_desc
        self.deploymentid = unique_id
        self.automation_name = fetched_desc
        self.env = os.getenv("APPLICATION_ENVIRONMENT")

        # Resource management
        self._executor = ThreadPoolExecutor(max_workers=1)
        self._finalizer = weakref.finalize(self, self._cleanup)


    @cached_property
    def db(self) -> SqlManager:
        """Get the SQL manager instance with thread-safe lazy initialization.
        
        Returns:
            SqlManager: Configured SQL manager for database operations.
        """
        config = self.config
        with self._db_lock:
            if self._db is None:
                logging.debug("Initializing SQL Manager")
                self._db = SqlManager(config)
            return self._db

    @cached_property
    def api(self) -> MainSubSysApiManager:
        """Get the API manager instance with thread-safe lazy initialization.
        
        Returns:
            MainSubSysApiManager: Configured API manager for external service calls.
        """
        config = self.config
        with self._api_lock:
            if self._api is None:
                logging.debug("Initializing API Manager")
                self._api = MainSubSysApiManager.get_instance(self.tenant_id, config)
            return self._api

    @cached_property
    def sharepoint(self) -> SharePointManager:
        """Get the SharePoint manager instance with thread-safe lazy initialization.
        
        Returns:
            SharePointManager: Configured SharePoint manager for document operations.
        """
        config = self.config
        with self._sharepoint_lock:
            if self._sharepoint is None:
                logging.debug("Initializing SharePoint Manager")
                self._sharepoint = SharePointManager(config)
            return self._sharepoint

    @cached_property
    def config(self) -> Configurations:
        """Get the configuration manager instance with thread-safe lazy initialization.
        
        Returns:
            Configurations: Singleton configuration manager.
        """
        with self._config_lock:
            if self._config is None:
                logging.debug("Initializing Config Manager")
                self._config = Configurations.create()
            return self._config
        
    @cached_property
    def graphql(self) -> GraphQLServiceManager:
        """Thread-safe lazy initialization of graphql manager."""
        # Only Available for non-HTTP Triggered Automations
        if self.is_http_trigger:
            raise ValueError("GraphQL service is only available for Non-HTTP triggered automations.")
        
        with self._graphql_lock:
            if self._graphql is None:
                logging.debug("Initializing GraphQL Service Manager")
                self._graphql = GraphQLServiceManager(tenant_id=self.tenant_id, config=self.config)
            return self._graphql

    @cached_property
    def common(self) -> CommonManager:
        """Get the common manager instance with thread-safe lazy initialization.
        
        This property initializes all dependencies and creates a CommonManager
        with the required automation details.
        
        Returns:
            CommonManager: Configured common manager with all dependencies.
        """
        # Initialize dependencies first without holding the common lock
        sp = self.sharepoint
        api = self.api
        db = self.db
        config = self.config
        
        with self._common_lock:
            if self._common is None:
                logging.debug("Initializing Common Manager")
                automation_details = AutomationDetails(
                    code=self.code,
                    tenant_id=self.tenant_id,
                    description=self.description,
                    deployment_id=self.deploymentid
                )
                self._common = CommonManager(sp, api, db, automation_details, config)
            return self._common
    
    @cached_property
    def audit(self) -> AuditManager:
        """Get the audit manager instance with thread-safe lazy initialization.
        
        Returns:
            AuditManager: Configured audit manager for logging operations.
            
        Raises:
            Exception: If there are issues initializing the audit manager.
        """
        # Initialize dependencies first without holding the audit lock
        invocation_id = self.invocation_id
        tenant_id = self.tenant_id
        auto_id = int(self.auto_id)
        try:
            with self._audit_lock:
                if self._audit is None:
                    logging.debug("Initializing Audit Manager")
                    self._audit = AuditManager(invocation_id, auto_id, tenant_id, self.config)
                return self._audit
        except Exception as e:
            logging.error(f"Error initializing Audit Manager: {e}")
            raise
    
    async def cleanup(self):
        """Clean up all resources asynchronously.
        
        This method properly closes all initialized managers and releases resources.
        It's designed to be called at the end of automation processing or in error scenarios.
        
        Raises:
            Exception: If there are issues during cleanup (logged but not re-raised).
        """
        try:
            cleanup_tasks = []
            if self._db:
                cleanup_tasks.append(self._db.close())
            if self._api:
                cleanup_tasks.append(self._api.close())
            if self._graphql:
                cleanup_tasks.append(self._graphql.aclose())
            
            if cleanup_tasks:
                await asyncio.gather(*cleanup_tasks, return_exceptions=True)
        except Exception as e:
            logging.error(f"Error during cleanup: {e}")
            raise
        finally:
            self._db = None
            self._api = None
            self._sharepoint = None
            self._common = None
            self._config = None
            self._audit = None
            self._graphql = None
            if hasattr(self, '_executor'):
                self._executor.shutdown(wait=False)
            
            # Run garbage collection to free up resources
            garbage = gc.collect()
            logging.info(f"Garbage collector collected {garbage} objects.")
        
    def _cleanup(self):
        """Synchronous cleanup called by finalizer.
        
        This method is called automatically when the object is being garbage collected
        to ensure resources are properly released even if cleanup() wasn't called explicitly.
        """
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            loop.run_until_complete(self.cleanup())
            loop.close()
        except Exception as e:
            logging.error(f"Error in finalizer cleanup: {e}")

    def __del__(self):
        """Trigger cleanup through finalizer when object is destroyed."""
        if hasattr(self, '_finalizer'):
            self._finalizer()

    async def __aenter__(self):
        """Async context manager entry.
        
        Returns:
            AutomationManager: Self reference for context manager usage.
        """
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit with proper cleanup.
        
        Args:
            exc_type: Exception type if an exception occurred.
            exc_val: Exception value if an exception occurred.
            exc_tb: Exception traceback if an exception occurred.
        """
        await self.cleanup()


    @lru_cache(maxsize=50)
    def _fetch_automation_details_sync(self, code: str) -> Tuple[str, str, str]:
        """Synchronously fetch deployment ID, tenant ID, and description from database.
        
        Uses LRU cache to avoid repeated database hits for the same automation code.
        This method is called during initialization to populate automation context.
        
        Args:
            code (str): The automation code to look up in the database.
            
        Returns:
            Tuple[str, str, str, str]: A tuple containing (auto_id, unique_id, tenant_id, description).
            
        Raises:
            RequestException: If the automation code is not found or is disabled.
            Exception: For other unexpected database or connection issues.
            
        Note:
            This method uses the db property which will initialize the SQL manager
            if it hasn't been initialized yet.
        """
        try:
            query = """
            SELECT A.Id, DA.UniqueId, DA.TenantId, A.[Description]
            FROM AutomationNew.DeployedAutomation DA
            INNER JOIN AutomationNew.Automation A ON A.Id = DA.AutomationId
            WHERE A.Code = ?
            AND A.IsDeleted = 0
            AND DA.IsDeleted = 0
            """

            result_df = self.db.get_sync(query, "Id, UniqueId, TenantId, Description", (code))
            
            if result_df.empty:
                raise RequestException(f"No record found for code: {code} or it is disabled.")

            auto_id = result_df["Id"].iloc[0]
            unique_id = result_df["UniqueId"].iloc[0]
            tenant_id = result_df["TenantId"].iloc[0]
            description = result_df["Description"].iloc[0]
            return auto_id, unique_id, tenant_id, description
        except Exception as e:
            logging.error(f"Error fetching data for code {code}: {e}")
            raise e

    @abstractmethod
    async def start_processing(self, *args: Any, **kwargs: Any) -> Any:
        """Abstract method to run the automation process.
        
        This method must be implemented by all automation subclasses.
        It defines the core processing logic for the specific automation workflow.
        
        Args:
            *args: Variable length argument list passed to the automation.
            **kwargs: Arbitrary keyword arguments passed to the automation.
            
        Returns:
            Any: The result of the automation processing. Type depends on implementation.
            
        Note:
            Implementations should handle their own error logging and audit trail
            creation using the provided audit manager.
        """
        pass

    