import asyncio
import functools
import logging
import signal
from enum import Enum
from typing import List, Dict

import yaml

from defence360agent.utils import CheckRunError, atomic_rewrite, check_run, run
from defence360agent.utils.kwconfig import KWConfig


_PAM_EXECUTABLE = "imunify360-pam"
logger = logging.getLogger(__name__)


class PamService:
    DOVECOT_NATIVE = "dovecot-native"
    DOVECOT_PAM = "dovecot-pam"
    FTP = "ftp"
    SSHD = "sshd"


class PamServiceStatusValue:
    enabled = "enabled"
    disabled = "disabled"


class DovecotStatus(str, Enum):
    DISABLED = "disabled"
    PAM = "pam"
    NATIVE = "native"

    def __str__(self):
        return self.value


_DEFAULT_STATUS = {
    PamService.DOVECOT_NATIVE: PamServiceStatusValue.disabled,
    PamService.DOVECOT_PAM: PamServiceStatusValue.disabled,
    PamService.FTP: PamServiceStatusValue.disabled,
    PamService.SSHD: PamServiceStatusValue.disabled,
}


class PAMError(Exception):
    pass


class _Config(KWConfig):
    SEARCH_PATTERN = r"^\s*{}\s*=\s*(.*?)\s*$"
    WRITE_PATTERN = "{}={}"
    DEFAULT_FILENAME = "/etc/pam_imunify/i360.ini"

    _IP_WHITELIST_OPTION = "whitelisted_ips_path"
    _IP_WHITELIST_DEFAULT = "/var/i360_pam_imunify/wl/ips.txt"

    def get_default(self, default: str) -> str:
        v = self.get()
        return v if v is not None else default

    @classmethod
    def ip_whitelist_path(cls) -> str:
        try:
            return (
                cls(cls._IP_WHITELIST_OPTION).get_default(
                    cls._IP_WHITELIST_DEFAULT
                )
                # whitelisted_ips_path is comma separated list
                # where user ip list path goes the last
                .split(",")[-1]
            )
        except FileNotFoundError:
            return cls._IP_WHITELIST_DEFAULT


async def _export_list(path: str, values: List[str]) -> None:
    loop = asyncio.get_event_loop()
    content = "".join([v + "\n" for v in values])
    writer = functools.partial(atomic_rewrite, path, content, backup=False)
    await loop.run_in_executor(None, writer)


async def export_ip_whitelist(networks: List[str]) -> None:
    """Save a list of `networks` into IP address whitelist."""
    await _export_list(_Config.ip_whitelist_path(), networks)


async def get_status() -> Dict:
    cmd = [_PAM_EXECUTABLE, "status", "--yaml"]
    try:
        returncode, output, err = await run(cmd)
    except FileNotFoundError:
        return _DEFAULT_STATUS
    except OSError as exc:
        raise PAMError("PAM status failed") from exc
    else:
        if returncode == -signal.SIGTERM:
            # Don't send SIGTERM to Sentry
            # (presumably on shutdown on systemctl stop imunify360)
            logger.warning(
                "SIGTERM while getting pam status, rc: %s, out: %s, err: %s",
                returncode,
                output,
                err,
            )
            return _DEFAULT_STATUS
        elif returncode != 0 or output.strip() == b"":
            raise PAMError(
                "PAM status failed: run(%r) = %r"
                % (cmd, (returncode, output, err))
            )
    log_response_warnings(output)
    try:
        return yaml.safe_load(output.decode())["status"]
    except (UnicodeDecodeError, yaml.YAMLError, TypeError, KeyError) as e:
        raise PAMError(f"Can't get pam status from {output!r}") from e


async def enable(module, dry_run=False) -> None:
    """Enable PAM module. Raises PAMError."""
    pam_command = {
        PamService.SSHD: ["enable"],
        PamService.DOVECOT_PAM: ["set-dovecot", "pam"],
        PamService.DOVECOT_NATIVE: ["set-dovecot", "native"],
        PamService.FTP: ["enable-ftp"],
    }[module]
    try:
        output = await check_run(
            [
                _PAM_EXECUTABLE,
                *pam_command,
                *(["--dry-run"] if dry_run else []),
                "--yaml",
            ]
        )
        log_response_warnings(output)
    except FileNotFoundError:
        logger.warning(
            "%s not found, skipping enable for %s", _PAM_EXECUTABLE, module
        )
    except CheckRunError as exc:
        raise PAMError("failed to enable PAM for %s" % module) from exc


async def disable(module) -> None:
    """Disable PAM module. Raises PAMError."""
    pam_command = {
        PamService.SSHD: ["disable"],
        PamService.DOVECOT_PAM: ["set-dovecot", "disabled"],
        PamService.DOVECOT_NATIVE: ["set-dovecot", "disabled"],
        PamService.FTP: ["disable-ftp"],
    }[module]
    try:
        output = await check_run([_PAM_EXECUTABLE, *pam_command, "--yaml"])
        log_response_warnings(output)
    except FileNotFoundError:
        logger.warning(
            "%s not found, skipping disable for %s", _PAM_EXECUTABLE, module
        )
    except CheckRunError as exc:
        raise PAMError("failed to disable PAM for %s" % module) from exc


def log_response_warnings(output):
    try:
        response = yaml.safe_load(output.decode())
    except (yaml.YAMLError, UnicodeDecodeError):
        logger.warning("Not yaml response for %s: %s", _PAM_EXECUTABLE, output)
        response = None
    warnings = response and response.get("warnings")
    if warnings:
        logger.warning("imunify360-pam warnings: %s", warnings)
