""" Eter-cli implementation """
import abc
import asyncio
import logging
import os
import signal
import sys
from asyncio import AbstractEventLoop
from functools import wraps
from threading import Thread
from typing import Any, Optional

from absl import app, flags
from marshmallow import EXCLUDE

from utils.ether_service.entities.data.kr_packet import KrPacketV2
from utils.ether_service.utils.json_func import json_loads
from utils.ws_client import WSClient


logger = logging.getLogger(__name__)


flags.DEFINE_string('station_id', None, "Station Id")
flags.DEFINE_list('station_ids', [], 'Station IDs')
flags.DEFINE_string('file', None, "File where to save the history")


def silence_event_loop_closed(func):
    """ prevent error when closing on windows """
    @wraps(func)
    def wrapper(_self, *_args, **_kwargs):
        """ wrapper """
        pass
    return wrapper


if sys.platform == "win32":
    from asyncio.proactor_events import _ProactorBasePipeTransport
    _ProactorBasePipeTransport.__del__ = silence_event_loop_closed(
        _ProactorBasePipeTransport.__del__
    )


class PacketLogger(abc.ABC):
    """ Abstract Logger """

    @abc.abstractmethod
    def log(self, kr_packet: KrPacketV2) -> None:
        """ Log packet """
        raise NotImplementedError()

    @abc.abstractmethod
    def save(self) -> None:
        """ Save data """
        raise NotImplementedError()


class CSVLogger(PacketLogger):
    """ CSVLogger """

    BREAK_LINE = "\r\n"

    def __init__(self, file_path: Optional[str] = None):
        self.file_path = file_path
        self.file = None
        self.records = 0

        if file_path is not None:
            self.file_path = file_path
            is_file_exists = os.path.exists(self.file_path)
            self.file = open(self.file_path, "a+")
            if not is_file_exists:
                self.write("; ".join(["Station ID", "Timestamp", "Frequency",
                                      "DoA", "Confidence", "Power", "Serial",
                                      "Radio Bearing",
                                      "Compass Offset"]) + self.BREAK_LINE)

    @staticmethod
    def serialize_data(data: Any) -> str:
        """ serialize data for the csv format """
        if isinstance(data, str):
            return data
        return str(data)

    # noinspection PyBroadException
    def write(self, data: str) -> None:
        """ Try write """
        if self.file is None:
            self.file = open(self.file_path, "a+")
        for _ in range(50):
            try:
                self.file.write(data)
                break
            except IOError:
                pass

        self.records += 1
        if self.records % 50 == 0:
            self.file.flush()

    def log(self, kr_packet: KrPacketV2) -> None:
        """ Log packet """
        self.write("; ".join([
            self.serialize_data(kr_packet.station_id),
            self.serialize_data(kr_packet.get_timestamp()),
            self.serialize_data(kr_packet.freq / 1000000),
            self.serialize_data(kr_packet.get_absolute_doa()),
            self.serialize_data(kr_packet.conf),
            self.serialize_data(kr_packet.power),
            self.serialize_data(kr_packet.serial),
            self.serialize_data(kr_packet.radio_bearing),
            self.serialize_data(kr_packet.compass_offset)
        ]) + self.BREAK_LINE)

    def save(self) -> None:
        """ Save data """
        if self.file is not None:
            self.file.flush()
            self.file.close()


def init_logging(filename: str = None, level: int = None) -> None:
    """ init logging on the app level """
    logging_config = {"format": "%(message)s",
                      "level": level or logging.DEBUG}
    if filename is not None:
        logging_config["filename"] = filename
    logging.getLogger().handlers = []
    logging.basicConfig(**logging_config)


def on_data_handler(packet_logger: PacketLogger):
    """ Handle data """

    def wr(data: bytes) -> None:
        """ On Data handler """
        message = json_loads(data)
        if message is None:
            logger.warning(f"Failed to parse message!")
            logger.debug(f"Failed to parse message!: {repr(data)}")
            return
        # noinspection PyBroadException
        try:
            kr_packet = KrPacketV2.load(message, unknown=EXCLUDE)
            station_ids = flags.FLAGS.station_ids
            if len(station_ids) > 0:
                for station_id in station_ids:
                    if station_id is None or \
                            station_id.lower() == kr_packet.station_id.lower():
                        logger.info(f"{kr_packet}")
                        packet_logger.log(kr_packet)
            else:
                station_id = flags.FLAGS.station_id
                if station_id is None or \
                        station_id.lower() == kr_packet.station_id.lower():
                    logger.info(f"{kr_packet}")
                    packet_logger.log(kr_packet)
        except Exception:
            logging.error("handle_message: Error!", exc_info=sys.exc_info())

    return wr


def stop_server(server: WSClient, packet_logger: PacketLogger,
                io_loop: AbstractEventLoop):
    """ Stop server and execute finish gracefully """

    def wr(_signum, _frame):
        """ We need a wraper to dynamicaly set server """

        def callback():
            """ callback """
            if server.is_running:
                logging.info("Stopping the service!")
                packet_logger.save()
                server.stop()
            else:
                logging.info("Service is already stopping!")

        io_loop.call_soon_threadsafe(callback)

    return wr


def main(_argv) -> int:
    """ main """
    # When main is called, means logging has been changed on the abseil level
    # Change geventwebsocket and asyncio log levels
    logging.getLogger("geventwebsocket").level = logging.WARNING
    logging.getLogger("asyncio").level = logging.INFO

    # change default level to INFO
    init_logging(level=logging.INFO)

    io_loop = asyncio.get_event_loop()

    packet_logger = CSVLogger(flags.FLAGS.file)

    ws_client = WSClient(io_loop=io_loop, scheme="https", host="cds.s1z.info",
                         path="/api/v1/ws-agg",
                         user="kraken", password="kraken", ca_file="./ca.pem",
                         on_data_handler=on_data_handler(packet_logger))
    ws_client.start()
    signal.signal(signal.SIGINT, stop_server(ws_client, packet_logger,
                                             io_loop))
    signal.signal(signal.SIGTERM, stop_server(ws_client, packet_logger,
                                              io_loop))
    thread = Thread(target=io_loop.run_forever)
    thread.start()
    try:
        while True:
            command = input().strip().lower()[:1]
            if command == "q":
                stop_server(ws_client, packet_logger, io_loop)(None, None)
                thread.join()
                break
    except KeyboardInterrupt:
        stop_server(ws_client, packet_logger, io_loop)(None, None)
    return 0


if __name__ == "__main__":
    app.run(main)
