#
# Copyright (C) 2020 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
from blivet.errors import StorageError, InconsistentPVSectorSize
from blivet.size import Size

from dasbus.structure import compare_data

from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.i18n import _
from pyanaconda.modules.common.errors.configuration import StorageConfigurationError
from pyanaconda.modules.common.structures.device_factory import DeviceFactoryRequest
from pyanaconda.modules.common.task import Task
from pyanaconda.modules.storage.constants import INCONSISTENT_SECTOR_SIZES_SUGGESTIONS
from pyanaconda.modules.storage.partitioning.interactive.utils import destroy_device, \
    get_device_factory_arguments, revert_reformat, resize_device, reformat_device, \
    validate_label, change_encryption, rename_container

log = get_module_logger(__name__)

__all__ = ["ChangeDeviceTask"]


class ChangeDeviceTask(Task):
    """A task for changing a device in the device tree."""

    def __init__(self, storage, device, request: DeviceFactoryRequest,
                 original_request: DeviceFactoryRequest):
        """Create a task.

        FIXME: Remove device and original request from the arguments.

        :param storage: an instance of Blivet
        :param device: a device to change
        :param request: a device factory request
        :param original_request: an original device factory request
        """
        super().__init__()
        self._storage = storage
        self._device = device
        self._request = request
        self._original_request = original_request

    @property
    def name(self):
        """Name of this task."""
        return "Change a device"

    def run(self):
        """Change a device in the device tree.

        :raise: StorageConfigurationError if the device cannot be changed
        """
        log.debug("Change device: %s", self._request)

        # Nothing to do. Skip.
        if compare_data(self._request, self._original_request):
            log.debug("Nothing to change.")
            return

        try:
            # Change the container.
            self._rename_container()

            # Change or replace the device.
            if not self._device.raw_device.exists:
                self._replace_device()
            else:
                self._change_device()

        except InconsistentPVSectorSize as e:
            self._handle_storage_error(e, "\n\n".join([
                _("Failed to change a device."),
                str(e).strip(),
                _(INCONSISTENT_SECTOR_SIZES_SUGGESTIONS)
            ]))
        except StorageError as e:
            self._handle_storage_error(e, str(e))

    def _handle_storage_error(self, exception, message):
        """Handle the storage error."""
        log.error("Failed to change a device: %s", message)
        raise StorageConfigurationError(message) from exception

    def _rename_container(self):
        """Rename the existing container."""
        container_spec = self._request.container_spec
        container_name = self._request.container_name

        # Nothing to do.
        if not container_spec or container_spec == container_name:
            return

        container = self._storage.devicetree.resolve_device(container_spec)

        # Container doesn't exist.
        if not container:
            return

        log.debug("Changing container name: %s", container_name)

        try:
            rename_container(self._storage, container, container_name)
        except StorageError as e:
            log.error("Invalid container name: %s", e)
            raise StorageError(str(e)) from e

    def _replace_device(self):
        """Replace the nonexistent device with a new one.

        If something has changed but the device does not exist,
        there is no need to schedule actions on the device. It
        is only necessary to create a new device object which
        reflects the current choices.
        """
        log.debug("Replacing a nonexistent device.")
        device = self._device

        if self._should_remove_device():
            # Remove the current device.
            destroy_device(self._storage, device)

            # We don't want to pass the device if we removed it.
            self._request.device_spec = ""

        # Create a new device.
        log.debug("Creating a new device.")
        arguments = get_device_factory_arguments(self._storage, self._request)
        device = self._storage.factory_device(**arguments)

        # Update the device.
        self._device = device

    def _should_remove_device(self):
        """Should we remove the current device?"""
        new_request = self._request
        old_request = self._original_request

        # Check the device type.
        if old_request.device_type != new_request.device_type:
            return True

        # Check the container name.
        if old_request.container_name and new_request.container_name != old_request.container_name:
            return True

        return False

    def _change_device(self):
        """Change the configuration of the existing device."""
        log.debug("Modifying an existing device.")

        self._revert_device_reformat()
        self._change_device_size()

        if self._should_reformat_device():
            self._change_device_encryption()
            self._change_device_format()
        else:
            self._change_device_label()
            self._change_device_mount_point()

        self._change_device_name()

    def _revert_device_reformat(self):
        """Revert reformat of the device."""
        if self._request.reformat:
            return

        log.debug("Reverting device reformat.")
        revert_reformat(self._storage, self._device)

    def _change_device_size(self):
        """Resize the device."""
        size = Size(self._request.device_size)
        original_size = Size(self._original_request.device_size)

        if size == original_size:
            return

        log.debug("Changing device size: %s", size)
        resize_device(self._storage, self._device, size, original_size)

    def _should_reformat_device(self):
        """Should we reformat the device?

        :return: True of False
        """
        if not self._request.reformat:
            return False

        if self._device.format.exists:
            return True

        if self._original_request.device_encrypted != self._request.device_encrypted:
            return True

        if self._original_request.luks_version != self._request.luks_version:
            return True

        if self._original_request.format_type != self._request.format_type:
            return True

        return False

    def _change_device_encryption(self):
        """Change the device encryption."""
        storage = self._storage
        device = self._device
        encrypted = self._request.device_encrypted
        luks_version = self._request.luks_version

        if self._original_request.device_encrypted != encrypted:
            log.debug("Changing device encryption: %s", encrypted)
            device = change_encryption(
                storage=storage,
                device=device,
                encrypted=encrypted,
                luks_version=luks_version
            )
        elif encrypted and self._original_request.luks_version != luks_version:
            log.debug("Changing LUKS version: %s", luks_version)

            # LUKS version cannot be easily changed,
            # so remove the current LUKS device.
            device = change_encryption(
                storage=storage,
                device=device,
                encrypted=False,
                luks_version=luks_version
            )

            # And create a new one with the requested
            # LUKS version.
            device = change_encryption(
                storage=storage,
                device=device,
                encrypted=True,
                luks_version=luks_version
            )

        self._device = device

    def _change_device_format(self):
        """Change the device format."""
        log.debug("Changing device format: %s", self._request.format_type)

        reformat_device(
            storage=self._storage,
            device=self._device,
            fstype=self._request.format_type,
            mountpoint=self._request.mount_point,
            label=self._request.label
        )

    def _change_device_label(self):
        """Change the device label."""
        label = self._request.label

        if self._original_request.label == label:
            return

        if not hasattr(self._device.format, "label"):
            log.warning("Cannot set a label to the current format.")
            return

        if self._device.format.exists:
            log.warning("Cannot relabel already existing file system.")
            return

        if validate_label(label, self._device.format):
            log.warning("Cannot set an invalid label.")
            return

        log.debug("Changing device label: %s", label)
        self._device.format.label = label

    def _change_device_mount_point(self):
        """Change the device mount point."""
        mount_point = self._request.mount_point

        if not mount_point:
            return

        if self._original_request.mount_point == mount_point:
            return

        log.debug("Changing device mount point: %s", mount_point)
        self._device.format.mountpoint = mount_point

    def _change_device_name(self):
        """Change the device name."""
        name = self._request.device_name
        original_name = self._original_request.device_name

        if name == original_name:
            return

        log.debug("Changing device name: %s", name)

        try:
            self._device.raw_device.name = name
        except ValueError as e:
            log.error("Invalid device name: %s", e)
            raise StorageError(str(e)) from e
