import os
import time
import shlex
import errno
import logging
import subprocess

from wurker.models import WurkerBeeState

from .base import BaseBee


class Bee(BaseBee):
    """Base class for external, arbitrary Wurker Bees."""

    def __init__(self, session, dbee, dcron=None, args=None, logger=None):
        """Get ready to run an arbitrary command."""

        BaseBee.__init__(self, session, dbee, dcron=dcron, args=args, logger=logger)
        self._init_command()

    def _init_command(self):
        """Validate we can execute the command."""

        self.command = shlex.split(self.command)

        # In case of 'language command' form, we still need an absolute path
        cmd_path = self.command[0] if len(self.command) == 1 else self.command[1]
        if not os.path.exists(cmd_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), cmd_path)

        if self.args and isinstance(self.args, str):
            self.args = shlex.split(self.args)
        elif self.args is None:
            self.args = []

    def run(self):
        """That's called self-commenting code right there."""

        self.start_dt = self.Now

        # Still a process running?
        if self.proc and self.proc.poll() is None:
            return False

        # Capture stderr
        stdout = subprocess.PIPE
        stderr = subprocess.PIPE

        args = self.command + (self.args or [])
        self.proc = subprocess.Popen(args, stdout=stdout, stderr=stderr, text=True)
        self.pid = self.proc.pid
        self.runcount = 1

        # Update to PROCESSING
        self.set_report(status=WurkerBeeState.PROCESSING)

        if not self.quiet_restart:
            self.log(f"[{self.pid}] Running Wurker Bee '{self.slug}'...")

        return True

    def poll(self):
        """Convenience wrapper around subprocess poll()."""

        if not self.proc:
            self.reset_proc()
            return None

        return self.proc.poll()

    def wait(self):
        """Convenience wrapper around subprocess wait()."""

        if not self.proc:
            self.reset_proc()
            return None

        return self.proc.wait(timeout=self.proc_timeout)

    def kill(self, error_status=WurkerBeeState.ERROR):
        """Kill everyone. Returns boolean."""

        # Already dead? With no process?
        if not self.proc:
            self.set_report(status=WurkerBeeState.COMPLETED)
            self.reset_proc()
            return True

        # We still have a process! So check to see if it's done...
        try:
            output, errors = self.proc.communicate(timeout=self.proc_timeout)
        except subprocess.TimeoutExpired as sp_te:
            self.log(sp_te, exception=True, level=logging.ERROR)
            return False

        self.output = output.strip() if output is not None else None
        self.errors = errors.strip() if errors is not None else None

        # Already done? Then return True
        if self.proc.poll() is not None:
            if self.proc.returncode or self.errors:
                self.set_report(status=error_status, status_info={
                    "exit_code": int(self.proc.returncode), "errors": [self.errors]
                })
            else:
                self.set_report(status=WurkerBeeState.COMPLETED)

            self.reset_proc()
            return True

        # Not dead yet! So try to kill it...
        self.log(f"[{self.pid}] Sending SIGTERM to Wurker Bee '{self.slug}'...",
                 level=logging.ERROR)

        self.proc.terminate()
        time.sleep(1.0)
        is_dead = True

        # SIGTERM didn't work? Try SIGKILL
        if self.proc.poll() is None:
            self.log(f"[{self.pid}] SIGTERM failed. Sending SIGKILL to Wurker Bee '{self.slug}'...",
                     level=logging.ERROR)

            self.proc.kill()
            time.sleep(1.0)

            # Did it finally stay dead? If not... it failed to die
            if self.proc.poll() is None:
                self.log(f"[{self.pid}] SIGTERM and SIGKILL failed. Wurker Bee '{self.slug}' is a Zombie!",
                         level=logging.ERROR)

                is_dead = False

        if is_dead:
            self.set_report(status=error_status, status_info={"message": f"Bee killed: '{self.slug}'"})
        else:
            self.set_report(status=error_status, status_info={"message": f"Bee is zombie: '{self.slug}'"})

        # Fingers crossed!
        self.reset_proc()
        return is_dead
