"""
Manage LXD containers.

.. versionadded:: 2019.2.0

.. note:

    - :ref:`pylxd` version 2 is required to let this work,
      currently only available via pip.

        To install on Ubuntu:

        $ apt-get install libssl-dev python-pip
        $ pip install -U pylxd

    - you need lxd installed on the minion
      for the init() and version() methods.

    - for the config_get() and config_get() methods
      you need to have lxd-client installed.

.. _pylxd: https://github.com/lxc/pylxd/blob/master/doc/source/installation.rst

:maintainer: René Jochum <rene@jochums.at>
:maturity: new
:depends: python-pylxd
:platform: Linux
"""


from salt.exceptions import CommandExecutionError, SaltInvocationError

__docformat__ = "restructuredtext en"

__virtualname__ = "lxd_container"

# Keep in sync with: https://github.com/lxc/lxd/blob/master/shared/status.go
CONTAINER_STATUS_RUNNING = 103
CONTAINER_STATUS_FROZEN = 110
CONTAINER_STATUS_STOPPED = 102


def __virtual__():
    """
    Only load if the lxd module is available in __salt__
    """
    if "lxd.version" in __salt__:
        return __virtualname__
    return (False, "lxd module could not be loaded")


def present(
    name,
    running=None,
    source=None,
    profiles=None,
    config=None,
    devices=None,
    architecture="x86_64",
    ephemeral=False,
    restart_on_change=False,
    remote_addr=None,
    cert=None,
    key=None,
    verify_cert=True,
):
    """
    Create the named container if it does not exist

    name
        The name of the container to be created

    running : None
        * If ``True``, ensure that the container is running
        * If ``False``, ensure that the container is stopped
        * If ``None``, do nothing with regards to the running state of the
          container

    source : None
        Can be either a string containing an image alias:

        .. code-block:: none

             "xenial/amd64"

        or an dict with type "image" with alias:

        .. code-block:: python

            {"type": "image",
             "alias": "xenial/amd64"}

        or image with "fingerprint":

        .. code-block:: python

            {"type": "image",
             "fingerprint": "SHA-256"}

        or image with "properties":

        .. code-block:: python

            {"type": "image",
             "properties": {
                "os": "ubuntu",
                "release": "14.04",
                "architecture": "x86_64"
             }}

        or none:

        .. code-block:: python

            {"type": "none"}

        or copy:

        .. code-block:: python

            {"type": "copy",
             "source": "my-old-container"}

    profiles : ['default']
        List of profiles to apply on this container

    config :
        A config dict or None (None = unset).

        Can also be a list:

        .. code-block:: python

            [{'key': 'boot.autostart', 'value': 1},
             {'key': 'security.privileged', 'value': '1'}]

    devices :
        A device dict or None (None = unset).

    architecture : 'x86_64'
        Can be one of the following:

        * unknown
        * i686
        * x86_64
        * armv7l
        * aarch64
        * ppc
        * ppc64
        * ppc64le
        * s390x

    ephemeral : False
        Destroy this container after stop?

    restart_on_change : False
        Restart the container when we detect changes on the config or
        its devices?

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normally uses self-signed certificates.
    """
    if profiles is None:
        profiles = ["default"]

    if source is None:
        source = {}

    ret = {
        "name": name,
        "running": running,
        "profiles": profiles,
        "source": source,
        "config": config,
        "devices": devices,
        "architecture": architecture,
        "ephemeral": ephemeral,
        "restart_on_change": restart_on_change,
        "remote_addr": remote_addr,
        "cert": cert,
        "key": key,
        "verify_cert": verify_cert,
        "changes": {},
    }

    container = None
    try:
        container = __salt__["lxd.container_get"](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, str(e))
    except SaltInvocationError as e:
        # Profile not found
        pass

    if container is None:
        if __opts__["test"]:
            # Test is on, just return that we would create the container
            msg = 'Would create the container "{}"'.format(name)
            ret["changes"] = {"created": msg}
            if running is True:
                msg = msg + " and start it."
                ret["changes"]["started"] = 'Would start the container "{}"'.format(
                    name
                )

            ret["changes"] = {"created": msg}
            return _unchanged(ret, msg)

        # create the container
        try:
            __salt__["lxd.container_create"](
                name,
                source,
                profiles,
                config,
                devices,
                architecture,
                ephemeral,
                True,  # Wait
                remote_addr,
                cert,
                key,
                verify_cert,
            )
        except CommandExecutionError as e:
            return _error(ret, str(e))

        msg = 'Created the container "{}"'.format(name)
        ret["changes"] = {"created": msg}

        if running is True:
            try:
                __salt__["lxd.container_start"](
                    name, remote_addr, cert, key, verify_cert
                )
            except CommandExecutionError as e:
                return _error(ret, str(e))

            msg = msg + " and started it."
            ret["changes"] = {"started": 'Started the container "{}"'.format(name)}

        return _success(ret, msg)

    # Container exists, lets check for differences
    new_profiles = set(map(str, profiles))
    old_profiles = set(map(str, container.profiles))

    container_changed = False

    profile_changes = []
    # Removed profiles
    for k in old_profiles.difference(new_profiles):
        if not __opts__["test"]:
            profile_changes.append('Removed profile "{}"'.format(k))
            old_profiles.discard(k)
        else:
            profile_changes.append('Would remove profile "{}"'.format(k))

    # Added profiles
    for k in new_profiles.difference(old_profiles):
        if not __opts__["test"]:
            profile_changes.append('Added profile "{}"'.format(k))
            old_profiles.add(k)
        else:
            profile_changes.append('Would add profile "{}"'.format(k))

    if profile_changes:
        container_changed = True
        ret["changes"]["profiles"] = profile_changes
        container.profiles = list(old_profiles)

    # Config and devices changes
    config, devices = __salt__["lxd.normalize_input_values"](config, devices)
    changes = __salt__["lxd.sync_config_devices"](
        container, config, devices, __opts__["test"]
    )
    if changes:
        container_changed = True
        ret["changes"].update(changes)

    is_running = container.status_code == CONTAINER_STATUS_RUNNING

    if not __opts__["test"]:
        try:
            __salt__["lxd.pylxd_save_object"](container)
        except CommandExecutionError as e:
            return _error(ret, str(e))

    if running != is_running:
        if running is True:
            if __opts__["test"]:
                changes["running"] = "Would start the container"
                return _unchanged(
                    ret,
                    'Container "{}" would get changed and started.'.format(name),
                )
            else:
                container.start(wait=True)
                changes["running"] = "Started the container"

        elif running is False:
            if __opts__["test"]:
                changes["stopped"] = "Would stopped the container"
                return _unchanged(
                    ret,
                    'Container "{}" would get changed and stopped.'.format(name),
                )
            else:
                container.stop(wait=True)
                changes["stopped"] = "Stopped the container"

    if (
        (running is True or running is None)
        and is_running
        and restart_on_change
        and container_changed
    ):

        if __opts__["test"]:
            changes["restarted"] = "Would restart the container"
            return _unchanged(ret, 'Would restart the container "{}"'.format(name))
        else:
            container.restart(wait=True)
            changes["restarted"] = 'Container "{}" has been restarted'.format(name)
            return _success(ret, 'Container "{}" has been restarted'.format(name))

    if not container_changed:
        return _success(ret, "No changes")

    if __opts__["test"]:
        return _unchanged(ret, 'Container "{}" would get changed.'.format(name))

    return _success(ret, "{} changes".format(len(ret["changes"].keys())))


def absent(name, stop=False, remote_addr=None, cert=None, key=None, verify_cert=True):
    """
    Ensure a LXD container is not present, destroying it if present

    name :
        The name of the container to destroy

    stop :
        stop before destroying
        default: false

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normally uses self-signed certificates.
    """
    ret = {
        "name": name,
        "stop": stop,
        "remote_addr": remote_addr,
        "cert": cert,
        "key": key,
        "verify_cert": verify_cert,
        "changes": {},
    }

    try:
        container = __salt__["lxd.container_get"](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, str(e))
    except SaltInvocationError as e:
        # Container not found
        return _success(ret, 'Container "{}" not found.'.format(name))

    if __opts__["test"]:
        ret["changes"] = {"removed": 'Container "{}" would get deleted.'.format(name)}
        return _unchanged(ret, ret["changes"]["removed"])

    if stop and container.status_code == CONTAINER_STATUS_RUNNING:
        container.stop(wait=True)

    container.delete(wait=True)

    ret["changes"]["deleted"] = 'Container "{}" has been deleted.'.format(name)
    return _success(ret, ret["changes"]["deleted"])


def running(
    name, restart=False, remote_addr=None, cert=None, key=None, verify_cert=True
):
    """
    Ensure a LXD container is running and restart it if restart is True

    name :
        The name of the container to start/restart.

    restart :
        restart the container if it is already started.

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normally uses self-signed certificates.
    """
    ret = {
        "name": name,
        "restart": restart,
        "remote_addr": remote_addr,
        "cert": cert,
        "key": key,
        "verify_cert": verify_cert,
        "changes": {},
    }

    try:
        container = __salt__["lxd.container_get"](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, str(e))
    except SaltInvocationError as e:
        # Container not found
        return _error(ret, 'Container "{}" not found'.format(name))

    is_running = container.status_code == CONTAINER_STATUS_RUNNING

    if is_running:
        if not restart:
            return _success(ret, 'The container "{}" is already running'.format(name))
        else:
            if __opts__["test"]:
                ret["changes"]["restarted"] = 'Would restart the container "{}"'.format(
                    name
                )
                return _unchanged(ret, ret["changes"]["restarted"])
            else:
                container.restart(wait=True)
                ret["changes"]["restarted"] = 'Restarted the container "{}"'.format(
                    name
                )
                return _success(ret, ret["changes"]["restarted"])

    if __opts__["test"]:
        ret["changes"]["started"] = 'Would start the container "{}"'.format(name)
        return _unchanged(ret, ret["changes"]["started"])

    container.start(wait=True)
    ret["changes"]["started"] = 'Started the container "{}"'.format(name)
    return _success(ret, ret["changes"]["started"])


def frozen(name, start=True, remote_addr=None, cert=None, key=None, verify_cert=True):
    """
    Ensure a LXD container is frozen, start and freeze it if start is true

    name :
        The name of the container to freeze

    start :
        start and freeze it

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normally uses self-signed certificates.
    """
    ret = {
        "name": name,
        "start": start,
        "remote_addr": remote_addr,
        "cert": cert,
        "key": key,
        "verify_cert": verify_cert,
        "changes": {},
    }

    try:
        container = __salt__["lxd.container_get"](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, str(e))
    except SaltInvocationError as e:
        # Container not found
        return _error(ret, 'Container "{}" not found'.format(name))

    if container.status_code == CONTAINER_STATUS_FROZEN:
        return _success(ret, 'Container "{}" is alredy frozen'.format(name))

    is_running = container.status_code == CONTAINER_STATUS_RUNNING

    if not is_running and not start:
        return _error(
            ret,
            'Container "{}" is not running and start is False, cannot freeze it'.format(
                name
            ),
        )

    elif not is_running and start:
        if __opts__["test"]:
            ret["changes"][
                "started"
            ] = 'Would start the container "{}" and freeze it after'.format(name)
            return _unchanged(ret, ret["changes"]["started"])
        else:
            container.start(wait=True)
            ret["changes"]["started"] = 'Start the container "{}"'.format(name)

    if __opts__["test"]:
        ret["changes"]["frozen"] = 'Would freeze the container "{}"'.format(name)
        return _unchanged(ret, ret["changes"]["frozen"])

    container.freeze(wait=True)
    ret["changes"]["frozen"] = 'Froze the container "{}"'.format(name)

    return _success(ret, ret["changes"]["frozen"])


def stopped(name, kill=False, remote_addr=None, cert=None, key=None, verify_cert=True):
    """
    Ensure a LXD container is stopped, kill it if kill is true else stop it

    name :
        The name of the container to stop

    kill :
        kill if true

    remote_addr :
        An URL to a remote Server, you also have to give cert and key if you
        provide remote_addr!

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normally uses self-signed certificates.
    """
    ret = {
        "name": name,
        "kill": kill,
        "remote_addr": remote_addr,
        "cert": cert,
        "key": key,
        "verify_cert": verify_cert,
        "changes": {},
    }

    try:
        container = __salt__["lxd.container_get"](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, str(e))
    except SaltInvocationError as e:
        # Container not found
        return _error(ret, 'Container "{}" not found'.format(name))

    if container.status_code == CONTAINER_STATUS_STOPPED:
        return _success(ret, 'Container "{}" is already stopped'.format(name))

    if __opts__["test"]:
        ret["changes"]["stopped"] = 'Would stop the container "{}"'.format(name)
        return _unchanged(ret, ret["changes"]["stopped"])

    container.stop(force=kill, wait=True)
    ret["changes"]["stopped"] = 'Stopped the container "{}"'.format(name)
    return _success(ret, ret["changes"]["stopped"])


def migrated(
    name,
    remote_addr,
    cert,
    key,
    verify_cert,
    src_remote_addr,
    stop_and_start=False,
    src_cert=None,
    src_key=None,
    src_verify_cert=None,
):
    """Ensure a container is migrated to another host

    If the container is running, it either must be shut down
    first (use stop_and_start=True) or criu must be installed
    on the source and destination machines.

    For this operation both certs need to be authenticated,
    use :mod:`lxd.authenticate <salt.states.lxd.authenticate`
    to authenticate your cert(s).

    name :
        The container to migrate

    remote_addr :
        An URL to the destination remote Server

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    cert :
        PEM Formatted SSL Zertifikate.

        Examples:
            ~/.config/lxc/client.crt

    key :
        PEM Formatted SSL Key.

        Examples:
            ~/.config/lxc/client.key

    verify_cert : True
        Wherever to verify the cert, this is by default True
        but in the most cases you want to set it off as LXD
        normally uses self-signed certificates.

    src_remote_addr :
        An URL to the source remote Server

        Examples:
            https://myserver.lan:8443
            /var/lib/mysocket.sock

    stop_and_start:
        Stop before migrating and start after

    src_cert :
        PEM Formatted SSL Zertifikate, if None we copy "cert"

        Examples:
            ~/.config/lxc/client.crt

    src_key :
        PEM Formatted SSL Key, if None we copy "key"

        Examples:
            ~/.config/lxc/client.key

    src_verify_cert :
        Wherever to verify the cert, if None we copy "verify_cert"
    """
    ret = {
        "name": name,
        "remote_addr": remote_addr,
        "cert": cert,
        "key": key,
        "verify_cert": verify_cert,
        "src_remote_addr": src_remote_addr,
        "src_and_start": stop_and_start,
        "src_cert": src_cert,
        "src_key": src_key,
        "changes": {},
    }

    dest_container = None
    try:
        dest_container = __salt__["lxd.container_get"](
            name, remote_addr, cert, key, verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, str(e))
    except SaltInvocationError as e:
        # Destination container not found
        pass

    if dest_container is not None:
        return _success(ret, 'Container "{}" exists on the destination'.format(name))

    if src_verify_cert is None:
        src_verify_cert = verify_cert

    try:
        __salt__["lxd.container_get"](
            name, src_remote_addr, src_cert, src_key, src_verify_cert, _raw=True
        )
    except CommandExecutionError as e:
        return _error(ret, str(e))
    except SaltInvocationError as e:
        # Container not found
        return _error(ret, 'Source Container "{}" not found'.format(name))

    if __opts__["test"]:
        ret["changes"][
            "migrated"
        ] = 'Would migrate the container "{}" from "{}" to "{}"'.format(
            name, src_remote_addr, remote_addr
        )
        return _unchanged(ret, ret["changes"]["migrated"])

    try:
        __salt__["lxd.container_migrate"](
            name,
            stop_and_start,
            remote_addr,
            cert,
            key,
            verify_cert,
            src_remote_addr,
            src_cert,
            src_key,
            src_verify_cert,
        )
    except CommandExecutionError as e:
        return _error(ret, str(e))

    ret["changes"]["migrated"] = 'Migrated the container "{}" from "{}" to "{}"'.format(
        name, src_remote_addr, remote_addr
    )
    return _success(ret, ret["changes"]["migrated"])


def _success(ret, success_msg):
    ret["result"] = True
    ret["comment"] = success_msg
    if "changes" not in ret:
        ret["changes"] = {}
    return ret


def _unchanged(ret, msg):
    ret["result"] = None
    ret["comment"] = msg
    if "changes" not in ret:
        ret["changes"] = {}
    return ret


def _error(ret, err_msg):
    ret["result"] = False
    ret["comment"] = err_msg
    if "changes" not in ret:
        ret["changes"] = {}
    return ret
