import json
import logging
import os
import sys
import time
from typing import Dict, List, Optional

import msgbus
from msgbus import MBContext, MBService

from ._BaseLocalProcess import BaseLocalProcess

logger = logging.getLogger(__name__)


class LocalComponent(BaseLocalProcess):

    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,
        coverage: bool = False,
    ) -> None:
        super().__init__(
            component_name=component_name,
            address=address,
            port=port,
            main_path=main_path or os.path.join("src", "main.py"),
            comp_dir=comp_dir or os.path.abspath(os.path.join("..")),
            debug_streams=debug_streams,
        )
        self.hv_port = hv_port
        self.config = config or "{}"
        self.log_config = log_config or default_log_config()
        self.folders_to_add_in_python_path = folders_to_add_in_python_path or []
        self.bundle_dir = bundle_dir
        self.process_args = process_args or []
        self._coverage = coverage

    def _build_env(self) -> dict:
        src_folders = list(dict.fromkeys(["src", "lib", "api"] + self.folders_to_add_in_python_path))

        if self._coverage_rcfile() and "dev_lib" not in src_folders:
            src_folders.append("dev_lib")

        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

        return env

    def _build_process_args(self) -> List[str]:
        coverage_rcfile = self._coverage_rcfile()
        if coverage_rcfile:
            return [
                sys.executable,
                "-m", "coverage", "run",
                "--parallel-mode",
                f"--rcfile={coverage_rcfile}",
                os.path.join(self.comp_dir, self.main_path),
            ] + self.process_args

        return super()._build_process_args() + self.process_args

    def _coverage_rcfile(self) -> Optional[str]:
        if self._coverage:
            return os.environ.get("COVERAGE_PROCESS_START")

        return None

    def _wait_for_start(self):
        perform_easy_wait_start(
            component_name=self.component_name,
            comp_port=self.port,
            hv_port=self.hv_port,
            address=self.address,
        )


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 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,
    }
