#
# Kickstart module for the storage.
#
# Copyright (C) 2018 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 pyanaconda.core.signal import Signal
from pyanaconda.core.dbus import DBus
from pyanaconda.core.storage import blivet_version
from pyanaconda.modules.common.base import KickstartService
from pyanaconda.modules.common.constants.services import STORAGE
from pyanaconda.modules.common.containers import TaskContainer
from pyanaconda.modules.common.errors.storage import InvalidStorageError
from pyanaconda.modules.common.structures.requirement import Requirement
from pyanaconda.modules.storage.bootloader import BootloaderModule
from pyanaconda.modules.storage.checker import StorageCheckerModule
from pyanaconda.modules.storage.dasd import DASDModule
from pyanaconda.modules.storage.devicetree import DeviceTreeModule, create_storage
from pyanaconda.modules.storage.disk_initialization import DiskInitializationModule
from pyanaconda.modules.storage.disk_selection import DiskSelectionModule
from pyanaconda.modules.storage.fcoe import FCOEModule
from pyanaconda.modules.storage.installation import MountFilesystemsTask, CreateStorageLayoutTask, \
    WriteConfigurationTask
from pyanaconda.modules.storage.iscsi import ISCSIModule
from pyanaconda.modules.storage.kickstart import StorageKickstartSpecification
from pyanaconda.modules.storage.nvdimm import NVDIMMModule
from pyanaconda.modules.storage.partitioning.constants import PartitioningMethod
from pyanaconda.modules.storage.partitioning.factory import PartitioningFactory
from pyanaconda.modules.storage.partitioning.validate import StorageValidateTask
from pyanaconda.modules.storage.platform import platform
from pyanaconda.modules.storage.reset import ScanDevicesTask
from pyanaconda.modules.storage.snapshot import SnapshotModule
from pyanaconda.modules.storage.storage_interface import StorageInterface
from pyanaconda.modules.storage.teardown import UnmountFilesystemsTask, TeardownDiskImagesTask
from pyanaconda.modules.storage.zfcp import ZFCPModule

from pyanaconda.anaconda_loggers import get_module_logger
log = get_module_logger(__name__)


class StorageService(KickstartService):
    """The Storage service."""

    def __init__(self):
        super().__init__()
        # The storage model.
        self._current_storage = None
        self._storage_playground = None
        self.storage_changed = Signal()

        # The created partitioning modules.
        self._created_partitioning = []
        self.created_partitioning_changed = Signal()

        # The applied partitioning module.
        self._applied_partitioning = None
        self.applied_partitioning_changed = Signal()
        self.partitioning_reset = Signal()

        # Initialize modules.
        self._modules = []

        self._storage_checker_module = StorageCheckerModule()
        self._add_module(self._storage_checker_module)

        self._device_tree_module = DeviceTreeModule()
        self._add_module(self._device_tree_module)

        self._disk_init_module = DiskInitializationModule()
        self._add_module(self._disk_init_module)

        self._disk_selection_module = DiskSelectionModule()
        self._add_module(self._disk_selection_module)

        self._snapshot_module = SnapshotModule()
        self._add_module(self._snapshot_module)

        self._bootloader_module = BootloaderModule()
        self._add_module(self._bootloader_module)

        self._fcoe_module = FCOEModule()
        self._add_module(self._fcoe_module)

        self._iscsi_module = ISCSIModule()
        self._add_module(self._iscsi_module)

        self._nvdimm_module = NVDIMMModule()
        self._add_module(self._nvdimm_module)

        self._dasd_module = DASDModule()
        self._add_module(self._dasd_module)

        self._zfcp_module = ZFCPModule()
        self._add_module(self._zfcp_module)

        # Connect modules to signals.
        self.storage_changed.connect(
            self._device_tree_module.on_storage_changed
        )
        self.storage_changed.connect(
            self._disk_init_module.on_storage_changed
        )
        self.storage_changed.connect(
            self._disk_selection_module.on_storage_changed
        )
        self.storage_changed.connect(
            self._snapshot_module.on_storage_changed
        )
        self.storage_changed.connect(
            self._bootloader_module.on_storage_changed
        )
        self.storage_changed.connect(
            self._nvdimm_module.on_storage_changed
        )
        self.storage_changed.connect(
            self._dasd_module.on_storage_changed
        )
        self._disk_init_module.format_unrecognized_enabled_changed.connect(
            self._dasd_module.on_format_unrecognized_enabled_changed
        )
        self._disk_init_module.format_ldl_enabled_changed.connect(
            self._dasd_module.on_format_ldl_enabled_changed
        )
        self._disk_selection_module.protected_devices_changed.connect(
            self.on_protected_devices_changed
        )

        # After connecting modules to signals, create the initial
        # storage model. It will be propagated to all modules.
        self._set_storage(create_storage())

    def _add_module(self, storage_module):
        """Add a base kickstart module."""
        self._modules.append(storage_module)

    def publish(self):
        """Publish the module."""
        TaskContainer.set_namespace(STORAGE.namespace)

        for kickstart_module in self._modules:
            kickstart_module.publish()

        DBus.publish_object(STORAGE.object_path, StorageInterface(self))
        DBus.register_service(STORAGE.service_name)

    @property
    def kickstart_specification(self):
        """Return the kickstart specification."""
        return StorageKickstartSpecification

    def process_kickstart(self, data):
        """Process the kickstart data."""
        # Process the kickstart data in modules.
        for kickstart_module in self._modules:
            kickstart_module.process_kickstart(data)

        # Set the default filesystem type.
        if data.autopart.autopart and data.autopart.fstype:
            self.storage.set_default_fstype(data.autopart.fstype)

        # Create a new partitioning module.
        partitioning_method = PartitioningFactory.get_method_for_kickstart(data)

        if partitioning_method:
            partitioning_module = self.create_partitioning(partitioning_method)
            partitioning_module.process_kickstart(data)

    def setup_kickstart(self, data):
        """Set up the kickstart data."""
        for kickstart_module in self._modules:
            kickstart_module.setup_kickstart(data)

        if self.applied_partitioning:
            self.applied_partitioning.setup_kickstart(data)

    def generate_kickstart(self):
        """Generate kickstart string representation of this module's data

        Adds Blivet version to the output because most of the strings come from Blivet anyway.
        """
        return "# Generated using Blivet version {}\n{}".format(
            blivet_version,
            super().generate_kickstart()
        )

    @property
    def storage(self):
        """The storage model.

        :return: an instance of Blivet
        """
        if self._storage_playground:
            return self._storage_playground

        if not self._current_storage:
            self._set_storage(create_storage())

        return self._current_storage

    def _set_storage(self, storage):
        """Set the current storage model.

        The current storage is the latest model of
        the system’s storage configuration created
        by scanning all devices.

        :param storage: a storage
        """
        self._current_storage = storage

        if self._storage_playground:
            return

        self.storage_changed.emit(storage)
        log.debug("The storage model has changed.")

    def _set_storage_playground(self, storage):
        """Set the storage playground.

        The storage playground is a model of a valid
        partitioned storage configuration, that can be
        used for an installation.

        :param storage: a storage or None
        """
        self._storage_playground = storage

        if storage is None:
            storage = self.storage

        self.storage_changed.emit(storage)
        log.debug("The storage model has changed.")

    def on_protected_devices_changed(self, protected_devices):
        """Update the protected devices in the storage model."""
        if not self._current_storage:
            return

        self.storage.protect_devices(protected_devices)

    def scan_devices_with_task(self):
        """Scan all devices with a task.

        We will reset a copy of the current storage model
        and switch the models if the reset is successful.

        :return: a task
        """
        # Copy the storage.
        storage = self.storage.copy()

        # Set up the storage.
        storage.ignored_disks = self._disk_selection_module.ignored_disks
        storage.exclusive_disks = self._disk_selection_module.exclusive_disks
        storage.protected_devices = self._disk_selection_module.protected_devices
        storage.disk_images = self._disk_selection_module.disk_images

        # Create the task.
        task = ScanDevicesTask(storage)
        task.succeeded_signal.connect(lambda: self._set_storage(storage))
        return task

    def create_partitioning(self, method: PartitioningMethod):
        """Create a new partitioning.

        Allowed values:
            AUTOMATIC
            CUSTOM
            MANUAL
            INTERACTIVE
            BLIVET

        :param PartitioningMethod method: a partitioning method
        :return: a partitioning module
        """
        module = PartitioningFactory.create_partitioning(method)

        # Update the module.
        module.on_storage_changed(
            self._current_storage
        )
        module.on_selected_disks_changed(
            self._disk_selection_module.selected_disks
        )

        # Connect the callbacks to signals.
        self.storage_changed.connect(
            module.on_storage_changed
        )
        self.partitioning_reset.connect(
            module.on_partitioning_reset
        )
        self._disk_selection_module.selected_disks_changed.connect(
            module.on_selected_disks_changed
        )

        # Update the list of modules.
        self._add_created_partitioning(module)
        return module

    @property
    def created_partitioning(self):
        """List of all created partitioning modules."""
        return self._created_partitioning

    def _add_created_partitioning(self, module):
        """Add a created partitioning module."""
        self._created_partitioning.append(module)
        self.created_partitioning_changed.emit(module)
        log.debug("Created the partitioning %s.", module)

    def apply_partitioning(self, module):
        """Apply a partitioning.

        :param module: a partitioning module
        :raise: InvalidStorageError if the partitioning is not valid
        """
        # Validate the partitioning.
        storage = module.storage.copy()
        task = StorageValidateTask(storage)
        report = task.run()

        if not report.is_valid():
            raise InvalidStorageError(" ".join(report.error_messages))

        # Apply the partitioning.
        self._set_storage_playground(storage)
        self._set_applied_partitioning(module)

    @property
    def applied_partitioning(self):
        """The applied partitioning."""
        return self._applied_partitioning

    def _set_applied_partitioning(self, module):
        """Set the applied partitioning.

        :param module: a partitioning module or None
        """
        self._applied_partitioning = module
        self.applied_partitioning_changed.emit()

        if module is None:
            module = "NONE"

        log.debug("The partitioning %s is applied.", module)

    def reset_partitioning(self):
        """Reset the partitioning."""
        self._set_storage_playground(None)
        self._set_applied_partitioning(None)
        self.partitioning_reset.emit()

    def collect_requirements(self):
        """Return installation requirements for this module.

        :return: a list of requirements
        """
        requirements = []

        # Add the platform requirements.
        for name in platform.packages:
            requirements.append(Requirement.for_package(
                name, reason="Required for the platform."
            ))

        # Add the storage requirements.
        for name in self.storage.packages:
            requirements.append(Requirement.for_package(
                name, reason="Required to manage storage devices."
            ))

        # Add other requirements, for example for bootloader.
        for kickstart_module in self._modules:
            requirements.extend(kickstart_module.collect_requirements())

        return requirements

    def install_with_tasks(self):
        """Returns installation tasks of this module.

        :returns: list of installation tasks
        """
        storage = self.storage

        return [
            CreateStorageLayoutTask(storage),
            MountFilesystemsTask(storage)
        ]

    def write_configuration_with_task(self):
        """Write the storage configuration with a task.

        FIXME: This is a temporary workaround.

        :return: an installation task
        """
        return WriteConfigurationTask(self.storage)

    def teardown_with_tasks(self):
        """Returns teardown tasks for this module.

        :return: a list installation tasks
        """
        storage = self.storage

        return [
            UnmountFilesystemsTask(storage),
            TeardownDiskImagesTask(storage)
        ]
