"""whmapi1 functions"""
from pathlib import Path
from typing import Literal, TypedDict
from typing import Union
from ._base import CpAPIBase
from ._errors import CpAPIErrorMsg


class ResellerIPsDict(TypedDict):
    """Return format of whmapi1.getresellerips"""

    all: Literal[0, 1]  # if all of the reseller's IP addresses are available
    ip: list[str]  # list of IPs


class Whmapi1(CpAPIBase):
    """whmapi1 functions"""

    __module__ = 'cpapis'

    def __init__(self):
        if self._can_exec('/usr/sbin/whmapi1'):
            super().__init__('/usr/sbin/whmapi1')
        else:
            super().__init__('/usr/local/cpanel/bin/whmapi1')

    def __call__(
        self,
        function: str,
        args: Union[dict, None] = None,
        timeout: Union[float, None] = None,
        *,
        check: bool = False,
    ) -> dict:
        """Query whmapi1

        Args:
            function: whmapi1 function to use
            args: key-vals to send to whmapi1
            timeout: timeout for the whmapi1 command in secs
            check: check .metadata.result from the result and raise
                an CpAPIErrorMsg containing .metadata.reason on failure.
                Defaults False.

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: check=True was set and the API reported an error
        """
        ret = self._exec(
            module_args=[function],
            user=None,
            args=args,
            timeout=timeout,
        )
        if check:
            self.check(ret)
        return ret

    @classmethod
    def check(cls, data: dict) -> None:
        """Parse output common to most whmapi1 functions and raise an
        exception if the function failed

        Args:
            data: result data from a whmapi1 call

        Raises:
            CpAPIErrorMsg: the API reported an error
        """
        try:
            if data['metadata']['result'] != 1:
                raise CpAPIErrorMsg(msg=data['metadata']['reason'], data=data)
        except (TypeError, KeyError) as exc:
            raise CpAPIErrorMsg(
                msg=f"{type(exc).__name__}: {exc}", data=data
            ) from exc

    def set_owner(
        self, user: str, owner: str, timeout: Union[float, None] = None
    ) -> None:
        """Change a user's owner

        Args:
            user: cPanel user to modify
            owner: username of a reseller to assign ``user`` to
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error
        """
        self(
            'modifyacct',
            {'user': user, 'owner': owner},
            check=True,
            timeout=timeout,
        )

    def setsiteip(
        self, user: str, addr: str, timeout: Union[float, None] = None
    ) -> None:
        """Change a user's IP address

        Args:
            user: cPanel user to modify
            addr: IP address
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error
        """
        # the api can handle "domain" en lieu of "user" but we only support
        # specifying by user here; it seems safer that way
        self(
            'setsiteip', {'user': user, 'ip': addr}, check=True, timeout=timeout
        )

    def removeacct(
        self, user: str, keepdns: bool, timeout: Union[float, None] = None
    ) -> None:
        """Remove/Delete an account

        Args:
            user: cPanel user to delete
            keepdns: whether to retain the account's DNS entries
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error
        """
        self(
            'removeacct',
            {'username': user, 'keepdns': int(keepdns)},
            check=True,
            timeout=timeout,
        )

    def setupreseller(
        self,
        user: str,
        makeowner: bool = False,
        timeout: Union[float, None] = None,
    ) -> None:
        """Grants reseller status to an account

        Args:
            user: cPanel username
            makeowner: whether to make the user own their own account
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error
        """
        ret = self(
            'setupreseller',
            {'user': user, 'makeowner': int(makeowner)},
            check=False,
            timeout=timeout,
        )
        try:
            if ret['metadata']['result'] == 1:
                return
            msg = ret['metadata']['reason']
            if 'tried to make a reseller out of a reseller' in msg:
                return
            raise CpAPIErrorMsg(msg=ret['metadata']['reason'], data=ret)
        except (TypeError, KeyError) as exc:
            raise CpAPIErrorMsg(
                msg=f"{type(exc).__name__}: {exc}", data=ret
            ) from exc

    def set_acllist(
        self,
        user: str,
        acl: Union[str, None],
        timeout: Union[float, None] = None,
    ) -> list[str]:
        """Assign a predefined ACL to a reseller

        Args:
            user: cPanel reseller to modify
            acl: name of the ACL list in /var/cpanel/acllists or None to strip
                all permissions
            timeout: timeout for the whmapi1 command in secs

        Returns:
            a list of permissions applied to the account

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: ACL did not exist or the API reported an error
        """
        if acl:
            if not Path('/var/cpanel/acllists', acl).is_file():
                raise CpAPIErrorMsg(msg=f"ACL {acl!r} does not exist", data={})
            args = {'user': user, 'acllist': acl}
        else:
            args = {'user': user}
        ret = self(
            'setacls',
            args,
            check=True,
            timeout=timeout,
        )
        return ret['data']['acl']

    def changepackage(
        self, user: str, pkg: str, timeout: Union[float, None] = None
    ) -> None:
        """Changes a cPanel account's hosting plan / package

        Args:
            user: cPanel user to modify
            pkg: the hosting plan's name (case sensitive!)
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error
        """
        self(
            'changepackage',
            {'user': user, 'pkg': pkg},
            check=True,
            timeout=timeout,
        )

    def list_users(self, timeout: Union[float, None] = None) -> list[str]:
        """lists the cPanel user accounts and the root user on the server

        Args:
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error

        Returns:
            list[str]: list of users + root
        """
        ret = self('list_users', check=True, timeout=timeout)
        return ret['data']['users']

    def listresellers(self, timeout: Union[float, None] = None) -> list[str]:
        """lists the reseller accounts on the server

        Args:
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error

        Returns:
            list[str]: list of one or more valid usernames
        """
        ret = self("listresellers", check=True, timeout=timeout)
        return ret["data"]["reseller"]

    def getresellerips(
        self, user: str, timeout: Union[float, None] = None
    ) -> ResellerIPsDict:
        """lists a reseller's available IP addresses

        Args:
            user (str): cPanel username
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error

        Returns:
            ResellerIPsDict: dict containing "ip" (list[str]) and "all" (0 or 1)
        """
        ret = self(
            'getresellerips', {'user': user}, check=True, timeout=timeout
        )
        return ret['data']

    def setresellerips(
        self,
        user: str,
        ips: Union[list[str], set[str]],
        delegate: bool,
        timeout: Union[float, None] = None,
    ) -> None:
        """adds IP addresses to a reseller's account

        Args:
            user (str): cPanel username
            ips (list[str] | set[str]): IPs to assign
            delegate (bool): restrict the reseller's account to its dedicated IP
                addresses. If False, allow the user to dedicate any available IP
            timeout: timeout for the whmapi1 command in secs

        Raises:
            CpAPIExecFail: command failed to execute or returned invalid json
            CpAPIErrorMsg: the API reported an error
        """
        if not isinstance(ips, (list, set)):
            raise TypeError(f"{ips=}")
        self(
            'setresellerips',
            {'user': user, 'ips': ','.join(ips), 'delegate': int(delegate)},
            check=True,
            timeout=timeout,
        )
