import glob
import logging
import os
import re

from django.db.models import Q

from freenasUI.truenas import ses

log = logging.getLogger('truenas.notifier')


class TNNotifier(object):

    def get_swapsize(self):
        """
        TrueNAS uses swap in the boot device
        """
        return 0

    def get_enclosure_stat(self, encnumb):
        """
        Call getencstat for single enclosures device
        """

        cmd = "/usr/sbin/getencstat -V /dev/ses%d" % (encnumb,)
        p1 = self._pipeopen(cmd)
        out = p1.communicate()[0]
        return out

    def get_enclosures_stat(self):
        """
        Call getencstat for all enclosures devices avaiable

        Returns:
            dict: all enclosures available with index as key
        """

        output = {}
        encnumb = 0
        while os.path.exists('/dev/ses%d' % (encnumb, )):
            out = self.get_enclosure_stat(encnumb)
            if out:
                # In short, getencstat reserves the exit codes for
                # failing to change states and doesn't actually
                # error out if it can't read or poke at the enclosure
                # device.
                output[encnumb] = out
            encnumb += 1

        return output

    NAME_MAP = {
        'quanta s2lr 0b01 stor2': 'Internal Backplane',
        'storage 1729': '24 Bay Storage Expansion Shelf',
    }

    def get_enclosure_name(self, encnumb):
        data = self.get_enclosure_stat(encnumb)
        reg = re.search(r'Enclosure Name: (.+)', data)
        if reg:
            name = reg.groups()[0].strip()
        else:
            name = 'ses%d' % (encnumb, )

        for key, val in self.NAME_MAP.items():
            if name.lower().startswith(key):
                name = val
                break
        return name

    def __find_enclosure_slot(self, enclosures, devname):
        """
        Get the element that the device name points to
        getencstat /dev/ses0 | grep da6
        Element 0x7: Array Device Slot, status: OK (0x01 0x00 0x00 0x00),
        descriptor: 'Slot 07', dev: 'da6,pass6'
        What we are interested in is the 0x7

        Returns:
            A tuple of the form (enclosure-slot-number, element, )

        Raises:
            AssertionError: enclosure slot not found
        """
        for encnum, elements in enclosures.items():
            reg = re.search(
                r'Element\s+(?P<element>.+?):.*?dev.*?\b%s\b' % (devname,),
                elements,
                re.M
            )
            if reg:
                return encnum, int(reg.group("element"), 16)
        raise AssertionError("Enclosure slot not found for %s" % devname)

    def control_enclosure(self, device, status):
        """
        This method provides control of enclosure slots.  Set status to
        the device in the enclosure, and status to one of:
        clear, identify, fault

        clear will cause the enclosure to return to normal
        identify will cause the enclosure to be set to identify device
        fault will set the enclosure to fault device

        Example:
            control_enclosure("/dev/da6", "fault")

        Returns:
            True if the enclosure was poked sucessfully False otherwise
        """

        if status not in ses.ENCLOSURE_ACTIONS:
            return False

        if not os.path.exists(device):
            return False

        # Strip off the /dev part of /dev/da6 as the command we will
        # be passing this to omits the /dev/ part of the device name
        devname = device.split('/')[-1:][0]

        encs = ses.Enclosures()
        try:
            element = encs.find_device_slot(devname)
        except AssertionError:
            return False
        return element.device_slot_set(status)

    def get_enc_slot_from_dev(self, devname):
        from freenasUI.storage.models import Disk

        stats = self.get_enclosures_stat()
        try:
            return self.__find_enclosure_slot(stats, devname)
        except AssertionError:
            pass
        log.debug(
            "get_enc_slot_from_dev for '%s' not found in enclosure, "
            "trying from disk cache table",
            devname
        )

        devname = devname.replace("/dev/", "")
        devname = re.sub(r'^(.*)p[0-9]+$', r'\1', devname)
        if devname.startswith("multipath/"):
            query = Q(disk_multipath_name=devname.replace("multipath/", ""))
        else:
            query = Q(disk_name=devname)
        disk = Disk.objects.filter(query).order_by('disk_enabled')
        if disk.exists():
            disk = disk[0]
            slot = disk.get_enc_slot()
            return slot
        return None

    def get_disks(self):
        if glob.glob('/dev/isp*'):
            """
            We need to rescan in case of a FC initiator
            """
            self._pipeopen("/sbin/camcontrol rescan all").wait()
            while True:
                try:
                    doc = self.sysctl('kern.geom.confxml')
                    break
                except:
                    pass
            while True:
                self._system('/bin/sleep 1')
                try:
                    newdoc = self.sysctl('kern.geom.confxml')
                    if doc == newdoc:
                        break
                    doc = newdoc
                except:
                    pass

        """ Rescan and sync multipath before everything """
        self.multipath_sync()
        disks = super(TNNotifier, self).get_disks()
        return disks

    def sync_disk_extra(self, disk, add=False):
        encs = ses.Enclosures()
        try:
            element = encs.find_device_slot(disk.disk_name)
            if add:
                dskslot = disk.get_enc_slot()
                if (
                    dskslot and (
                        element.enclosure.num != dskslot[0] or
                        element.slot != dskslot[1]
                    )
                ) or dskslot:
                    disk.set_enc_slot(element.enclosure.num, element.slot)
            else:
                disk.set_enc_slot(element.enclosure.num, element.slot)
        except:
            disk.disk_enclosure_slot = None

    def zpool_enclosure_sync(self, poolname=None):
        """
        Sync enclosure of a given ZFS pool
        """
        from freenasUI.storage.models import Disk, Volume

        encs = ses.Enclosures()
        if len(list(encs)) == 0:
            log.debug('Enclosure not found, skipping enclosure sync')
            return None

        if not poolname:
            pools = [vol.vol_name for vol in Volume.objects.filter(
                vol_fstype='ZFS')
            ]
        else:
            pools = [poolname]

        seen_devs = []
        for poolname in pools:
            try:
                pool = self.zpool_parse(poolname)
            except:
                pool = None
            if not pool:
                continue
            for dev in pool.find_not_online():
                seen_devs.append(dev)
                if not dev.path:
                    continue
                enc_slot = self.get_enc_slot_from_dev(dev.path)
                #FIXME: dev.path is not enough
                if enc_slot:
                    enc = encs.get_by_id(enc_slot[0])
                    element = enc.get_by_slot(enc_slot[1])
                    element.device_slot_set("fault")
            if pool.spares:
                for vdev in pool.spares.children:
                    for dev in vdev.children:
                        if dev.status != 'AVAIL' or not dev.disk:
                            continue
                        seen_devs.append(dev)
                        slot = self.get_enc_slot_from_dev(dev.disk)
                        if slot:
                            enc = encs.get_by_id(slot[0])
                            element = enc.get_by_slot(slot[1])
                            log.debug(
                                "Identifying bay slot for '%s'", dev.disk
                            )
                            element.device_slot_set("identify")

            """
            Go through all devs in the pool
            Make sure the enclosure status is clear for everything else
            """
            for dev in pool.get_devs():
                if dev in seen_devs or not dev.devname:
                    continue
                seen_devs.append(dev)
                slot = self.get_enc_slot_from_dev(dev.devname)
                if not slot:
                    continue
                enc = encs.get_by_id(slot[0])
                element = enc.get_by_slot(slot[1])
                element.device_slot_set("clear")

        disks = []
        for dev in seen_devs:
            if not dev.disk:
                continue
            if dev.disk.startswith('multipath'):
                disk = Disk.objects.filter(
                    disk_multipath_name=dev.disk.replace("multipath/", "")
                ).order_by('disk_enabled')
                if disk.exists():
                    disks.append(disk[0].disk_name)
            else:
                disks.append(dev.disk)
        """
        Clear all slots without an attached disk
        """
        for enc in encs:
            for element in enc.iter_by_name().get("Array Device Slot", []):
                if not element.devname or element.devname not in disks:
                    element.device_slot_set("clear")

    def _multipath_is_active(self, name, geom):
        descr = geom.xpath("./provider/config/descr")[0].text or ""
        # List of devices that benefit from active/active multipath.
        if (
            descr == 'STEC ZeusRAM' or
            descr.startswith('VIOLIN') or
            descr.startswith('3PAR')
        ):
            return True
        return False

    def multipath_create(self, name, consumers, actives=None, mode=None):
        if mode is None:
            mode = 'R'
        elif actives and consumers[0] in actives:
            mode = 'A'
        return super(TNNotifier, self).multipath_create(
            name, consumers, mode=mode
        )

    def set_global_alarm(self, value):
        cmd = "/usr/local/sbin/dmidecode -s system-manufacturer"
        p1 = self._pipeopen(cmd)
        if p1.communicate()[0].strip('\n') == "GIGABYTE":
            try:
                with open('/dev/cuau1', 'w') as f:
                    if value == 'OK':
                        f.write('1')
                    else:
                        f.write('f')
            except IOError, e:
                log.debug('Failed to set global alarm: %s', e)
                return False
            finally:
                return True
