import signal as os_signal
import os
import signal
import time
import grpc
import ntpath
import posixpath
import binascii
import tempfile
import zipfile
import queue
from threading import Thread
import remotivelabs.broker.sync as br
import remotivelabs.broker.sync.helper as br_helper
import typer
from typing import List
import remotivelabs.broker.generated.sync.traffic_api_pb2 as traffic_api


class Broker:

    def __init__(self, url, api_key):
        self.url = url
        self.api_key = api_key
        self.q = queue.Queue()
        """Main function, checking arguments passed to script, setting up stubs, configuration and starting Threads."""
        # Setting up stubs and configuration
        self.intercept_channel = br.create_channel(url, api_key)

        self.network_stub = br.network_api_pb2_grpc.NetworkServiceStub(self.intercept_channel)
        self.system_stub = br.system_api_pb2_grpc.SystemServiceStub(self.intercept_channel)
        self.traffic_stub = br.traffic_api_pb2_grpc.TrafficServiceStub(self.intercept_channel)
        self.signal_creator = br.SignalCreator(self.system_stub)

    def play(self, recording_and_namespace: List):

        def to_playback(rec):
            return {
                "namespace": rec['namespace'],
                "path": rec['recording'],
                "mode": br.traffic_api_pb2.Mode.PLAY,
            }

        playback_list = map(to_playback, recording_and_namespace)

        status = self.traffic_stub.PlayTraffic(
            br.traffic_api_pb2.PlaybackInfos(
                playbackInfo=list(map(self.__create_playback_config, playback_list))
            )
        )

    def stop_play(self, recording_and_namespace: List):

        def to_playback(rec):
            return {
                "namespace": rec['namespace'],
                "path": rec['recording'],
                "mode": br.traffic_api_pb2.Mode.STOP,
            }

        playback_list = map(to_playback, recording_and_namespace)

        status = self.traffic_stub.PlayTraffic(
            br.traffic_api_pb2.PlaybackInfos(
                playbackInfo=list(map(self.__create_playback_config, playback_list))
            )
        )

    def pause_play(self, recording_and_namespace: List):

        def to_playback(rec):
            return {
                "namespace": rec['namespace'],
                "path": rec['recording'],
                "mode": br.traffic_api_pb2.Mode.PAUSE,
            }

        playback_list = map(to_playback, recording_and_namespace)

        status = self.traffic_stub.PlayTraffic(
            br.traffic_api_pb2.PlaybackInfos(
                playbackInfo=list(map(self.__create_playback_config, playback_list))
            )
        )
            # print(status)

    def record_multiple(self, namespaces: List[str], path: str):

        def to_playback(namespace):
            return {
                "namespace": namespace,
                "path": path + "_" + namespace,
                "mode": br.traffic_api_pb2.Mode.RECORD,
            }

        playback_list = list(map(to_playback, namespaces))

        status = self.traffic_stub.PlayTraffic(
            br.traffic_api_pb2.PlaybackInfos(
                playbackInfo=list(map(self.__create_playback_config, playback_list))
            )
        )

    def record(self, namespace: str, path: str):
        playback_list = [
            {
                "namespace": namespace,
                "path": path,
                "mode": br.traffic_api_pb2.Mode.RECORD,
            }]

        status = self.traffic_stub.PlayTraffic(
            br.traffic_api_pb2.PlaybackInfos(
                playbackInfo=list(map(self.__create_playback_config, playback_list))
            )
        )
        # print(status)

    def stop(self, namespace: str, path: str):
        playback_list = [
            {
                "namespace": namespace,
                "path": path,
                "mode": br.traffic_api_pb2.Mode.STOP,
            }]

        status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
            br.traffic_api_pb2.PlaybackInfos(
                playbackInfo=list(map(self.__create_playback_config, playback_list))
            )
        )
        mode = status.playbackInfo[0].playbackMode
        if mode.errorMessage:
            print(f'Error stopping recording {mode.errorMessage}')
            raise typer.Exit(1)

    def pause(self, namespace: str, path: str):
        playback_list = [
            {
                "namespace": namespace,
                "path": path,
                "mode": br.traffic_api_pb2.Mode.PAUSE,
            }]

        status: traffic_api.PlaybackInfos = self.traffic_stub.PlayTraffic(
            br.traffic_api_pb2.PlaybackInfos(
                playbackInfo=list(map(self.__create_playback_config, playback_list))
            )
        )
        mode = status.playbackInfo[0].playbackMode
        if mode.errorMessage:
            print(f'Error pausing recording {mode.errorMessage}')
            raise typer.Exit(1)

    def stop_multiple(self, namespaces: List[str], path: str):
        def to_playback(namespace):
            return {
                "namespace": namespace,
                "path": path + "_" + namespace,
                "mode": br.traffic_api_pb2.Mode.STOP,
            }

        playback_list = list(map(to_playback, namespaces))

        status = self.traffic_stub.PlayTraffic(
            br.traffic_api_pb2.PlaybackInfos(
                playbackInfo=list(map(self.__create_playback_config, playback_list))
            )
        )
        mode = status.playbackInfo[0].playbackMode
        if mode.errorMessage:
            print(f'Error stopping recording {mode.errorMessage}')
            raise typer.Exit(1)

    def diagnose_stop(self, namespace: List[str]):
        recording_name = f"diagnose__"
        self.stop_multiple(namespace, recording_name)

    def diagnose(self, namespace: List[str], wait_for_traffic: bool = False):
        recording_name = f"diagnose__"

        keep_running = True
        keep_running_during_recording = True
        def exit_on_ctrlc(sig, frame):
            nonlocal keep_running
            keep_running = False
            nonlocal keep_running_during_recording
            keep_running_during_recording = False
            #progress.add_task(description=f"Cleaning up, please wait...", total=None)

        os_signal.signal(os_signal.SIGINT, exit_on_ctrlc)

        while keep_running:
            keep_running = wait_for_traffic
            self.record_multiple(namespace, recording_name)
            for i in range(5):
                if keep_running_during_recording:
                    time.sleep(1)

            self.stop_multiple(namespace, recording_name)

            response = []
            with tempfile.TemporaryDirectory() as tmpdirname:
                for ns in namespace:
                    path = recording_name + "_" + ns
                    tmp_file = os.path.join(tmpdirname, path)
                    self.download(path, tmp_file)
                    self.delete_files([path], False)
                    with zipfile.ZipFile(tmp_file, 'r') as zip_ref:
                        zip_ref.extractall(tmpdirname)

                        file_stat = os.stat(os.path.join(tmpdirname, path + '.raw'))
                        response.append({'namespace': ns, 'data': file_stat.st_size > 0})

            for r in response:
                if r['data']:
                    print(f'Successfully received traffic on {r["namespace"]}')
                    keep_running = False
                else:
                    if not wait_for_traffic or (not keep_running and not keep_running_during_recording):
                        print(f'Namespace {r["namespace"]} did not receive any traffic')



    def upload(self, file: str, dest: str):
        try:
            br.helper.upload_file(system_stub=self.system_stub, path=file, dest_path=dest)
        except grpc.RpcError as rpc_error:
            print(f"Failed to upload file - {rpc_error.details()} ({rpc_error.code()})")
            raise typer.Exit(1)


    def delete_files(self,
            path: List[str],
                     exitOnFaliure:bool
    ) -> None:

            for file in path:
                try:
                    self.system_stub.BatchDeleteFiles(
                        br.system_api_pb2.FileDescriptions(
                                fileDescriptions=[br.system_api_pb2.FileDescription(path=file.replace(ntpath.sep,
                                                                                                   posixpath.sep))]
                            )
                    )
                except grpc.RpcError as rpc_error:
                    print(f"Failed to delete file - {rpc_error.details()} ({rpc_error.code()})")
                    if exitOnFaliure:
                        raise typer.Exit(1)

        #rpc BatchDeleteFiles (FileDescriptions) returns (Empty) {}

    def upload_folder(self, folder: str):
        try:
            br.helper.upload_folder(system_stub=self.system_stub, folder=folder)
        except grpc.RpcError as rpc_error:
            print(f"Failed to upload file - {rpc_error.details()} ({rpc_error.code()})")
            raise typer.Exit(1)

    def download(self, file: str, dest: str):
        try:
            br_helper.download_file(system_stub=self.system_stub, path=file, dest_path=dest)
        except grpc.RpcError as rpc_error:
            print(f"Failed to download file - {rpc_error.details()} ({rpc_error.code()})")
            # There will be an empty file if the download fails so remove that one here
            os.remove(dest)
            raise typer.Exit(1)

    def reload_config(self):
        try:
            request = br.common_pb2.Empty()
            response = self.system_stub.ReloadConfiguration(request, timeout=60000) 
            if response.errorMessage:
                print(f"Failed to reload config: {response.errorMessage}")
                raise typer.Exit(1)
            #br.helper.reload_configuration(system_stub=self.system_stub)
        except grpc.RpcError as rpc_error:
            print(f"Failed to reload configuration - {rpc_error.details()} ({rpc_error.code()})")
            raise typer.Exit(1)

    def list_namespaces(self):
        # Lists available signals
        configuration = self.system_stub.GetConfiguration(br.common_pb2.Empty())
        namespaces = []
        for networkInfo in configuration.networkInfo:
            namespaces.append(networkInfo.namespace.name)
        return namespaces

    def list_signal_names(self, namespace):
        # Lists available signals
        configuration = self.system_stub.GetConfiguration(br.common_pb2.Empty())

        signal_names = []
        for networkInfo in configuration.networkInfo:
            res = self.system_stub.ListSignals(networkInfo.namespace)
            for finfo in res.frame:
                for sinfo in finfo.childInfo:
                    signal_names.append({'signal': sinfo.id.name, 'namespace': networkInfo.namespace.name})
        return signal_names

    def list_signal_names(self):
        # Lists available signals
        configuration = self.system_stub.GetConfiguration(br.common_pb2.Empty())

        signal_names = []
        for networkInfo in configuration.networkInfo:
            res = self.system_stub.ListSignals(networkInfo.namespace)
            for finfo in res.frame:
                for sinfo in finfo.childInfo:
                    signal_names.append({'signal': sinfo.id.name, 'namespace': networkInfo.namespace.name})
        return signal_names

    def subscribe(self, signals: list, namespace, on_frame, changed_values_only: bool = True):
        client_id = br.common_pb2.ClientId(id="cloud_demo")

        signals_to_subscribe_on = \
            map(lambda signal: self.signal_creator.signal(signal, namespace), signals)

        Thread(
            target=br.act_on_signal,
            args=(
                client_id,
                self.network_stub,
                signals_to_subscribe_on,
                changed_values_only,  # True: only report when signal changes
                lambda frame: self.__each_signal(frame, on_frame),
                lambda sub: (self.q.put(("cloud_demo", sub))),
            ),
        ).start()
        # Wait for subscription
        ecu, subscription = self.q.get()
        return subscription

    @classmethod
    def connect(cls, url, api_key):
        return Broker(url, api_key)

    def __each_signal(self, signals, callback):
        callback(map(lambda s: {
            'timestamp_nanos': s.timestamp,
            'name': s.id.name,
            'value': self.__get_value(s)
        }, signals))

    @staticmethod
    def __get_value(signal):
        if signal.raw != b"":
            return "0x" + binascii.hexlify(signal.raw).decode("ascii")
        elif signal.HasField("integer"):
            return signal.integer
        elif signal.HasField("double"):
            return signal.double
        elif signal.HasField("arbitration"):
            return signal.arbitration
        else:
            return "empty"

    @staticmethod
    def __create_playback_config(item):
        """Creating configuration for playback

        Parameters
        ----------
        item : dict
            Dictionary containing 'path', 'namespace' and 'mode'

        Returns
        -------
        PlaybackInfo
            Object instance of class

        """
        playbackConfig = br.traffic_api_pb2.PlaybackConfig(
            fileDescription=br.system_api_pb2.FileDescription(path=item["path"]),
            namespace=br.common_pb2.NameSpace(name=item["namespace"]),
        )
        return br.traffic_api_pb2.PlaybackInfo(
            playbackConfig=playbackConfig,
            playbackMode=br.traffic_api_pb2.PlaybackMode(mode=item["mode"]),
        )


Broker.connect = classmethod(Broker.connect)
