import os
import subprocess
from collections import OrderedDict

import cothread
from annotypes import Anno

from malcolm import __version__
from malcolm.core import (
    Alarm,
    AlarmSeverity,
    BadValueError,
    ProcessStartHook,
    ProcessStopHook,
    StringMeta,
    Widget,
)
from malcolm.modules import builtin, ca
from malcolm.modules.ca.util import catools

from ..parts.dirparsepart import DirParsePart
from ..parts.iociconpart import IocIconPart


def await_ioc_start(stats, prefix):
    cothread.Yield()
    pid_rbv = catools.caget(f"{prefix}:PID", timeout=5)
    if int(pid_rbv) != os.getpid():
        raise BadValueError(
            "Got back different PID: "
            + "is there another system instance on the machine?"
        )
    catools.caput(
        f"{prefix}:YAML:PATH", stats["yaml_path"], datatype=catools.DBR_CHAR_STR
    )
    catools.caput(
        f"{prefix}:PYMALCOLM:PATH",
        stats["pymalcolm_path"],
        datatype=catools.DBR_CHAR_STR,
    )


def start_ioc(stats, prefix):
    db_macros = f"prefix='{prefix}'"
    try:
        epics_base = os.environ["EPICS_BASE"]
    except KeyError:
        raise BadValueError("EPICS base not defined in environment")
    softIoc_bin = epics_base + "/bin/linux-x86_64/softIoc"
    for key, value in stats.items():
        db_macros += f",{key}='{value}'"
    root = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0]
    db_template = os.path.join(root, "db", "system.template")
    ioc = subprocess.Popen(
        [softIoc_bin, "-m", db_macros, "-d", db_template],
        stdout=subprocess.PIPE,
        stdin=subprocess.PIPE,
    )
    cothread.Spawn(await_ioc_start, stats, prefix)
    return ioc


with Anno("prefix for self.system PVs"):
    APvPrefix = str
with Anno("space-separated list of IOCs to monitor"):
    AIocList = str


class ProcessController(builtin.controllers.ManagerController):
    def __init__(
        self,
        mri: builtin.controllers.AMri,
        prefix: APvPrefix,
        config_dir: builtin.controllers.AConfigDir,
        ioc_list: AIocList = "",
    ) -> None:
        super().__init__(mri, config_dir)
        self.ioc = None
        self.ioc_blocks: OrderedDict = OrderedDict()
        self.prefix = prefix
        self.bl_iocs = ioc_list.split(" ")
        if self.bl_iocs[-1] == "":
            self.bl_iocs = self.bl_iocs[:-1]
        self.stats = dict()
        # TODO: the following stuff is all Linux-specific....
        sys_call_bytes = open(f"/proc/{os.getpid()}/cmdline", "rb").read().split(b"\0")
        sys_call = [el.decode("utf-8") for el in sys_call_bytes]
        self.stats["pymalcolm_path"] = os.path.abspath(sys_call[1])
        self.stats["yaml_path"] = os.path.abspath(sys_call[2])

        self.stats["yaml_ver"] = self.parse_yaml_version(
            self.stats["yaml_path"], "/dls_sw/work", "/dls_sw/prod"
        )

        self.stats["pymalcolm_ver"] = __version__
        hostname = os.uname()[1]
        self.stats["kernel"] = f"{os.uname()[0]} {os.uname()[2]}"
        self.stats["hostname"] = (
            hostname if len(hostname) < 39 else hostname[:35] + "..."
        )
        self.stats["pid"] = str(os.getpid())

        self.pymalcolm_path = StringMeta(
            "Path to pymalcolm executable", tags=[Widget.MULTILINETEXTUPDATE.tag()]
        ).create_attribute_model(self.stats["pymalcolm_path"])
        self.pymalcolm_ver = StringMeta(
            "Version of pymalcolm executable", tags=[Widget.TEXTUPDATE.tag()]
        ).create_attribute_model(self.stats["pymalcolm_ver"])
        self.yaml_path = StringMeta(
            "Path to yaml configuration file", tags=[Widget.MULTILINETEXTUPDATE.tag()]
        ).create_attribute_model(self.stats["yaml_path"])
        self.yaml_ver = StringMeta(
            "version of yaml configuration file", tags=[Widget.TEXTUPDATE.tag()]
        ).create_attribute_model(self.stats["yaml_ver"])
        self.hostname = StringMeta(
            "Name of host machine", tags=[Widget.TEXTUPDATE.tag()]
        ).create_attribute_model(self.stats["hostname"])
        self.kernel = StringMeta(
            "Kernel of host machine", tags=[Widget.TEXTUPDATE.tag()]
        ).create_attribute_model(self.stats["kernel"])
        self.pid = StringMeta(
            "process ID of pymalcolm instance", tags=[Widget.TEXTUPDATE.tag()]
        ).create_attribute_model(self.stats["pid"])

        self.field_registry.add_attribute_model("pymalcolmPath", self.pymalcolm_path)
        self.field_registry.add_attribute_model("pymalcolmVer", self.pymalcolm_ver)
        self.field_registry.add_attribute_model("yamlPath", self.yaml_path)
        self.field_registry.add_attribute_model("yamlVer", self.yaml_ver)
        self.field_registry.add_attribute_model("hostname", self.hostname)
        self.field_registry.add_attribute_model("kernel", self.kernel)
        self.field_registry.add_attribute_model("pid", self.pid)

        if self.stats["yaml_ver"] in ["work", "unknown"]:
            message = "Non-prod YAML config"
            alarm = Alarm(message=message, severity=AlarmSeverity.MINOR_ALARM)
            self.update_health("", builtin.infos.HealthInfo(alarm))

        self.register_hooked(ProcessStartHook, self.init)

        self.register_hooked(ProcessStopHook, self.stop_ioc)

    def init(self):
        if self.ioc is None:
            self.ioc = start_ioc(self.stats, self.prefix)
        self.get_ioc_list()
        super().init()

    def set_default_layout(self):
        name = []
        mri = []
        x = []
        y = []
        visible = []
        for part_name in self.parts.keys():
            if isinstance(self.parts[part_name], builtin.parts.ChildPart):
                visible += [True]
                x += [0]
                y += [0]
                name += [part_name]
                mri += [self.parts[part_name].mri]

        self.set_layout(builtin.util.LayoutTable(name, mri, x, y, visible))

    def stop_ioc(self):
        if self.ioc is not None:
            self.ioc.terminate()
            self.ioc = None

    def get_ioc_list(self):
        ioc_controllers = []
        for ioc in self.bl_iocs:
            ioc_controller = make_ioc_status(ioc)
            ioc_controllers += [ioc_controller]
        self.process.add_controllers(ioc_controllers)
        for ioc in self.bl_iocs:
            self.add_part(builtin.parts.ChildPart(name=ioc, mri=ioc + ":STATUS"))

    def parse_yaml_version(self, file_path, work_area, prod_area):
        ver = "unknown"
        if file_path.startswith(work_area):
            ver = "work"
        elif file_path.startswith(prod_area):
            ver = self._run_git_cmd(
                "describe", "--tags", "--exact-match", cwd=os.path.split(file_path)[0]
            )
            if ver is None:
                return "Prod (unknown version)"
            ver = ver.strip(b"\n").decode("utf-8")
        return ver

    def _run_git_cmd(self, *args, **kwargs):
        # Run git command, don't care if it fails, logging the output
        cwd = kwargs.get("cwd", self.config_dir)
        try:
            output = subprocess.check_output(
                ("git",) + args, cwd=cwd
            )
        except subprocess.CalledProcessError as e:
            self.log.warning("Git command failed: %s\n%s", e, e.output)
            return None
        else:
            self.log.debug("Git command completed: %s", output)
            return output


def make_ioc_status(ioc):
    controller = builtin.controllers.StatefulController(ioc + ":STATUS")

    controller.add_part(
        ca.parts.CAStringPart(
            name="epicsVersion",
            description="EPICS version",
            rbv=(ioc + ":EPICS_VERS"),
            throw=False,
        )
    )

    controller.add_part(
        IocIconPart(ioc, (os.path.split(__file__)[0] + "/../icons/epics-logo.svg"))
    )
    controller.add_part(DirParsePart(ioc, ioc))

    controller.add_part(
        ca.parts.CAActionPart(
            "restartIoc",
            description="restart IOC via procServ",
            pv=(ioc + ":RESTART"),
            throw=False,
        )
    )

    return controller
