"""Tiferet App Contexts"""

# *** imports

# ** core
import time
from typing import Dict, Any, List

# ** app
from ..assets import TiferetError, TiferetAPIError
from ..di import ServiceProvider, DependenciesServiceProvider
from ..domain import (
    DomainObject,
    AppInterface,
    AppServiceDependency,
)
from ..events import (
    a,
    DomainEvent,
    ImportDependency,
    RaiseError,
)
from ..events.app import GetAppInterface
from .feature import FeatureContext
from .error import ErrorContext
from .logging import LoggingContext
from .request import RequestContext

# *** contexts

# ** context: app_manager_context
class AppManagerContext(object):
    '''
    The AppManagerContext is responsible for managing the application context.
    It provides methods to load the application interface and run features.
    '''

    # * attribute: settings
    settings: Dict[str, Any]

    # * attribute: service_provider
    service_provider: ServiceProvider

    # * init
    def __init__(self, settings: Dict[str, Any] = {}, service_provider: ServiceProvider = None):
        '''
        Initialize the AppManagerContext with application settings.

        :param settings: The application settings used to configure the app repository.
        :type settings: dict
        :param service_provider: An optional service provider for dependency injection.
        :type service_provider: ServiceProvider
        '''

        # Set the settings.
        self.settings = settings

        # Initialize the service provider with the provided one or a default dependencies provider.
        self.service_provider = service_provider if service_provider else DependenciesServiceProvider()

    # * method: load_app_repo
    def load_app_repo(self):
        '''
        Load the application repository using the configured settings.

        :return: The application repository instance.
        :rtype: Any
        '''

        # Resolve repository module path, class name, and parameters from settings with defaults.
        app_repo_module_path = self.settings.get('app_repo_module_path', a.const.DEFAULT_APP_SERVICE_MODULE_PATH)
        app_repo_class_name = self.settings.get('app_repo_class_name', a.const.DEFAULT_APP_SERVICE_CLASS_NAME)
        app_repo_params = self.settings.get('app_repo_params', dict(
            app_yaml_file='app/configs/app.yml'
        ))

        # Import and construct the app repository.
        try:
            repository_cls = ImportDependency.execute(
                app_repo_module_path,
                app_repo_class_name,
            )
            app_repo = repository_cls(**app_repo_params)

        # Wrap import failures in a structured Tiferet error.
        except TiferetError as e:
            RaiseError.execute(
                a.const.APP_REPOSITORY_IMPORT_FAILED_ID,
                f'Failed to import app repository: {e}.',
                exception=str(e),
            )

        # Return the imported app repository.
        return app_repo

    # * method: load_default_services
    def load_default_services(self) -> List[AppServiceDependency]:
        '''
        Load the default app service dependencies from the configuration constants.

        :return: A list of default app service dependencies.
        :rtype: List[AppServiceDependency]
        '''

        # Retrieve the default service dependencies from the configuration constants.
        return [
            DomainObject.new(
                AppServiceDependency,
                **service_data,
                validate=False,
            )
            for service_data in a.const.DEFAULT_SERVICES
        ]

    # * method: load_app_instance
    def load_app_instance(self, app_interface: AppInterface) -> Any:
        '''
        Load the app instance based on the provided app interface settings.

        :param app_interface: The app interface definition.
        :type app_interface: AppInterface
        :return: The app interface context instance.
        :rtype: Any
        '''

        # Build the full service type mapping from the app interface.
        try:
            dependencies = app_interface.get_service_type_mapping()

        # Raise a structured error if the dependency import fails.
        except Exception as e:
            RaiseError.execute(
                a.const.APP_SERVICE_IMPORT_FAILED_ID,
                exception=str(e),
            )

        # Register all dependencies in the service provider.
        self.service_provider.add_services(dependencies)

        # Resolve and return the app interface context from the service provider.
        return self.service_provider.get_service('app_context')

    # * method: load_interface
    def load_interface(self, interface_id: str) -> 'AppInterfaceContext':
        '''
        Load the application interface.

        :param interface_id: The interface ID.
        :type interface_id: str
        :return: The application interface context.
        :rtype: AppInterfaceContext
        '''

        # Load the app repository or service implementation.
        app_repo = self.load_app_repo()

        # Load the default app service dependencies.
        default_services = self.load_default_services()

        # Get the app interface settings via the AppService abstraction.
        app_interface = DomainEvent.handle(
            GetAppInterface,
            dependencies=dict(
                app_service=app_repo,
            ),
            interface_id=interface_id,
            default_services=default_services,
        )

        # Create the app interface context.
        app_interface_context = self.load_app_instance(app_interface)

        # Verify that the app interface context is valid.
        if not isinstance(app_interface_context, AppInterfaceContext):
            RaiseError.execute(
                a.const.INVALID_APP_INTERFACE_TYPE_ID,
                f'App context for interface is not valid: {interface_id}.',
                interface_id=interface_id,
            )

        # Return the app interface context.
        return app_interface_context

    # * method: run
    def run(self,
            interface_id: str,
            feature_id: str,
            headers: Dict[str, str] = {},
            data: Dict[str, Any] = {},
            debug: bool = False,
            **kwargs
        ) -> Any:
        '''
        Run the application interface.

        :param interface_id: The interface ID.
        :type interface_id: str
        :param dependencies: The dependencies.
        :type dependencies: dict
        :param kwargs: Additional keyword arguments.
        :type kwargs: dict
        :return: The response.
        :rtype: Any
        '''

        # Load the interface.
        app_interface = self.load_interface(interface_id)

        # Run the interface.
        return app_interface.run(
            feature_id, 
            headers, 
            data, 
            debug=debug,
            **kwargs
        )

# ** context: app_interface_context
class AppInterfaceContext(object): 
    '''
    The application interface context is a class that is used to create and run the application interface.
    '''

    # * attribute: interface_id
    interface_id: str

    # * attribute: features
    features: FeatureContext

    # * attribute: errors
    errors: ErrorContext

    # * attribute: logging
    logging: LoggingContext

    # * init
    def __init__(self, interface_id: str, features: FeatureContext, errors: ErrorContext, logging: LoggingContext):
        '''
        Initialize the application interface context.

        :param interface_id: The interface ID.
        :type interface_id: str
        :param features: The feature context.
        :type features: FeatureContext
        :param errors: The error context.
        :type errors: ErrorContext
        '''

        # Assign instance variables.
        self.interface_id = interface_id
        self.features = features
        self.errors = errors
        self.logging = logging

    # * method: parse_request
    def parse_request(self, headers: Dict[str, str] = {}, data: Dict[str, Any] = {}, feature_id: str = None) -> RequestContext:
        '''
        Parse the incoming request.

        :param headers: The request headers.
        :type headers: dict
        :param data: The request data.
        :type data: dict
        :param feature_id: The feature identifier if provided.
        :type feature_id: str
        :return: The parsed request as a request context.
        :rtype: RequestContext
        '''

        # Add the interface id to the request headers.
        headers.update(dict(
            interface_id=self.interface_id,
        ))

        # Create the request context object.
        request = RequestContext(
            headers=headers,
            data=data,
            feature_id=feature_id,
        )

        # Return the request model object.
        return request

    # * method: execute_feature
    def execute_feature(self, feature_id: str, request: RequestContext, **kwargs):
        '''
        Execute the feature context.

        :param feature_id: The feature identifier.
        :type feature_id: str
        :param request: The request context object.
        :type request: RequestContext
        :param kwargs: Additional keyword arguments.
        :type kwargs: dict
        '''

        # Add the feature id to the request headers.
        request.headers.update(dict(
            feature_id=feature_id
        ))

        # Execute feature context and return session.
        self.features.execute_feature(feature_id, request, **kwargs)

    # * method: handle_error
    def handle_error(self, error: Exception) -> Any:
        '''
        Handle the error by formatting it via ErrorContext and raising TiferetAPIError.

        :param error: The error to handle.
        :type error: Exception
        :return: The error response.
        :rtype: Any
        '''

        # If the error is not a TiferetError, wrap it in one.
        if not isinstance(error, TiferetError):
            error = TiferetError(
                'APP_ERROR',
                f'An error occurred in the app: {str(error)}',
                error=str(error)
            )

        # Get formatted response from ErrorContext.
        formatted_error = self.errors.handle_error(error)

        # Raise the API exception with the formatted payload.
        raise TiferetAPIError(**formatted_error)

    # * method: handle_response
    def handle_response(self, request: RequestContext) -> Any:
        '''
        Handle the response from the request.

        :param request: The request context.
        :type request: RequestContext
        :return: The response.
        :rtype: Any
        '''

        # Handle the response and return it.
        return request.handle_response()

    # * method: run
    def run(self, 
            feature_id: str, 
            headers: Dict[str, str] = {}, 
            data: Dict[str, Any] = {},
            **kwargs) -> Any:
        '''
        Run the application interface by executing the feature.

        :param feature_id: The feature identifier.
        :type feature_id: str
        :param headers: The request headers.
        :type headers: dict
        :param data: The request data.
        :type data: dict
        :param kwargs: Additional keyword arguments.
        :type kwargs: dict
        '''

        # Start timing immediately.
        start_time = time.perf_counter()

        # Create the logger for the app interface context.
        logger = self.logging.build_logger()

        # Parse request.
        logger.debug(f'Parsing request for feature: {feature_id}')
        request = self.parse_request(headers, data, feature_id)

        # Execute feature context and return session.
        try:
            logger.debug(f'Executing feature: {feature_id} with request: {request.data}')
            self.execute_feature(
                feature_id=feature_id, 
                request=request, 
                logger=logger,
                **kwargs)

        # Handle error and return response if triggered.
        except TiferetError as e:
            logger.error(f'Error executing feature {feature_id}: {str(e)}')
            return self.handle_error(e)

        # Calculate execution duration in milliseconds.
        duration_ms = round((time.perf_counter() - start_time) * 1000)
        duration_str = f" ({duration_ms}ms)"

        # Log successful execution with timing.
        logger.debug(f'Feature {feature_id} executed successfully, handling response.')
        logger.info(f'Executed Feature - {feature_id}{duration_str}')

        # Handle response.
        return self.handle_response(request)

