import json
import logging
import os
import subprocess
import sys
import time
from threading import Thread
from typing import (
    Dict,
    List,
    Optional,
)

import msgbus
from msgbus import (
    MBContext,
    MBService,
)

logger = logging.getLogger(__name__)


class LocalComponent(object):

    def __init__(
        self,
        component_name: str = "LocalComponent",
        address: str = "127.0.0.1",
        port: int = 17000,
        hv_port: int = 16000,
        main_path: Optional[str] = None,
        comp_dir: Optional[str] = None,
        config: Optional[str] = None,
        folders_to_add_in_python_path: Optional[List[str]] = None,
        debug_streams: bool = False,
        bundle_dir: Optional[str] = None,
        process_args: Optional[List[str]] = None,
        log_config: Optional[Dict] = None,
    ) -> None:

        self.component_name = component_name
        self.address = address
        self.port = port
        self.hv_port = hv_port
        self.config = config or "{}"
        self.log_config = log_config or default_log_config()
        self.debug_streams = debug_streams
        self.comp_dir = comp_dir or os.path.abspath(os.path.join(".."))
        self.folders_to_add_in_python_path = folders_to_add_in_python_path or []
        self.main_path = main_path or os.path.join("src", "main.py")
        self.bundle_dir = bundle_dir
        self.process_args = process_args or []

        self.process: Optional[subprocess.Popen] = None

    def start(self):
        if self.is_running():
            msg = f"Component {self.component_name} already running"
            logger.error(msg)
            raise RuntimeError(msg)

        logger.debug(f"Starting component {self.component_name}")

        src_folders = list(dict.fromkeys(["src", "lib", "api"] + self.folders_to_add_in_python_path))
        python_path = os.pathsep.join(
            os.path.join(self.comp_dir, folder) for folder in src_folders
        )

        env = os.environ.copy()
        env.update({
            "PYTHONPATH": python_path,
            "LOADERCFG": self.config,
            "LOG_PATH": json.dumps(self.log_config),
            "HVIP": self.address,
            "HVPORT": str(self.hv_port),
            "HVCOMPPORT": str(self.port),
        })

        if self.bundle_dir is not None:
            env["BUNDLEDIR"] = self.bundle_dir

        stdout = subprocess.DEVNULL
        stderr = subprocess.DEVNULL

        if self.debug_streams:
            stdout = subprocess.PIPE
            stderr = subprocess.PIPE

        process_args = [
            sys.executable,
            os.path.join(self.comp_dir, self.main_path),
        ] + self.process_args

        self.process = subprocess.Popen(
            args=process_args,
            stdout=stdout,
            stderr=stderr,
            env=env,
        )

        if self.debug_streams:
            stdout_thread = Thread(
                target=read_stream,
                args=(self.process.stdout, logger.info),
                daemon=True,
            )
            stderr_thread = Thread(
                target=read_stream,
                args=(self.process.stderr, logger.error),
                daemon=True,
            )

            stdout_thread.start()
            stderr_thread.start()

        try:
            perform_easy_wait_start(
                component_name=self.component_name,
                comp_port=self.port,
                hv_port=self.hv_port,
                address=self.address,
            )
        except Exception:
            logger.exception(f"Error starting component {self.component_name}")
            self.stop()
            raise

    def stop(self):
        if not self.process:
            logger.debug(f"Component {self.component_name} already stopped")
            return -1

        logger.debug(f"Stopping component {self.component_name}")

        try:
            stop_component(
                component_name=self.component_name,
                comp_port=self.port,
                address=self.address,
            )

            return self.process.wait(timeout=10)
        except (Exception,):
            logger.exception(f"Error stopping component {self.component_name}")
            self.process.terminate()
            raise
        finally:
            self.process = None

    def reload(self):
        logger.debug(f"Reloading component {self.component_name}")

        mb_context = MBContext(
            my_service=None,
            name="CompReloader"
        )
        try:
            other_service = MBService(self.address, self.port)

            reply = mb_context.MB_SendMessage(
                destination=other_service,
                token=msgbus.TK_CMP_RELOAD,
            )

            if reply.token != msgbus.TK_SYS_ACK:
                logger.error(f"Error sending TK_CMP_RELOAD for {self.component_name}")
                raise RuntimeError("Error sending TK_CMP_RELOAD")
        finally:
            mb_context.MB_Finalize()

    def recreate(self, new_config: str):
        logger.debug(f"Recreating component {self.component_name} with config: {new_config}")


        mb_context = MBContext(
            my_service=None,
            name="CompReloader",
        )
        try:
            other_service = MBService(self.address, self.port)
            reply = mb_context.MB_SendMessage(
                destination=other_service,
                token=msgbus.TK_CMP_RECREATE,
                data=new_config.encode(),
            )
            if reply.token != msgbus.TK_SYS_ACK:
                logger.error(f"Error sending TK_CMP_RECREATE for {self.component_name}")
                raise RuntimeError("Error sending TK_CMP_RECREATE")
        finally:
            mb_context.MB_Finalize()

    def is_running(self):
        return self.process is not None and self.process.poll() is None


def read_stream(stream, log_fn):
    if not stream:
        return

    try:
        for line in iter(stream.readline, b""):
            log_fn(line.decode(errors="replace").rstrip())
    except (Exception,):
        logger.debug("Stream reader terminated with error", exc_info=True)
    finally:
        stream.close()


def perform_easy_wait_start(
    component_name: str,
    comp_port: int,
    hv_port: int = 16000,
    address: str = "127.0.0.1",
    retries: int = 10,
    delay: float = 0.3,
) -> None:
    mb_context = MBContext(
        my_service=None,
        name="EasyWaitStart"
    )

    try:
        comp_service = MBService(address, comp_port)
        latest_exception = None

        for _ in range(retries):
            try:
                logger.debug(f"Requesting services for component {component_name}")
                reply = mb_context.MB_SendMessage(
                    destination=comp_service,
                    token=msgbus.TK_CMP_GETSERVICES,
                    format=msgbus.FM_PARAM,
                )
                break
            except Exception as ex:
                latest_exception = ex
                time.sleep(delay)
        else:
            raise RuntimeError(
                f"Error requesting services for component {component_name}"
            ) from latest_exception

        if reply.token != msgbus.TK_SYS_ACK:
            raise RuntimeError("Invalid reply requesting services")

        service_name, service_type = reply.data.decode().split(":", 1)

        hv_service = MBService(address, hv_port)
        mb_context.MB_RegisterService(
            my_service=comp_service,
            hv_service=hv_service,
            service_type=service_type,
            service_name=service_name,
        )

        reply = mb_context.MB_SendMessage(
            destination=comp_service,
            token=msgbus.TK_CMP_START,
            format=msgbus.FM_PARAM,
        )

        if reply.token != msgbus.TK_SYS_ACK:
            raise RuntimeError("Error sending TK_CMP_START")

        logger.debug(f"Component {component_name} successfully started")
    finally:
        mb_context.MB_Finalize()


def stop_component(
    component_name: str,
    comp_port: int,
    address: str = "127.0.0.1",
):
    logger.debug(f"Stopping component {component_name}")

    mb_context = MBContext(my_service=None, name="CompStopper")
    try:
        other_service = MBService(address, comp_port)

        reply = mb_context.MB_SendMessage(
            destination=other_service,
            token=msgbus.TK_CMP_TERM_NOW,
            format=msgbus.FM_PARAM,
        )

        if reply.token != msgbus.TK_CMP_TERM_NOW:
            raise RuntimeError("Error sending TK_CMP_TERM_NOW")
    finally:
        mb_context.MB_Finalize()

    logger.debug(f"Component {component_name} successfully stopped")


def default_log_config():
    return {
        "version": 1,
        "formatters": {
            "defaultFormatter": {
                "()": "helper.IsoFormatter"
            }
        },
        "handlers": {
            "stdout": {
                "class": "logging.StreamHandler",
                "formatter": "defaultFormatter",
                "level": "DEBUG",
                "stream": "ext://sys.stdout",
            },
            "stderr": {
                "class": "logging.StreamHandler",
                "formatter": "defaultFormatter",
                "level": "ERROR",
                "stream": "ext://sys.stderr",
            },
        },
        "loggers": {
            "msgbus": {
                "level": "INFO",
                "propagate": True,
            }
        },
        "root": {
            "level": "DEBUG",
            "handlers": [
                "stdout",
                "stderr"
            ]
        },
        "disable_existing_loggers": False,
    }
