# Copyright (c) 2015 iXsystems, Inc.
# All rights reserved.
# This file is a part of TrueNAS
# and may not be copied and/or distributed
# without the express permission of iXsystems.

import glob
import logging
import os
import re
import subprocess

from django.db.models import Q

from freenasUI.middleware.client import client
from freenasUI.truenas import ses

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


class TNNotifier(object):

    def is_freenas(self):
        return False

    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 = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True)
        # getencstat may return not valid utf8 bytes (especially on Legacy TrueNAS)
        out = p1.communicate()[0].decode('utf8', 'ignore')
        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',
        '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 list(self.NAME_MAP.items()):
            if name.lower().find(key) != -1:
                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 list(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

        devname = devname.replace("/dev/", "")
        devname = re.sub(r'^(.*)p[0-9]+$', r'\1', devname)
        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
        )

        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_expiretime')
        if disk.exists():
            disk = disk[0]
            slot = disk.get_enc_slot()
            return slot
        return None


    def sync_disk_extra(self, disk, add=False):
        from freenasUI.storage.models import Disk
        if isinstance(disk, str):
            disk = Disk.objects.get(pk=disk)
        encs = ses.Enclosures()
        original = disk.disk_enclosure_slot
        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
        if disk.disk_enclosure_slot != original:
            disk.save()

    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.all()]
        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")
            """
            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_expiretime')
                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 set_global_alarm(self, value):
        cmd = "/usr/local/sbin/dmidecode -s baseboard-manufacturer"
        p1 = self._pipeopen(cmd, logger=None)
        motherboard = p1.communicate()[0].split('\n', 1)[0].strip()
        if motherboard == "GIGABYTE":
            try:
                with open('/dev/cuau1', 'w') as f:
                    if value == 'OK':
                        f.write('1')
                    else:
                        f.write('f')
            except IOError as e:
                log.debug('Failed to set global alarm: %s', e)
                return False
            finally:
                return True
        elif motherboard == "Celestica":
            try:
                enclosures = ["/dev/" + enc for enc in os.listdir("/dev") if enc.startswith("ses")]
                for enclosure in enclosures:
                    proc = subprocess.Popen([
                        '/usr/sbin/getencstat',
                        '-V', enclosure,
                    ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8')
                    encstat = proc.communicate()[0].strip()
                    if not re.search("Enclosure Name: CELESTIC (P3215-O|P3217-B)", encstat, re.M):
                        continue;
                    proc = subprocess.Popen([
                        '/usr/sbin/setobjstat', enclosure,
                        '0x3f', '0x80', '0', '0', ('0' if value == 'OK' else '0x02')
                    ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8')
                    proc.communicate()
            except IOError as e:
                log.debug('Failed to set global alarm: %s', e)
                return False
            finally:
                return True

    def get_chassis_hardware(self):
        """Returns what type of hardware this is
        Detected from dmidecode:

        TRUENAS-X10-HA-D
        TRUENAS-X10-S
        TRUENAS-X20-HA-D
        TRUENAS-X20-S
        TRUENAS-M40-HA
        TRUENAS-M40-S
        TRUENAS-M50-HA
        TRUENAS-M50-S
        TRUENAS-Z20-S
        TRUENAS-Z20-HA-D
        TRUENAS-Z30-HA-D
        TRUENAS-Z30-S
        TRUENAS-Z35-HA-D
        TRUENAS-Z35-S
        TRUENAS-Z50-HA-D
        TRUENAS-Z50-S

        Nothing in dmidecode but a M, X or Z class machine:
        (Note this is means production didn't burn the hardware model
        into SMBIOS. We can detect this case by looking at the
        motherboard)
        TRUENAS-M
        TRUENAS-X
        TRUENAS-Z

        Detected by the motherboard model:
        TRUENAS-SBB

        Pretty much anything else with a SM X8 board:
        (X8DTH was popular but there are a few other boards out there)
        TRUENAS-SM

        Really NFI about hardware at this point.  TrueNAS on a Dell?
        TRUENAS-UNKNOWN
        """

        chassis = self._pipeopen('/usr/local/sbin/dmidecode -s system-product-name',
                                 logger=None).communicate()[0].split('\n', 1)[0].strip()
        if chassis.startswith("TRUENAS-M") or chassis.startswith("TRUENAS-X") or chassis.startswith("TRUENAS-Z"):
            return chassis
        # We don't match a burned in name for a M, X or Z series.  Let's catch
        # the case where we are a M, X or Z. (shame on you production!)
        motherboard = self._pipeopen('/usr/local/sbin/dmidecode -s baseboard-manufacturer',
                                     logger=None).communicate()[0].split('\n', 1)[0].strip()
        motherboard_model = self._pipeopen('/usr/local/sbin/dmidecode -s baseboard-product-name',
                                           logger=None).communicate()[0].split('\n', 1)[0].strip()
        if motherboard_model == "X11DPi-NT" or motherboard_model == "X11SPi-TF":
            return "TRUENAS-M"
        if motherboard_model == "iXsystems TrueNAS X10":
            return "TRUENAS-X"
        if motherboard == "GIGABYTE":
            return "TRUENAS-Z"

        # Are we an SBB?  We can tell this because all SBBs used
        # the same motherboard: X8DTS
        if motherboard_model == "X8DTS":
            return "TRUENAS-SBB"

        # Most likely we are an X8DTH at this point, but there are some
        # unicorns that used various X8 boards, so we're going to make
        # allowances
        if motherboard_model.startswith("X8"):
            return "TRUENAS-SM"

        # Give up
        return "TRUENAS-UNKNOWN"

    def hardware_manifest(self):

        re_class = re.compile(r'class=(\S+)')
        re_card = re.compile(r'card=(\S+)')

        hba = {
            '9206-16E': 0,
            '9207-8E': 0,
        }

        network = {
            'Chelsio T520': 0,
        }

        proc = self._pipeopen('/usr/sbin/pciconf -l')
        for line in proc.communicate()[0].split():
            reg = re_class.search(line)
            if not reg:
                continue
            klass = reg.group(1)
            reg = re_card.search(line)
            if not reg:
                continue
            card = reg.group(1)

            if klass == '0x010700' and card == '0x30701000':
                hba['9206-16E'] += 1
            elif klass == '0x010700' and card == '0x30701000':
                hba['9207-8E'] += 1
            elif klass == '0x020000' and card == '0x00001425':
                network['Chelsio T520'] += 1

        output = [self.get_chassis_hardware()]
        for name, num in list((hba + network).items()):
            if num == 0:
                continue
            output.append('%d x %s' % (num, name))
        return ', '.join(output)
