import asyncio
from typing import Union
from pyee.asyncio import AsyncIOEventEmitter
from dataclasses import dataclass

from gumband_python_sdk.auth_service import AuthService
from gumband_python_sdk.content_service import ContentService
from gumband_python_sdk.constants import SocketEventType
from gumband_python_sdk.endpoint_manager import EndpointManager
from gumband_python_sdk.logging_handler import get_local_logger
from gumband_python_sdk.user_event_service import UserEventService
from gumband_python_sdk.notification_service import NotificationService
from gumband_python_sdk.network_handler import NetworkHandler
from gumband_python_sdk.web_socket_service import WebSocketService
from gumband_python_sdk.manifest_service import ManifestService
from gumband_python_sdk.utils.manifest_utils import find_status_by_manifest_id, find_setting_by_manifest_id

local_logger = get_local_logger()


@dataclass
class GumbandOptions:
    """
    Configuration options for the Gumband SDK.

    Attributes:
        environment (str): Environment type, such as 'app' or 'local' (default: 'app').
        custom_server_ip (str): Custom server IP to connect to if you are not connecting to app.gumband.com.
        content_location (str): Path to local content storage directory (default: None).
        skip_organization_file_sync (str): Skips file syncing of organizational-level files (default: True).
        interaction_timeout (int): The timeout for interactions in milliseconds (default: 60000).
    """

    environment: str = "app"
    custom_server_ip: str = "0.0.0.0"
    content_location: str = None
    skip_organization_file_sync: bool = True
    interaction_timeout: int = 60000


class Gumband(AsyncIOEventEmitter):
    """
    The Gumband SDK class handles authentication, WebSocket connection, and content syncing with Gumband.
    """

    def __init__(self, exhibit_id: int, token: str, manifest: Union[str, dict] = {}, options: GumbandOptions = {}):
        """
        Initializes the Gumband SDK.

        Args:
            exhibit_id (int): The ID of the exhibit.
            token (str): The Authentication token for this exhibit.
            manifest (Union[str, dict], optional): Path or dictionary of the manifest (default: {}).
            options (GumbandOptions, optional): Configuration options (default: GumbandOptions()).
            options.environment (str, optional): Environment type, such as 'app' or 'local' (default: 'app').
            options.custom_server_ip (str, optional): Custom server IP to connect to if you're not connecting to app.gumband.com.
            options.content_location (str, optional): Path to local content storage directory (default: None).
            options.skip_organization_file_sync (str, optional): Skips file syncing of organizational-level files (default: True).
        """
        super().__init__()
        self.exhibit_id = exhibit_id
        self.token = token
        self.manifest = {}
        self.environment = options.environment
        self.interaction_timeout = options.interaction_timeout
        self.emit_ready_event = True

        self.endpoint_manager: EndpointManager = EndpointManager(environment=options.environment, custom_server_ip=options.custom_server_ip)
        self.network_handler: NetworkHandler = NetworkHandler(self.endpoint_manager)
        self.auth_service: AuthService = AuthService(self.exhibit_id, self.token, self.network_handler)
        self.content_service: ContentService = ContentService(
            self.exhibit_id, self.network_handler, options.content_location, options.skip_organization_file_sync
        )
        self.user_event_service: UserEventService = UserEventService(exhibit_id=self.exhibit_id, network_handler=self.network_handler)
        self.manifest_service: ManifestService = ManifestService(self.exhibit_id, self.network_handler)
        self.notification_service: NotificationService = NotificationService(self.exhibit_id, self.network_handler)
        self.websocket: WebSocketService = WebSocketService(
            network_handler=self.network_handler,
            exhibit_id=self.exhibit_id,
        )

        self.manifest = {}

        self._set_config(
            exhibit_id=exhibit_id,
            token=token,
            manifest=manifest,
            environment=self.environment,
        )

    async def connect(self):
        """
        Authenticates and establishes a WebSocket connection with Gumband and syncs content files if applicable.
        """
        try:
            await self._authenticate()

            self.websocket = WebSocketService(
                network_handler=self.network_handler,
                exhibit_id=self.exhibit_id,
            )
            await asyncio.gather(
                self.user_event_service.set_metrics_configuration(self.interaction_timeout),
                self.manifest_service.setManifest(),
                self.manifest_service.setSdkVersion(),
                self.websocket.connect(),
                self.content_service.sync(),
            )

            self._setup_ws_event_reemission()
            local_logger.info("Authenticated and WebSocket connection initiated.")

            self.emit(SocketEventType.READY.value)
        except Exception as e:
            self.emit(SocketEventType.ERROR.value, f"There was an error connecting to Gumband: {e}")

    def _set_config(
        self,
        exhibit_id: int,
        token: str,
        manifest: Union[str, dict] = "./manifest.json",
        environment: str = "prod",
    ):
        """
        Private function. Set the configuration for the Gumband SDK.

        :param exhibit_id: Exhibit ID.
        :param token: Authentication token.
        :param manifest: Manifest JSON string, dict, or file path.
        :param environment: Environment (e.g., 'app', 'dev', 'stag', 'local').
        """
        self.exhibit_id = exhibit_id
        self.token = token
        self.environment = environment

        # Load and parse the manifest
        self.manifest_service.parse_manifest(manifest)

    async def _authenticate(self):
        """
        Private function.
        Authenticates the exhibit and sets the JWT token for future requests.
        """
        try:
            await self.auth_service.authenticate()
        except Exception as e:
            local_logger.error(f"Authentication failed: {e}")
            raise

    async def get_manifest(self) -> dict:
        """
        Retrieves the manifest from the server.

        Returns:
            dict: The manifest data.
        """
        local_logger.info("Getting manifest from server...")
        try:
            return await self.manifest_service.get_manifest()
        except Exception as e:
            local_logger.error(f"Failed to get manifest: {e}")
            self.emit(SocketEventType.ERROR.value, f"Failed to get manifest: {e}")
            return {}

    def _setup_ws_event_reemission(self):
        """
        Private function
        Sets up event listeners for WebSocket events to reemit them.
        """
        self.websocket.on(SocketEventType.CONTROL_RECEIVED.value, self._reemit_event)
        self.websocket.on(SocketEventType.SETTING_RECEIVED.value, self._reemit_event)
        self.websocket.on(SocketEventType.SETTING_LIST_RECEIVED.value, self._reemit_event)
        self.websocket.on(SocketEventType.SETTING_LIST_ITEM_DELETED.value, self._reemit_event)
        self.websocket.on(SocketEventType.FILE_UPLOADED.value, self._reemit_event)
        self.websocket.on(SocketEventType.FILE_DELETED.value, self._reemit_event)
        self.websocket.on(SocketEventType.EXHIBIT_ONLINE.value, self._reemit_event)
        self.websocket.on(SocketEventType.EXHIBIT_OFFLINE.value, self._reemit_event)
        self.websocket.on(SocketEventType.CLOSE_SDK_CONNECTION.value, self._reemit_event)
        self.websocket.on(SocketEventType.COMPONENT_ADDED_TO_EXHIBIT.value, self._reemit_event)
        self.websocket.on(SocketEventType.COMPONENT_REMOVED_FROM_EXHIBIT.value, self._reemit_event)
        self.websocket.on(SocketEventType.COMPONENT_CONNECTED.value, self._reemit_event)
        self.websocket.on(SocketEventType.COMPONENT_DISCONNECTED.value, self._reemit_event)
        self.websocket.on(SocketEventType.COMPONENT_PROPERTY_RECEIVED.value, self._reemit_event)

    async def _reemit_event(self, action, data):
        """
        Private function.

        Args:
            action (str): The event action.
            data (dict): The event data.
        """
        self.emit(action, data=data)

    async def set_status(self, status_manifest_id, status_value) -> dict:
        """
        Sets the status value for the provided manifest ID.

        Args:
            status_manifest_id (str): The manifest ID for the status.
            status_value (str): The value to set for the status.
        """
        try:
            local_logger.info(f"Setting status, {status_manifest_id}, to value, {status_value}")
            status = find_status_by_manifest_id(self.manifest_service.manifest, status_manifest_id)
            return await self.manifest_service.set_status(status["id"], status_value)
        except Exception as e:
            local_logger.error(f"Failed to set value, {status_value}, for id, {status_manifest_id}: {e}")
            self.emit(SocketEventType.ERROR.value, f"Failed to set status: {e}")
            return {}

    async def get_status(self, status_manifest_id) -> dict:
        """
        Retrieves the status for the provided manifest ID.

        Args:
            status_manifest_id (str): The manifest ID for the status.

        Returns:
            dict: The status.
        """
        try:
            local_logger.info(f"Getting status, {status_manifest_id} from server")
            status = find_status_by_manifest_id(self.manifest_service.manifest, status_manifest_id)
            return await self.manifest_service.get_status(status["id"])
        except Exception as e:
            local_logger.error(f"Failed to get status: {e}")
            self.emit(SocketEventType.ERROR.value, f"Failed to get status: {e}")
            return {}

    async def set_setting(self, setting_manifest_id, setting_value) -> dict:
        """
        Sets the setting value for the provided manifest ID.

        Args:
            setting_manifest_id (str): The manifest ID for the setting.
            setting_value (str): The value to set for the setting.
        """
        try:
            local_logger.info(f"Changing setting, {setting_manifest_id}, to value, {setting_value}")
            setting = find_setting_by_manifest_id(self.manifest_service.manifest, setting_manifest_id)
            return await self.manifest_service.set_setting(setting["id"], setting_value)
        except Exception as e:
            local_logger.error(f"Failed to set value, {setting_value}, for id, {setting_manifest_id}: {e}")
            self.emit(SocketEventType.ERROR.value, f"Failed to set setting: {e}")
            return {}

    async def get_setting(self, setting_manifest_id) -> dict:
        """
        Retrieves the setting for the provided manifest ID.

        Args:
            setting_manifest_id (str): The manifest ID for the setting.

        Returns:
            dict: The setting.
        """
        try:
            local_logger.info(f"Getting setting, {setting_manifest_id} from server")
            setting = find_setting_by_manifest_id(self.manifest_service.manifest, setting_manifest_id)
            return await self.manifest_service.get_setting(setting["id"])
        except Exception as e:
            local_logger.error(f"Failed to get setting: {e}")
            self.emit(SocketEventType.ERROR.value, f"Failed to get setting: {e}")
            return {}

    async def create_event(self, event_name: str, event_data: dict):
        """
        Sends a user interaction event to Gumband.

        Args:
            event_name (str): The name of the event.
            event_data (dict): The metadata associated with the event.
        """
        try:
            local_logger.info(f"Sending user event, {event_name}, to Gumband")
            return await self.user_event_service.create_event(event_name, event_data)
        except Exception as e:
            local_logger.error(f"User event creation failed: {e}")
            self.emit(SocketEventType.ERROR.value, f"Failed to create user event: {e}")

    async def end_interaction(self, event_data: dict):
        """
        Sends a user event to end the current interaction with Gumband.

        Args:
            event_data (dict): The metadata associated with ending the interaction.
        """
        try:
            local_logger.info("Sending a user event to end the interaction to Gumband")
            return await self.user_event_service.end_interaction(event_data)
        except Exception as e:
            local_logger.error(f"User event for ending the interaction failed: {e}")
            self.emit(SocketEventType.ERROR.value, f"Failed to create an event to end the interaction: {e}")

    async def send_email_notification(self, message: str):
        """
        Sends an email notification to users subscribed to "Custom Email Notifications" in the Gumband UI.

        Args:
            message (str): The content of the email message.
        """
        try:
            local_logger.info("Dispatching an email notification to subscribed users")
            return await self.notification_service.send_email(message)
        except Exception as e:
            local_logger.error(f"Email notification failed to send: {e}")
            self.emit(SocketEventType.ERROR.value, f"Email notification failed to send: {e}")

    async def sync_content_files(self):
        """
        Syncs the local and remote content files, pulling any missing remote files.
        """
        try:
            await self.content_service.sync()
        except Exception as e:
            local_logger.error(f"Failed to sync content files: {e}")
            self.emit(SocketEventType.ERROR.value, f"Failed to sync content files: {e}")
