import json
import logging
import os
import re
from pathlib import Path
from typing import Union

import psutil

from clarabot_nano.utility.subprocess.interrupt import interrupt_child

NANO_INFO_LOGGER = logging.getLogger("nano/info")


class SubprocessTracker(object):
    """
    Manage (record/cleanup) processes by pid-files.
    This utility creates a robust solution that has custom metadata in the pid-file to facilitate custom cleanup.
    """

    PID_FILE_RE = re.compile(r"^(.+?)-(\d+)\.pid$")

    @classmethod
    def record(cls, pid_dir: Path, p: psutil.Process, metadata=None, pid_file_prefix: str = "process"):
        pid_dir.mkdir(parents=True, exist_ok=True)
        pid_file = pid_dir / f"{pid_file_prefix}-{p.pid}.pid"
        with open(pid_file, "w", encoding="utf-8") as f:
            json.dump({"pid": p.pid, "create_time": p.create_time(), "metadata": metadata}, f)
        NANO_INFO_LOGGER.info("Spawned %s with pid %s", pid_file_prefix, p.pid)

    @classmethod
    def cleanup(
        cls, pid_dir: Path, kill_timeout=5.0, pid_file_prefix: Union[None, str] = "process", gui_type: bool = False,
    ):
        """
        Generator that yields process data objects to execute graceful cleanup by. If the process is still running
        when the yield returns then the process is terminated and eventually killed.
        """
        pid_dir.mkdir(parents=True, exist_ok=True)
        for item in list(pid_dir.iterdir()):
            if not item.is_file():  # TODO: on Windows PermissionError may happen here (antivirus locking the file?)
                continue
            m = cls.PID_FILE_RE.match(item.name)
            if not m:
                continue
            if pid_file_prefix and m.group(1) != pid_file_prefix:
                continue
            try:
                with open(item, "r", encoding="utf-8") as f:
                    data = json.load(f)
                p = psutil.Process(data["pid"])
                killed = False
                try:
                    if p.create_time() == data["create_time"]:
                        data["p"] = p
                        yield data
                        if p.is_running():
                            interrupt_child(p, gui_type=gui_type)
                            try:
                                p.wait(kill_timeout)
                            except psutil.TimeoutExpired:
                                killed = True
                                p.kill()
                finally:
                    NANO_INFO_LOGGER.info("%s %s by pid %s", "Killed" if killed else "Stopped", pid_file_prefix, p.pid)
            except Exception:
                pass  # if anything goes wrong, the cleanup cannot/should not work or already done
            try:
                os.unlink(item)
            except FileNotFoundError:
                pass
