import copy
import logging
import threading
from queue import Empty, Queue
from threading import Event

from . import utils
from .callbacks import ReconnectionCallback, SubscribeCallback
from .endpoints.presence.heartbeat import Heartbeat
from .endpoints.presence.leave import Leave
from .endpoints.pubsub.subscribe import Subscribe
from .enums import (
    PNHeartbeatNotificationOptions,
    PNOperationType,
    PNReconnectionPolicy,
    PNStatusCategory,
)
from .managers import (
    PublishSequenceManager,
    ReconnectionManager,
    SubscriptionManager,
    TelemetryManager,
)
from .models.consumer.common import PNStatus
from .pnconfiguration import PNConfiguration
from .pubnub_core import PubNubCore
from .request_handlers.base import BaseRequestHandler
from .request_handlers.requests_handler import RequestsRequestHandler
from .structures import PlatformOptions
from .workers import SubscribeMessageWorker

logger = logging.getLogger("pubnub")


class PubNub(PubNubCore):
    """PubNub Python API"""

    def __init__(self, config):
        assert isinstance(config, PNConfiguration)
        PubNubCore.__init__(self, config)
        self._request_handler = RequestsRequestHandler(self)

        if self.config.enable_subscribe:
            self._subscription_manager = NativeSubscriptionManager(self)

        self._publish_sequence_manager = PublishSequenceManager(PubNubCore.MAX_SEQUENCE)

        self._telemetry_manager = NativeTelemetryManager()

    def sdk_platform(self):
        return ""

    def set_request_handler(self, handler):
        assert isinstance(handler, BaseRequestHandler)
        self._request_handler = handler

    def request_sync(self, endpoint_call_options):
        platform_options = PlatformOptions(self.headers, self.config)

        self.merge_in_params(endpoint_call_options)

        if self.config.log_verbosity:
            print(endpoint_call_options)

        return self._request_handler.sync_request(
            platform_options, endpoint_call_options
        )

    def request_async(
        self, endpoint_name, endpoint_call_options, callback, cancellation_event
    ):
        platform_options = PlatformOptions(self.headers, self.config)

        self.merge_in_params(endpoint_call_options)

        if self.config.log_verbosity:
            print(endpoint_call_options)

        return self._request_handler.async_request(
            endpoint_name,
            platform_options,
            endpoint_call_options,
            callback,
            cancellation_event,
        )

    def merge_in_params(self, options):
        params_to_merge_in = {}

        if options.operation_type == PNOperationType.PNPublishOperation:
            params_to_merge_in["seqn"] = (
                self._publish_sequence_manager.get_next_sequence()
            )

        options.merge_params_in(params_to_merge_in)

    def stop(self):
        if self._subscription_manager is not None:
            self._subscription_manager.stop()
        else:
            raise Exception("Subscription manager is not enabled for this instance")

    def request_deferred(self, options_func):
        raise NotImplementedError

    def request_future(self, *args, **kwargs):
        raise NotImplementedError


class NativeReconnectionManager(ReconnectionManager):
    def __init__(self, pubnub):
        super().__init__(pubnub)

    def _register_heartbeat_timer(self):
        self.stop_heartbeat_timer()

        self._recalculate_interval()

        self._timer = threading.Timer(self._timer_interval, self._call_time)
        self._timer.daemon = True
        self._timer.start()

    def _call_time(self):
        self._pubnub.time().pn_async(self._call_time_callback)

    def _call_time_callback(self, resp, status):
        if not status.is_error():
            self._connection_errors = 1
            self.stop_heartbeat_timer()
            self._callback.on_reconnect()
            logger.debug(
                "reconnection manager stop due success time endpoint call: %s"
                % utils.datetime_now()
            )
        elif self._pubnub.config.reconnect_policy == PNReconnectionPolicy.EXPONENTIAL:
            logger.debug("reconnect interval increment at: %s" % utils.datetime_now())
            self.stop_heartbeat_timer()
            self._connection_errors += 1
            self._register_heartbeat_timer()
        elif self._pubnub.config.reconnect_policy == PNReconnectionPolicy.LINEAR:
            self.stop_heartbeat_timer()
            self._connection_errors += 1
            self._register_heartbeat_timer()

    def start_polling(self):
        if self._pubnub.config.reconnect_policy == PNReconnectionPolicy.NONE:
            logger.warning(
                "reconnection policy is disabled, please handle reconnection manually."
            )
            return

        logger.debug("reconnection manager start at: %s" % utils.datetime_now())

        self._register_heartbeat_timer()

    def stop_heartbeat_timer(self):
        if self._timer is not None:
            self._timer.cancel()


class NativePublishSequenceManager(PublishSequenceManager):
    def __init__(self, provided_max_sequence):
        super().__init__(provided_max_sequence)
        self._lock = threading.Lock()

    def get_next_sequence(self):
        with self._lock:
            if self.max_sequence == self.next_sequence:
                self.next_sequence = 1
            else:
                self.next_sequence += 1

            return self.next_sequence


class NativeSubscriptionManager(SubscriptionManager):
    def __init__(self, pubnub_instance):
        subscription_manager = self

        self._message_queue = Queue()
        self._consumer_event = threading.Event()
        self._subscribe_call = None
        self._heartbeat_periodic_callback = None
        self._reconnection_manager = NativeReconnectionManager(pubnub_instance)

        super().__init__(pubnub_instance)
        self._start_worker()

        class NativeReconnectionCallback(ReconnectionCallback):
            def on_reconnect(self):
                subscription_manager.reconnect()

                pn_status = PNStatus()
                pn_status.category = PNStatusCategory.PNReconnectedCategory
                pn_status.error = False

                subscription_manager._subscription_status_announced = True
                subscription_manager._listener_manager.announce_status(pn_status)

        self._reconnection_listener = NativeReconnectionCallback()
        self._reconnection_manager.set_reconnection_listener(
            self._reconnection_listener
        )

    def _send_leave(self, unsubscribe_operation):
        def leave_callback(result, status):
            self._listener_manager.announce_status(status)

        Leave(self._pubnub).channels(unsubscribe_operation.channels).channel_groups(
            unsubscribe_operation.channel_groups
        ).pn_async(leave_callback)

    def _register_heartbeat_timer(self):
        super()._register_heartbeat_timer()

        self._perform_heartbeat_loop()

        self._heartbeat_periodic_callback = NativePeriodicCallback(
            self._perform_heartbeat_loop, self._pubnub.config.heartbeat_interval
        )

        if not self._should_stop:
            self._heartbeat_periodic_callback.start()

    def _perform_heartbeat_loop(self):
        if self._heartbeat_call is not None:
            # TODO: cancel call
            pass

        state_payload = self._subscription_state.state_payload()
        presence_channels = self._subscription_state.prepare_channel_list(False)
        presence_groups = self._subscription_state.prepare_channel_group_list(False)

        if len(presence_channels) == 0 and len(presence_groups) == 0:
            return

        def heartbeat_callback(raw_result, status):
            heartbeat_verbosity = self._pubnub.config.heartbeat_notification_options
            if status.is_error():
                if heartbeat_verbosity in (
                    PNHeartbeatNotificationOptions.ALL,
                    PNHeartbeatNotificationOptions.FAILURES,
                ):
                    self._listener_manager.announce_status(status)
            else:
                if heartbeat_verbosity == PNHeartbeatNotificationOptions.ALL:
                    self._listener_manager.announce_status(status)

        try:
            (
                Heartbeat(self._pubnub)
                .channels(presence_channels)
                .channel_groups(presence_groups)
                .state(state_payload)
                .pn_async(heartbeat_callback)
            )
        except Exception as e:
            logger.error("Heartbeat request failed: %s" % e)

    def _stop_heartbeat_timer(self):
        if self._heartbeat_periodic_callback is not None:
            self._heartbeat_periodic_callback.stop()

    def _set_consumer_event(self):
        self._consumer_event.set()
        self._message_queue_put(None)

    def _message_queue_put(self, message):
        self._message_queue.put(message)

    def reconnect(self):
        self._should_stop = False
        self._start_subscribe_loop()
        # Check the instance flag to determine if we want to perform the presence heartbeat
        # This is False by default
        if self._pubnub.config.enable_presence_heartbeat is True:
            self._register_heartbeat_timer()

    def disconnect(self):
        self._should_stop = True
        self._stop_heartbeat_timer()
        self._stop_subscribe_loop()

    def _start_worker(self):
        consumer = NativeSubscribeMessageWorker(
            self._pubnub,
            self._listener_manager,
            self._message_queue,
            self._consumer_event,
        )
        self._consumer_thread = threading.Thread(
            target=consumer.run, name="SubscribeMessageWorker"
        )
        self._consumer_thread.daemon = True
        self._consumer_thread.start()

    def _start_subscribe_loop(self):
        self._stop_subscribe_loop()

        combined_channels = self._subscription_state.prepare_channel_list(True)
        combined_groups = self._subscription_state.prepare_channel_group_list(True)

        if len(combined_channels) == 0 and len(combined_groups) == 0:
            return

        def callback(raw_result, status):
            """SubscribeEndpoint callback"""
            if status.is_error():
                if status and status.category == PNStatusCategory.PNCancelledCategory:
                    return

                if (
                    status.category is PNStatusCategory.PNTimeoutCategory
                    and not self._should_stop
                ):
                    self._start_subscribe_loop()
                    return

                logger.error(
                    "Exception in subscribe loop: %s" % str(status.error_data.exception)
                )

                if (
                    status
                    and status.category == PNStatusCategory.PNAccessDeniedCategory
                ):
                    status.operation = PNOperationType.PNUnsubscribeOperation
                    self._listener_manager.announce_status(status)
                    self.unsubscribe_all()
                    self.disconnect()
                    return

                self._listener_manager.announce_status(status)
                self._reconnection_manager.start_polling()
                self.disconnect()
            else:
                self._handle_endpoint_call(raw_result, status)
                self._start_subscribe_loop()

        try:
            self._subscribe_call = (
                Subscribe(self._pubnub)
                .channels(combined_channels)
                .channel_groups(combined_groups)
                .timetoken(self._timetoken)
                .region(self._region)
                .filter_expression(self._pubnub.config.filter_expression)
                .pn_async(callback)
            )
        except Exception as e:
            logger.error("Subscribe request failed: %s" % e)

    def _stop_subscribe_loop(self):
        sc = self._subscribe_call

        if sc is not None and not sc.is_executed and not sc.is_canceled:
            sc.cancel()


class NativePeriodicCallback:
    def __init__(self, callback, callback_time):
        self._callback = callback
        self._callback_time = callback_time
        self._running = False
        self._timeout = None

    def start(self):
        self._running = True
        self._schedule_next()

    def stop(self):
        self._running = False
        if self._timeout is not None:
            self._timeout.cancel()
            self._timeout = None

    def _run(self):
        if not self._running:
            return
        try:
            self._callback()
        except Exception:
            # TODO: handle the exception
            pass
        finally:
            self._schedule_next()

    def _schedule_next(self):
        self._timeout = threading.Timer(self._callback_time, self._run)
        self._timer.daemon = True
        self._timeout.start()


class NativeSubscribeMessageWorker(SubscribeMessageWorker):
    def _take_message(self):
        while not self._event.is_set():
            try:
                # TODO: get rid of 1s timeout
                msg = self._queue.get(True, 1)
                if msg is not None:
                    self._process_incoming_payload(msg)
                self._queue.task_done()
            except Empty:
                continue
            except Exception as e:
                # TODO: move to finally
                self._queue.task_done()
                self._event.set()
                logger.error("take message interrupted: %s" % str(e))
                raise


class SubscribeListener(SubscribeCallback):
    def __init__(self):
        self.connected = False
        self.connected_event = Event()
        self.disconnected_event = Event()
        self.presence_queue = Queue()
        self.message_queue = Queue()
        self.channel_queue = Queue()
        self.uuid_queue = Queue()
        self.membership_queue = Queue()

    def status(self, pubnub, status):
        if utils.is_subscribed_event(status) and not self.connected_event.is_set():
            self.connected_event.set()
        elif (
            utils.is_unsubscribed_event(status) and not self.disconnected_event.is_set()
        ):
            self.disconnected_event.set()

    def message(self, pubnub, message):
        self.message_queue.put(message)

    def presence(self, pubnub, presence):
        self.presence_queue.put(presence)

    def wait_for_connect(self):
        if not self.connected_event.is_set():
            self.connected_event.wait()
        else:
            raise Exception("the instance is already connected")

    def channel(self, pubnub, channel):
        self.channel_queue.put(channel)

    def uuid(self, pubnub, uuid):
        self.uuid_queue.put(uuid)

    def membership(self, pubnub, membership):
        self.membership_queue.put(membership)

    def wait_for_disconnect(self):
        if not self.disconnected_event.is_set():
            self.disconnected_event.wait()
        else:
            raise Exception("the instance is already disconnected")

    def wait_for_message_on(self, *channel_names):
        channel_names = list(channel_names)
        while True:
            env = self.message_queue.get()
            self.message_queue.task_done()
            if env.channel in channel_names:
                return env
            else:
                continue

    def wait_for_presence_on(self, *channel_names):
        channel_names = list(channel_names)
        while True:
            env = self.presence_queue.get()
            self.presence_queue.task_done()
            if env.channel in channel_names:
                return env
            else:
                continue


class NonSubscribeListener:
    def __init__(self):
        self.result = None
        self.status = None
        self.done_event = Event()

    def callback(self, result, status):
        self.result = result
        self.status = status
        self.done_event.set()

    def pn_await(self, timeout=5):
        """Returns False if a timeout happened, otherwise True"""
        return self.done_event.wait(timeout)

    def await_result(self, timeout=5):
        self.pn_await(timeout)
        return self.result

    def await_result_and_reset(self, timeout=5):
        self.pn_await(timeout)
        cp = copy.copy(self.result)
        self.reset()
        return cp

    def reset(self):
        self.result = None
        self.status = None
        self.done_event.clear()


class NativeTelemetryManager(TelemetryManager):
    def store_latency(self, latency, operation_type):
        super().store_latency(latency, operation_type)
        self.clean_up_telemetry_data()
