import configparser
import subprocess
from datetime import UTC, datetime
from typing import Self

from . import (
    Activity,
    Check,
    ConfigurationError,
    SevereCheckError,
    TemporaryCheckError,
    Wakeup,
)
from ..config import ParameterType, config_param


def raise_severe_if_command_not_found(error: subprocess.CalledProcessError) -> None:
    if error.returncode == 127:
        # see http://tldp.org/LDP/abs/html/exitcodes.html
        raise SevereCheckError(f"Command '{' '.join(error.cmd)}' does not exist")


@config_param(
    "command",
    ParameterType.STRING,
    "The command to execute including all arguments",
    required=True,
)
class CommandMixin(Check):
    """Mixin for configuring checks based on external commands."""

    @classmethod
    def create(cls: type[Self], name: str, config: configparser.SectionProxy) -> Self:
        try:
            return cls(name, config["command"].strip())  # type: ignore
        except KeyError as error:
            raise ConfigurationError("Missing command specification") from error

    def __init__(self, command: str) -> None:
        self._command = command


class CommandActivity(CommandMixin, Activity):
    """Execute an external command to determine activity.

    Executes an arbitrary command.
    In case this command returns 0, the system is assumed to be active.

    The command is executed as is using shell execution.
    Beware of malicious commands in obtained configuration files.

    .. seealso::

       * :ref:`external-command-activity-scripts` for a collection of user-provided scripts for some common use cases.
    """

    def __init__(self, name: str, command: str) -> None:
        CommandMixin.__init__(self, command)
        Activity.__init__(self, name)

    def check(self) -> str | None:
        try:
            subprocess.check_call(self._command, shell=True)
            return f"Command {self._command} succeeded"
        except subprocess.CalledProcessError as error:
            raise_severe_if_command_not_found(error)
            return None


class CommandWakeup(CommandMixin, Wakeup):
    """Determine wake up times from an external command.

    The called command must return a timestamp in UTC or nothing in case no
    wake up is planned.

    The command always has to succeed.
    If something is printed on stdout by the command, this has to be the next wake up time in UTC seconds.

    The command is executed as is using shell execution.
    Beware of malicious commands in obtained configuration files.
    """

    def __init__(self, name: str, command: str) -> None:
        CommandMixin.__init__(self, command)
        Wakeup.__init__(self, name)

    def check(self, timestamp: datetime) -> datetime | None:  # noqa: ARG002
        try:
            output = subprocess.check_output(
                self._command,
                shell=True,
            ).splitlines()[0]
            self.logger.debug(
                "Command %s succeeded with output %s", self._command, output
            )
            if output.strip():
                return datetime.fromtimestamp(float(output.strip()), UTC)
            else:
                return None

        except subprocess.CalledProcessError as error:
            raise_severe_if_command_not_found(error)
            raise TemporaryCheckError(
                "Unable to call the configured command"
            ) from error
        except ValueError as error:
            raise TemporaryCheckError(
                "Return value cannot be interpreted as a timestamp"
            ) from error
