"""
States for managing zpools

:maintainer:    Jorge Schrauwen <sjorge@blackdot.be>
:maturity:      new
:depends:       salt.utils.zfs, salt.modules.zpool
:platform:      smartos, illumos, solaris, freebsd, linux

.. versionadded:: 2016.3.0
.. versionchanged:: 2018.3.1
  Big refactor to remove duplicate code, better type conversions and improved
  consistency in output.

.. code-block:: yaml

    oldpool:
      zpool.absent:
        - export: true

    newpool:
      zpool.present:
        - config:
            import: false
            force: true
        - properties:
            comment: salty storage pool
        - layout:
            - mirror:
              - /dev/disk0
              - /dev/disk1
            - mirror:
              - /dev/disk2
              - /dev/disk3

    partitionpool:
      zpool.present:
        - config:
            import: false
            force: true
        - properties:
            comment: disk partition salty storage pool
            ashift: '12'
            feature@lz4_compress: enabled
        - filesystem_properties:
            compression: lz4
            atime: on
            relatime: on
        - layout:
            - /dev/disk/by-uuid/3e43ce94-77af-4f52-a91b-6cdbb0b0f41b

    simplepool:
      zpool.present:
        - config:
            import: false
            force: true
        - properties:
            comment: another salty storage pool
        - layout:
            - /dev/disk0
            - /dev/disk1

.. warning::

    The layout will never be updated, it will only be used at time of creation.
    It's a whole lot of work to figure out if a devices needs to be detached, removed,
    etc. This is best done by the sysadmin on a case per case basis.

    Filesystem properties are also not updated, this should be managed by the zfs state module.

"""

import logging
import os
from collections import OrderedDict

log = logging.getLogger(__name__)

# Define the state's virtual name
__virtualname__ = "zpool"


def __virtual__():
    """
    Provides zpool state
    """
    if not __grains__.get("zfs_support"):
        return False, "The zpool state cannot be loaded: zfs not supported"
    return __virtualname__


def _layout_to_vdev(layout, device_dir=None):
    """
    Turn the layout data into usable vdevs spedcification

    We need to support 2 ways of passing the layout:

    .. code::
        layout_new:
          - mirror:
            - disk0
            - disk1
          - mirror:
            - disk2
            - disk3

    .. code:
        layout_legacy:
          mirror-0:
            disk0
            disk1
          mirror-1:
            disk2
            disk3

    """
    vdevs = []

    # NOTE: check device_dir exists
    if device_dir and not os.path.exists(device_dir):
        device_dir = None

    # NOTE: handle list of OrderedDicts (new layout)
    if isinstance(layout, list):
        # NOTE: parse each vdev as a tiny layout and just append
        for vdev in layout:
            if isinstance(vdev, OrderedDict):
                vdevs.extend(_layout_to_vdev(vdev, device_dir))
            else:
                if device_dir and vdev[0] != "/":
                    vdev = os.path.join(device_dir, vdev)
                vdevs.append(vdev)

    # NOTE: handle nested OrderedDict (legacy layout)
    #       this is also used to parse the nested OrderedDicts
    #       from the new layout
    elif isinstance(layout, OrderedDict):
        for vdev in layout:
            # NOTE: extract the vdev type and disks in the vdev
            vdev_type = vdev.split("-")[0]
            vdev_disk = layout[vdev]

            # NOTE: skip appending the dummy type 'disk'
            if vdev_type != "disk":
                vdevs.append(vdev_type)

            # NOTE: ensure the disks are a list (legacy layout are not)
            if not isinstance(vdev_disk, list):
                vdev_disk = vdev_disk.split(" ")

            # NOTE: also append the actualy disks behind the type
            #       also prepend device_dir to disks if required
            for disk in vdev_disk:
                if device_dir and disk[0] != "/":
                    disk = os.path.join(device_dir, disk)
                vdevs.append(disk)

    # NOTE: we got invalid data for layout
    else:
        vdevs = None

    return vdevs


def present(
    name, properties=None, filesystem_properties=None, layout=None, config=None
):
    """
    ensure storage pool is present on the system

    name : string
        name of storage pool
    properties : dict
        optional set of properties to set for the storage pool
    filesystem_properties : dict
        optional set of filesystem properties to set for the storage pool (creation only)
    layout: dict
        disk layout to use if the pool does not exist (creation only)
    config : dict
        fine grain control over this state

    .. note::

        The following configuration properties can be toggled in the config parameter.
          - import (true) - try to import the pool before creating it if absent
          - import_dirs (None) - specify additional locations to scan for devices on import (comma-separated)
          - device_dir (None, SunOS=/dev/dsk, Linux=/dev) - specify device directory to prepend for none
            absolute device paths
          - force (false) - try to force the import or creation

    .. note::

        It is no longer needed to give a unique name to each top-level vdev, the old
        layout format is still supported but no longer recommended.

        .. code-block:: yaml

            - mirror:
              - /tmp/vdisk3
              - /tmp/vdisk2
            - mirror:
              - /tmp/vdisk0
              - /tmp/vdisk1

        The above yaml will always result in the following zpool create:

        .. code-block:: bash

            zpool create mypool mirror /tmp/vdisk3 /tmp/vdisk2 mirror /tmp/vdisk0 /tmp/vdisk1

    .. warning::

        The legacy format is also still supported but not recommended,
        because ID's inside the layout dict must be unique they need to have a suffix.

        .. code-block:: yaml

            mirror-0:
              /tmp/vdisk3
              /tmp/vdisk2
            mirror-1:
              /tmp/vdisk0
              /tmp/vdisk1

    .. warning::

        Pay attention to the order of your dict!

        .. code-block:: yaml

            - mirror:
              - /tmp/vdisk0
              - /tmp/vdisk1
            - /tmp/vdisk2

        The above will result in the following zpool create:

        .. code-block:: bash

            zpool create mypool mirror /tmp/vdisk0 /tmp/vdisk1 /tmp/vdisk2

        Creating a 3-way mirror! While you probably expect it to be mirror
        root vdev with 2 devices + a root vdev of 1 device!

    """
    ret = {"name": name, "changes": {}, "result": None, "comment": ""}

    # config defaults
    default_config = {
        "import": True,
        "import_dirs": None,
        "device_dir": None,
        "force": False,
    }
    if __grains__["kernel"] == "SunOS":
        default_config["device_dir"] = "/dev/dsk"
    elif __grains__["kernel"] == "Linux":
        default_config["device_dir"] = "/dev"

    # merge state config
    if config:
        default_config.update(config)
    config = default_config

    # ensure properties are zfs values
    if properties:
        properties = __utils__["zfs.from_auto_dict"](properties)
    elif properties is None:
        properties = {}
    if filesystem_properties:
        filesystem_properties = __utils__["zfs.from_auto_dict"](filesystem_properties)
    elif filesystem_properties is None:
        filesystem_properties = {}

    # parse layout
    vdevs = _layout_to_vdev(layout, config["device_dir"])
    if vdevs:
        vdevs.insert(0, name)

    # log configuration
    log.debug("zpool.present::%s::config - %s", name, config)
    log.debug("zpool.present::%s::vdevs - %s", name, vdevs)
    log.debug("zpool.present::%s::properties -  %s", name, properties)
    log.debug(
        "zpool.present::%s::filesystem_properties -  %s", name, filesystem_properties
    )

    # ensure the pool is present
    ret["result"] = False

    # don't do anything because this is a test
    if __opts__["test"]:
        if __salt__["zpool.exists"](name):
            ret["result"] = True
            ret["comment"] = "storage pool {} is {}".format(name, "uptodate")
        else:
            ret["result"] = None
            ret["changes"][name] = "imported" if config["import"] else "created"
            ret["comment"] = "storage pool {} would have been {}".format(
                name, ret["changes"][name]
            )

    # update pool
    elif __salt__["zpool.exists"](name):
        ret["result"] = True

        # fetch current pool properties
        properties_current = __salt__["zpool.get"](name, parsable=True)

        # build list of properties to update
        properties_update = []
        if properties:
            for prop in properties:
                # skip unexisting properties
                if prop not in properties_current:
                    log.warning(
                        "zpool.present::%s::update - unknown property: %s", name, prop
                    )
                    continue

                # compare current and wanted value
                # Enabled "feature@" properties may report either "enabled" or
                # "active", depending on whether they're currently in-use.
                if prop.startswith("feature@") and properties_current[prop] == "active":
                    effective_property = "enabled"
                else:
                    effective_property = properties_current[prop]

                if effective_property != properties[prop]:
                    properties_update.append(prop)

        # update pool properties
        for prop in properties_update:
            res = __salt__["zpool.set"](name, prop, properties[prop])

            if res["set"]:
                if name not in ret["changes"]:
                    ret["changes"][name] = {}
                ret["changes"][name][prop] = properties[prop]
            else:
                ret["result"] = False
                if ret["comment"] == "":
                    ret["comment"] = "The following properties were not updated:"
                ret["comment"] = "{} {}".format(ret["comment"], prop)

        if ret["result"]:
            ret["comment"] = (
                "properties updated" if ret["changes"] else "no update needed"
            )

    # import or create the pool (at least try to anyway)
    else:
        # import pool
        if config["import"]:
            mod_res = __salt__["zpool.import"](
                name,
                force=config["force"],
                dir=config["import_dirs"],
            )

            ret["result"] = mod_res["imported"]
            if ret["result"]:
                ret["changes"][name] = "imported"
                ret["comment"] = f"storage pool {name} was imported"

        # create pool
        if not ret["result"] and vdevs:
            log.debug("zpool.present::%s::creating", name)

            # execute zpool.create
            mod_res = __salt__["zpool.create"](
                *vdevs,
                force=config["force"],
                properties=properties,
                filesystem_properties=filesystem_properties,
            )

            ret["result"] = mod_res["created"]
            if ret["result"]:
                ret["changes"][name] = "created"
                ret["comment"] = f"storage pool {name} was created"
            elif "error" in mod_res:
                ret["comment"] = mod_res["error"]
            else:
                ret["comment"] = f"could not create storage pool {name}"

        # give up, we cannot import the pool and we do not have a layout to create it
        if not ret["result"] and not vdevs:
            ret["comment"] = (
                "storage pool {} was not imported, no (valid) layout specified for"
                " creation".format(name)
            )

    return ret


def absent(name, export=False, force=False):
    """
    ensure storage pool is absent on the system

    name : string
        name of storage pool
    export : boolean
        export instead of destroy the zpool if present
    force : boolean
        force destroy or export

    """
    ret = {"name": name, "changes": {}, "result": None, "comment": ""}

    # log configuration
    log.debug("zpool.absent::%s::config::force = %s", name, force)
    log.debug("zpool.absent::%s::config::export = %s", name, export)

    # ensure the pool is absent
    if __salt__["zpool.exists"](name):  # looks like we need to do some work
        mod_res = {}
        ret["result"] = False

        # NOTE: handle test
        if __opts__["test"]:
            ret["result"] = True

        # NOTE: try to export the pool
        elif export:
            mod_res = __salt__["zpool.export"](name, force=force)
            ret["result"] = mod_res["exported"]

        # NOTE: try to destroy the pool
        else:
            mod_res = __salt__["zpool.destroy"](name, force=force)
            ret["result"] = mod_res["destroyed"]

        if ret["result"]:  # update the changes and comment
            ret["changes"][name] = "exported" if export else "destroyed"
            ret["comment"] = "storage pool {} was {}".format(name, ret["changes"][name])
        elif "error" in mod_res:
            ret["comment"] = mod_res["error"]

    else:  # we are looking good
        ret["result"] = True
        ret["comment"] = f"storage pool {name} is absent"

    return ret
