import logging
import os
import subprocess
import sys
from threading import Thread
from typing import List, Optional

import msgbus
from msgbus import MBContext, MBService

logger = logging.getLogger(__name__)


class BaseLocalProcess:
    def __init__(
        self,
        component_name: str,
        address: str,
        port: int,
        main_path: str,
        comp_dir: str,
        debug_streams: bool,
    ) -> None:
        self.component_name = component_name
        self.address = address
        self.port = port
        self.main_path = main_path
        self.comp_dir = comp_dir
        self.debug_streams = debug_streams
        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}")

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

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

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

        try:
            self._wait_for_start()
        except Exception:
            logger.exception(f"Error starting component {self.component_name}")
            try:
                self.stop()
            except Exception:
                logger.exception(f"Error stopping component {self.component_name} after start failure")
            raise

    def _build_env(self) -> dict:
        raise NotImplementedError

    def _build_process_args(self) -> List[str]:
        return [sys.executable, os.path.join(self.comp_dir, self.main_path)]

    def _wait_for_start(self):
        raise NotImplementedError

    def stop(self) -> int:
        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}")
        self._send_component_message(msgbus.TK_CMP_RELOAD)

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

    def _send_component_message(self, token: int, data: Optional[bytes] = None) -> None:
        mb_context = MBContext(my_service=None, name="CompReloader")
        try:
            reply = mb_context.MB_SendMessage(
                destination=MBService(self.address, self.port),
                token=token,
                data=data,
            )
            if reply.token != msgbus.TK_SYS_ACK:
                raise RuntimeError(f"Unexpected reply for token 0x{token:08X}")
        finally:
            mb_context.MB_Finalize()

    def is_running(self) -> bool:
        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 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")
