#
# Copyright (c) 2013-2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#

import logging
import re

from django import template
from django.urls import reverse  # noqa
from django.utils.text import format_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy

from horizon import exceptions
from horizon import tables

from starlingx_dashboard.api import sysinv

LOG = logging.getLogger(__name__)


class CreateStorageVolume(tables.LinkAction):
    name = "createstoragevolume"
    verbose_name = ("Assign Storage Function")
    url = "horizon:admin:inventory:addstoragevolume"
    classes = ("ajax-modal", "btn-create")
    icon = "plus"

    def get_link_url(self, datum=None):
        host_id = self.table.kwargs['host_id']
        return reverse(self.url, args=(host_id,))

    def allowed(self, request, datum):
        host_id = self.table.kwargs['host_id']
        is_host_with_storage = sysinv.is_host_with_storage(request, host_id)
        host = self.table.kwargs['host']
        self.verbose_name = _("Assign Storage Function")

        classes = [c for c in self.classes if c != "disabled"]
        self.classes = classes

        if not is_host_with_storage:
            return False

        if host._administrative == 'unlocked':
            if (is_host_with_storage and
                    host._personality == sysinv.PERSONALITY_STORAGE and
                    "disabled" not in self.classes):
                self.classes = [c for c in self.classes] + ['disabled']
                self.verbose_name = format_lazy(
                    '{verbose_name} {node_unlocked}',
                    verbose_name=self.verbose_name,
                    node_unlocked=_("(Node Unlocked)"))
        return True


class CreatePartition(tables.LinkAction):
    name = "createpartition"
    verbose_name = ("Create a new partition")
    url = "horizon:admin:inventory:createpartition"
    classes = ("ajax-modal", "btn-create")
    icon = "plus"

    def get_link_url(self, datum=None):
        host_id = self.table.kwargs['host_id']
        return reverse(self.url, args=(host_id,))

    def allowed(self, request, datum):
        host = self.table.kwargs['host']
        self.verbose_name = _("Create a new partition")

        classes = [c for c in self.classes if c != "disabled"]
        self.classes = classes

        if host._personality != 'storage':
            return True

        return True


class DeletePartition(tables.DeleteAction):
    @staticmethod
    def action_present(count):
        return ungettext_lazy(
            "Delete Partition",
            "Delete Partitions",
            count
        )

    @staticmethod
    def action_past(count):
        return ungettext_lazy(
            "Deleted Partition",
            "Deleted Partitions",
            count
        )

    def allowed(self, request, partition=None):
        host = self.table.kwargs['host']
        PARTITION_IN_USE_STATUS = sysinv.PARTITION_IN_USE_STATUS
        PARTITION_STATUS_MSG = sysinv.PARTITION_STATUS_MSG

        if partition:
            if partition.type_guid != sysinv.USER_PARTITION_PHYS_VOL:
                return False

            if (partition.status ==
                    PARTITION_STATUS_MSG[PARTITION_IN_USE_STATUS]):
                return False

            if partition.ipv_uuid:
                return False

            # Get all the partitions from the same disk.
            disk_partitions = \
                sysinv.host_disk_partition_list(request, host.uuid,
                                                partition.idisk_uuid)

            if partition.device_path:
                partition_number = re.match('.*?([0-9]+)$',
                                            partition.device_path).group(1)
                for dpart in disk_partitions:
                    dpart_number = re.match('.*?([0-9]+)$',
                                            dpart.device_path).group(1)
                    if int(dpart_number) > int(partition_number):
                        return False

        return True

    def delete(self, request, partition_id):
        host_id = self.table.kwargs['host_id']
        try:
            sysinv.host_disk_partition_delete(request, partition_id)
        except Exception as e:
            msg = _('Failed to delete host %(hid)s partition %(pv)s. '
                    '%(e_msg)s') % {'hid': host_id,
                                    'pv': partition_id,
                                    'e_msg': e}
            LOG.info(msg)
            redirect = reverse('horizon:admin:inventory:detail',
                               args=(host_id,))
            exceptions.handle(request, msg, redirect=redirect)


class EditPartition(tables.LinkAction):
    name = "editpartition"
    verbose_name = _("Edit")
    url = "horizon:admin:inventory:editpartition"

    classes = ("ajax-modal", "btn-edit")

    def get_link_url(self, partition):
        host_id = self.table.kwargs['host_id']
        return reverse(self.url, args=(host_id, partition.uuid))

    def allowed(self, request, partition=None):
        host = self.table.kwargs['host']
        PARTITION_IN_USE_STATUS = sysinv.PARTITION_IN_USE_STATUS
        PARTITION_STATUS_MSG = sysinv.PARTITION_STATUS_MSG

        if partition:
            pv = None

            if partition.type_guid != sysinv.USER_PARTITION_PHYS_VOL:
                return False

            if partition.ipv_uuid:
                pv = sysinv.host_pv_get(
                    request, partition.ipv_uuid)
                if pv.lvm_vg_name == sysinv.LVG_CINDER_VOLUMES:
                    if (host.personality == "Controller-Active" and
                            host._administrative == 'unlocked'):
                        return False
                else:
                    return False

            if (partition.status ==
                    PARTITION_STATUS_MSG[PARTITION_IN_USE_STATUS]):
                if not (pv and
                        pv.lvm_vg_name == sysinv.LVG_CINDER_VOLUMES):
                    return False

            # Get all the partitions from the same disk.
            disk_partitions = \
                sysinv.host_disk_partition_list(request,
                                                host.uuid,
                                                partition.idisk_uuid)

            if partition.device_path:
                partition_number = re.match('.*?([0-9]+)$',
                                            partition.device_path).group(1)
                for dpart in disk_partitions:
                    dpart_number = re.match('.*?([0-9]+)$',
                                            dpart.device_path).group(1)
                    if int(dpart_number) > int(partition_number):
                        return False

        return True

# ##########
# TABLES
# ##########


def get_model_num(disk):
    return disk.get_model_num()


def get_disk_info(disk):
    template_name = 'admin/inventory/_disk_info.html'
    context = {
        "disk": disk,
        "disk_info": disk.device_path,
        "id": disk.uuid,
    }
    return template.loader.render_to_string(template_name, context)


class DisksTable(tables.DataTable):
    uuid = tables.Column('uuid',
                         verbose_name=('UUID'))
    disk_info = tables.Column(get_disk_info,
                              verbose_name=_("Disk info"),
                              attrs={'data-type': 'disk_info'})
    type = tables.Column('device_type',
                         verbose_name=('Type'))
    size = tables.Column('size_gib',
                         verbose_name=('Size (GiB)'))
    available_size = tables.Column('available_gib',
                                   verbose_name=('Available Size (GiB)'))
    rpm = tables.Column('rpm',
                        verbose_name=('RPM'))

    serial_id = tables.Column('serial_id',
                              verbose_name=('Serial ID'))

    model_num = tables.Column(get_model_num,
                              verbose_name=('Model'))

    def get_object_id(self, datum):
        return str(datum.uuid)

    class Meta(object):
        name = "disks"
        verbose_name = ("Disks")
        columns = ('uuid', 'disk_info', 'type', 'size', 'available_size',
                   'rpm', 'serial_id', 'model_num')
        multi_select = False
        table_actions = ()
        hidden_title = False
        footer = False


class EditStor(tables.LinkAction):
    name = "editstoragevolume"
    verbose_name = _("Edit")
    url = "horizon:admin:inventory:editstoragevolume"

    classes = ("ajax-modal", "btn-edit")

    def get_link_url(self, stor):
        host_id = self.table.kwargs['host_id']
        return reverse(self.url, args=(host_id, stor.uuid))

    def allowed(self, request, stor=None):
        host = self.table.kwargs['host']

        classes = [c for c in self.classes if c != "disabled"]
        self.classes = classes

        if host._administrative == 'unlocked':
            if 'storage' in host._subfunctions:
                if "disabled" not in self.classes:
                    self.classes = [c for c in self.classes] + ['disabled']

        if stor and stor.function == 'osd':
            forihostuuid = self.table.kwargs['host'].uuid
            journal_stors = \
                sysinv.host_stor_get_by_function(request, forihostuuid,
                                                 'journal')

            if not journal_stors:
                self.classes = [c for c in self.classes] + ['disabled']

            return True


class DeleteStor(tables.DeleteAction):
    @staticmethod
    def action_present(count):
        return ungettext_lazy(
            "Delete Journal",
            "Delete Journals",
            count
        )

    @staticmethod
    def action_past(count):
        return ungettext_lazy(
            "Deleted Journal",
            "Deleted Journals",
            count
        )

    def allowed(self, request, stor):
        host = self.table.kwargs['host']

        classes = [c for c in self.classes if c != "disabled"]
        self.classes = classes

        if host._administrative == 'unlocked':
            if 'storage' in host._subfunctions:
                if "disabled" not in self.classes:
                    self.classes = [c for c in self.classes] + ['disabled']

        if stor:
            return stor.function == 'journal'

    def delete(self, request, obj_id):
        sysinv.host_stor_delete(request, obj_id)


class StorageVolumesTable(tables.DataTable):
    uuid = tables.Column('uuid',
                         verbose_name=('UUID'))
    osdid = tables.Column('osdid',
                          verbose_name=('OSD ID'))
    function = tables.Column('function',
                             verbose_name=('Function'))
    state = tables.Column('state',
                          verbose_name=('State'))
    idisk_uuid = tables.Column('idisk_uuid',
                               verbose_name=('Disk UUID'))
    journal_path = tables.Column('journal_path',
                                 verbose_name=('Journal Path'))
    journal_size_mib = tables.Column('journal_size_gib',
                                     verbose_name=('Journal GiB'))
    journal_location = tables.Column('journal_location',
                                     verbose_name=('Journal Location'))

    def get_object_id(self, datum):
        return str(datum.uuid)

    class Meta(object):
        name = "storagevolumes"
        verbose_name = ("Storage Functions")
        columns = ('uuid', 'function', 'osdid', 'state', 'idisk_uuid',
                   'journal_path', 'journal_size_mib', 'journal_location')
        multi_select = False
        row_actions = (DeleteStor, EditStor,)
        table_actions = (CreateStorageVolume,)
        hidden_title = False
        footer = False


class AddLocalVolumeGroup(tables.LinkAction):
    name = "addlocalvolumegroup"
    verbose_name = ("Add Local Volume Group")
    url = "horizon:admin:inventory:addlocalvolumegroup"
    classes = ("ajax-modal", "btn-create")
    icon = "plus"

    def get_link_url(self, datum=None):
        host_id = self.table.kwargs['host_id']
        return reverse(self.url, args=(host_id,))

    def allowed(self, request, datum):
        host = self.table.kwargs['host']
        self.verbose_name = _("Add Local Volume Group")
        classes = [c for c in self.classes if c != "disabled"]
        self.classes = classes

        if not host._administrative == 'locked':
            if host.config_required is False:
                if "disabled" not in self.classes:
                    self.classes = [c for c in self.classes] + ['disabled']
                    self.verbose_name = format_lazy(
                        '{verbose_name} {node_unlocked}',
                        verbose_name=self.verbose_name,
                        node_unlocked=_("(Node Unlocked)"))

        # LVGs that are considered as "present" in the system are those
        # in an adding or provisioned state.
        current_lvg_states = [sysinv.LVG_ADD, sysinv.LVG_PROV]
        ilvg_list = sysinv.host_lvg_list(request, host.uuid)
        current_lvgs = [lvg.lvm_vg_name for lvg in ilvg_list
                        if lvg.vg_state in current_lvg_states]
        compatible_lvgs = []

        if 'worker' in host._subfunctions:
            compatible_lvgs += [sysinv.LVG_NOVA_LOCAL, sysinv.LVG_CGTS_VG]

        allowed_lvgs = set(compatible_lvgs) - set(current_lvgs)
        if not any(allowed_lvgs):
            if "disabled" not in self.classes:
                self.classes = [c for c in self.classes] + ['disabled']
                self.verbose_name = format_lazy(
                    '{verbose_name} {all_added}',
                    verbose_name=self.verbose_name,
                    all_added=_("(All Added)"))
        return True  # The action should always be displayed


class RemoveLocalVolumeGroup(tables.DeleteAction):
    @staticmethod
    def action_present(count):
        return ungettext_lazy(
            "Delete Local Volume Group",
            "Delete Local Volume Groups",
            count
        )

    @staticmethod
    def action_past(count):
        return ungettext_lazy(
            "Deleted Local Volume Group",
            "Deleted Local Volume Groups",
            count
        )

    def allowed(self, request, lvg=None):
        host = self.table.kwargs['host']
        storage_backend = sysinv.get_storage_backend(request)

        if lvg.lvm_vg_name == sysinv.LVG_NOVA_LOCAL:
            return ((host._administrative == 'locked') or
                    (host.config_required is True))
        elif lvg.lvm_vg_name == sysinv.LVG_CINDER_VOLUMES:
            return (sysinv.STORAGE_BACKEND_LVM not in storage_backend and
                    sysinv.LVG_ADD in lvg.vg_state)

        return False

    def delete(self, request, lvg_id):
        host_id = self.table.kwargs['host_id']
        try:
            sysinv.host_lvg_delete(request, lvg_id)
        except Exception as e:
            msg = _('Failed to delete host %(hid)s local '
                    'volume group %(lvg)s '
                    '%(e_msg)s') % \
                {'hid': host_id, 'lvg': lvg_id, 'e_msg': e}

            redirect = reverse('horizon:admin:inventory:detail',
                               args=(host_id,))
            exceptions.handle(request, msg, redirect=redirect)


class LocalVolumeGroupsTable(tables.DataTable):
    name = tables.Column('lvm_vg_name',
                         link="horizon:admin:inventory:localvolumegroupdetail",
                         verbose_name=('Name'))
    state = tables.Column('vg_state',
                          verbose_name=('State'))
    access = tables.Column('lvm_vg_access',
                           verbose_name=('Access'))
    size = tables.Column('lvm_vg_size_gib',
                         verbose_name=('Size (GiB)'))
    avail_size = tables.Column('lvm_vg_avail_size_gib',
                               verbose_name=('Avail Size (GiB)'))
    pvs = tables.Column('lvm_cur_pv',
                        verbose_name=('Current Physical Volumes'))
    lvs = tables.Column('lvm_cur_lv',
                        verbose_name=('Current Logical Volumes'))

    def get_object_id(self, datum):
        return str(datum.uuid)

    def get_object_display(self, datum):
        msg = datum.uuid
        if datum.lvm_vg_name:
            msg += " (%s)" % datum.lvm_vg_name
        return str(msg)

    class Meta(object):
        name = "localvolumegroups"
        verbose_name = ("Local Volume Groups")
        columns = ('name', 'state', 'access', 'size', 'avail_size',
                   'pvs', 'lvs',)
        multi_select = False
        row_actions = (RemoveLocalVolumeGroup,)
        table_actions = (AddLocalVolumeGroup,)
        hidden_title = False
        footer = False


class AddPhysicalVolume(tables.LinkAction):
    name = "addphysicalvolume"
    verbose_name = ("Add Physical Volume")
    url = "horizon:admin:inventory:addphysicalvolume"
    classes = ("ajax-modal", "btn-create")
    icon = "plus"

    def get_link_url(self, datum=None):
        host_id = self.table.kwargs['host_id']
        return reverse(self.url, args=(host_id,))

    def allowed(self, request, datum):
        host = self.table.kwargs['host']
        self.verbose_name = _("Add Physical Volume")
        classes = [c for c in self.classes if c != "disabled"]
        self.classes = classes

        # Allow adding to any controller
        if host._personality == sysinv.PERSONALITY_CONTROLLER:
            return True

        # Allow adding to any locked host
        if host._administrative != 'locked':
            if 'worker' in host._subfunctions and \
               host.config_required is False:
                if "disabled" not in self.classes:
                    self.classes = [c for c in self.classes] + ['disabled']
                    self.verbose_name = format_lazy(
                        '{verbose_name} {node_unlocked}',
                        verbose_name=self.verbose_name,
                        node_unlocked=_("(Node Unlocked)"))
        return True  # The action should always be displayed


class RemovePhysicalVolume(tables.DeleteAction):
    @staticmethod
    def action_present(count):
        return ungettext_lazy(
            "Delete Physical Volume",
            "Delete Physical Volumes",
            count
        )

    @staticmethod
    def action_past(count):
        return ungettext_lazy(
            "Deleted Physical Volume",
            "Deleted Physical Volumes",
            count
        )

    def allowed(self, request, pv=None):
        host = self.table.kwargs['host']
        storage_backend = sysinv.get_storage_backend(request)

        if pv.lvm_vg_name == sysinv.LVG_NOVA_LOCAL:
            return ((host._administrative == 'locked') or
                    (host.config_required is True))
        elif pv.lvm_vg_name == sysinv.LVG_CINDER_VOLUMES:
            return (sysinv.STORAGE_BACKEND_LVM not in storage_backend and
                    sysinv.PV_ADD in pv.pv_state)

        return False

    def delete(self, request, pv_id):
        host_id = self.table.kwargs['host_id']
        try:
            sysinv.host_pv_delete(request, pv_id)
        except Exception as e:
            msg = _('Failed to delete host %(hid)s physical volume %(pv)s. '
                    '%(e_msg)s') % {'hid': host_id, 'pv': pv_id, 'e_msg': e}
            LOG.info(msg)
            redirect = reverse('horizon:admin:inventory:detail',
                               args=(host_id,))
            exceptions.handle(request, msg, redirect=redirect)


class PhysicalVolumesTable(tables.DataTable):
    name = tables.Column('lvm_pv_name',
                         link="horizon:admin:inventory:physicalvolumedetail",
                         verbose_name=('Name'))
    pv_state = tables.Column('pv_state',
                             verbose_name=('State'))
    pv_type = tables.Column('pv_type',
                            verbose_name=('Type'))
    disk_or_part_uuid = tables.Column('disk_or_part_uuid',
                                      verbose_name=('Disk or Partition UUID'))
    disk_or_part_device_path = tables.Column('disk_or_part_device_path',
                                             verbose_name=('Disk or Partition'
                                                           ' Device Path'))
    lvm_vg_name = tables.Column('lvm_vg_name',
                                verbose_name=('LVM Volume Group Name'))

    def get_object_id(self, datum):
        return str(datum.uuid)

    def get_object_display(self, datum):
        msg = datum.uuid
        if datum.lvm_pv_name:
            msg += " (%s)" % datum.lvm_pv_name
        return str(msg)

    class Meta(object):
        name = "physicalvolumes"
        verbose_name = ("Physical Volumes")
        columns = ('name', 'pv_state', 'pv_type', 'disk_or_part_uuid',
                   'disk_or_part_device_node', 'disk_or_part_device_path',
                   'lvm_vg_name')
        multi_select = False
        table_actions = (AddPhysicalVolume,)
        row_actions = (RemovePhysicalVolume,)
        hidden_title = False
        footer = False


class PartitionsTable(tables.DataTable):
    uuid = tables.Column('uuid',
                         verbose_name=('UUID'))
    size_mib = tables.Column('size_gib',
                             verbose_name=('Size (GiB)'))
    device_path = tables.Column('device_path',
                                verbose_name=('Partition Device Path'))
    type_name = tables.Column('type_name',
                              verbose_name=('Partition Type'))
    ipv_uuid = tables.Column('ipv_uuid',
                             verbose_name=('Physical Volume UUID'))
    idisk_uuid = tables.Column('disk_uuid',
                               verbose_name=('Disk UUID'))
    status = tables.Column('status',
                           verbose_name=('Status'))

    def get_object_id(self, datum):
        return str(datum.uuid)

    def get_object_display(self, datum):
        msg = datum.uuid
        if datum.device_path:
            msg += " (%s)" % datum.device_path
        return str(msg)

    class Meta(object):
        name = "partitions"
        verbose_name = ("Partitions")
        columns = ('uuid', 'device_path', 'size_mib', 'type_name', 'status')
        multi_select = False
        row_actions = (EditPartition, DeletePartition,)
        table_actions = (CreatePartition,)
        hidden_title = False
        footer = False
