import signal
import os
import re
import urllib.parse
from random import randint
from typing import Optional
from threading import Thread
from dotenv import load_dotenv
from contextlib import suppress
from urllib.parse import urlencode
from time import sleep as delay, time
from json import loads, dumps, JSONDecodeError
load_dotenv


from .entities import *
from .context import EventHandler
from .dispatcher import MessageDispatcher

proxy_url = (
    os.getenv('ALL_PROXY') or
    os.getenv('all_proxy') or
    os.getenv('HTTPS_PROXY') or
    os.getenv('HTTP_PROXY')
)

if orjson_exists():
    from orjson import (
        loads as orjson_loads, dumps as orjson_dumps
        )

try:
    from websocket import WebSocket, WebSocketApp
except ImportError as e:
    pipmain(["uninstall", "websocket", "-y"])
    pipmain(["install", "websocket-client==1.6.1"])
    raise WrongWebSocketPackage from e


class WSClient(EventHandler):
    """
    WSClient class that handles websocket events.

    This class extends `EventHandler` class, allowing the bot to use event handler features.

    Special Attributes:
    __slots__ : tuple
        A tuple containing a fixed set of attributes to optimize memory usage.

    Attributes:
    ws : WebSocketApp
        The websocket object.
    _communities : set
        A set containing the communities the bot is in.
    event_types : dict
        A dictionary containing the event types.
    notif_types : dict
        A dictionary containing the notification types.
    dispatcher : MessageDispatcher
        The message dispatcher object.
    channel : Optional[Channel] 
        The agora channel.
    orjson : bool
        Whether or not orjson is installed.

    """
    __slots__ = (
        "ws",
        "_communities",
        "event_types",
        "is_logging",
        "notif_types",
        "dispatcher",
        "channel",
        "orjson"
    )
    def __init__(self, **kwargs):
        global proxy_url
        self.ws:            WebSocketApp = None
        self._communities:  set = set()
        self.event_types:   dict =  EventTypes().events
        self.is_logging:    bool = bool(self.logger)
        self.notif_types:   dict =  NotifTypes().notifs
        self.dispatcher:    MessageDispatcher = MessageDispatcher()
        self.channel:       Optional[Channel] = None
        self.orjson:        bool = orjson_exists()

        self.dispatcher.register(10, self._handle_notification)
        self.dispatcher.register(201, self._handle_agora_channel)
        self.dispatcher.register(400, self._handle_user_online)
        self.dispatcher.register(1000, self._handle_message)
        
        if kwargs.get("proxy", None):
            proxy_url = kwargs.get("proxy")

        signal.signal(signal.SIGINT, signal.SIG_DFL)
        EventHandler.__init__(self)

    def fetch_ws_url(self) -> str:
        return f"wss://ws{randint(1, 4)}.aminoapps.com"
    
    def _log(self, message: str) -> None:
        """
        Logs a message to debug.log

        :param message: The message to log.
        :type message: str
        :return: None

        """
        if self.is_logging:
            try:
                self.logger.debug(message)
            except Exception:
                self.is_logging = False

    def connect(self) -> None:
        """Connects to the websocket."""
        self.run_forever()
        return self.emit("ready")

    def run_forever(self) -> None:
        """Runs the websocket forever."""
        self._log("Initializing websocket.")
        ws_data = f"{self.generate.device_id()}|{int(time() * 1000)}"
        self.ws = WebSocketApp(
            url = f"{self.fetch_ws_url()}/?{urlencode({'signbody': ws_data})}",
            on_open=self.on_websocket_open,
            on_message=self.on_websocket_message,
            on_error=self.on_websocket_error,
            on_close=self.on_websocket_close,
            header={
            "NDCDEVICEID": self.generate.device_id(),
            "NDCAUTH": f"sid={self.sid}",
            "NDC-MSG-SIG": self.generate.signature(ws_data)
            })
        
        self.start_processes()
        return self._log("Websocket connected.")

    def start_processes(self) -> None:
        """Starts the websocket processes."""
        websocket_thread = Thread(
                target=self.ws.run_forever,
                kwargs = {
                    "proxy_type": urllib.parse.urlparse(proxy_url).scheme if proxy_url else None,
                    "http_proxy_host": urllib.parse.urlparse(proxy_url).hostname if proxy_url else None,
                    "http_proxy_port": urllib.parse.urlparse(proxy_url).port if proxy_url else None,
                    "http_proxy_auth": (urllib.parse.urlparse(proxy_url).username, urllib.parse.urlparse(proxy_url).password) if proxy_url else None,
                }
            )
        websocket_thread.start()

        aalive_thread = Thread(target=run_alive_loop, args=(self,))
        aalive_thread.start()

    def on_websocket_error(self, ws: WebSocket, error: Exception) -> None:
        """Handles websocket errors."""
        with suppress(KeyError):
            self._events["error"](error)

        return self._log(f"Websocket error: {error}")

    def on_websocket_message(self, ws: WebSocket, message: str) -> None:
        """Receives websocket messages."""
        return Thread(target=self._handle_websocket_message, args=(message,)).start()

    def _handle_websocket_message(self, message: str) -> None:
        """Handles websocket messages."""        
        try:
            raw_message = orjson_loads(message) if self.orjson else loads(message)
        except JSONDecodeError:
            raw_message = loads(message)

        return self.dispatcher.handle(raw_message)

    def _handle_message(self, message: dict) -> None:
        """Sends the message to the event handler."""
        _message: Message = Message(message)

        if self.userId == _message.userId: return None
        None if any(
            [_message.ndcId is None, _message.ndcId == 0]
        ) else self._communities.add(_message.ndcId)

        key = self.event_types.get(f"{_message.type}:{_message.mediaType}")

        return self._handle_event(key, _message) if key else None

    def _handle_notification(self, message: dict) -> None:
        """Handles notifications."""
        notification: Notification = Notification(message)
        key = self.notif_types.get(notification.notification_type)
        
        return self._handle_event(key, notification) if key else None

    def _handle_agora_channel(self, message: dict) -> None:
        """Sets the agora channel."""
        self.channel: Channel = Channel(message)

    def _handle_user_online(self, message: dict) -> None:
        """Handles user online events."""
        return self._handle_event("user_online", OnlineMembers(message))

    def on_websocket_close(self, ws: WebSocket, close_status_code: int, close_msg: str) -> None:
        """Handles websocket close events."""
        if [close_status_code, close_msg] == [None, None]:
            self._log("Websocket closed unexpectedly.")
            return self.run_forever()

    def send_websocket_message(self, message: dict) -> None:
        """Sends a websocket message."""
        return self.ws.send(orjson_dumps(message).decode() if self.orjson else dumps(message))

    def stop_websocket(self) -> None:
        """Stops the websocket."""
        self._log("Websocket received stop signal.")
        return self.ws.close()

    def on_websocket_open(self, ws: WebSocket) -> None:
        """Handles websocket open events."""
        if all([self.community_id != None, "user_online" in self._events]):
            return self.send_websocket_message({
                "t": 300,
                "o": {
                    "ndcId": self.community_id,
                    "topic": f"ndtopic:x{self.community_id}:online-members",
                    "id": int(time() * 1000)
                }})

    def _last_active(self, last_activity_time: float) -> bool:
        """Returns True if the last activity was 5 minutes ago."""""
        return time() - last_activity_time >= 300

    def _last_message(self, last_message_time: float) -> bool:
        """Returns True if the last message was 30 seconds ago."""
        return time() - last_message_time >= 30

    def _send_message(self) -> None:
        self.send_websocket_message({
            "o":{
                "threadChannelUserInfoList": [],
                "id": randint(1, 100)},
                "t": 116
                })

    def _activity_status(self) -> None:
        """Sets the user's activity status to online."""
        for comId in self._communities:

            if self.online_status:
                try:
                    self.community.send_active(comId=comId,
                    timers=[{"start": int(time()), "end": int(time()) + 300}]
                    )
                except Exception:
                    self.online_status = False

            delay(randint(5, 10))