# Copyright 2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import struct
import time

import aiohmi.constants as pygconst
import aiohmi.exceptions as pygexc

try:
    range = xrange
except NameError:
    pass
try:
    buffer
except NameError:
    buffer = memoryview


psucfg_errors = {
    0: 'Vendor mismatch',
    1: 'Revision mismatch',
    2: 'Processor missing',  # e.g. pluggable CPU VRMs...
    3: 'Insufficient power',
    4: 'Voltage mismatch',
}

firmware_progress = {
    0: 'Unspecified',
    1: 'Memory initialization',
    2: 'Disk initialization',
    3: 'Non-primary Processor initialization',
    4: 'User authentication',
    5: 'In setup',
    6: 'USB initialization',
    7: 'PCI initialization',
    8: 'Option ROM initialization',
    9: 'Video initialization',
    0xa: 'Cache initialization',
    0xb: 'SMBus initialization',
    0xc: 'Keyboard initialization',
    0xd: 'Embedded controller initialization',
    0xe: 'Docking station attachment',
    0xf: 'Docking station enabled',
    0x10: 'Docking station ejection',
    0x11: 'Docking station disabled',
    0x12: 'Waking OS',
    0x13: 'Starting OS boot',
    0x14: 'Baseboard initialization',
    0x16: 'Floppy initialization',
    0x17: 'Keyboard test',
    0x18: 'Pointing device test',
    0x19: 'Primary processor initialization',
}

firmware_errors = {
    0: 'Unspecified',
    1: 'No memory installed',
    2: 'All memory failed',
    3: 'Unrecoverable disk failure',
    4: 'Unrecoverable board failure',
    5: 'Unrecoverable diskette failure',
    6: 'Unrecoverable storage controller failure',
    7: 'Unrecoverable keyboard failure',  # Keyboard error, press
                                          # any key to continue..
    8: 'Removable boot media not found',
    9: 'Video adapter failure',
    0xa: 'No video device',
    0xb: 'Firmware corruption detected',
    0xc: 'CPU voltage mismatch',
    0xd: 'CPU speed mismatch',
}

auxlog_actions = {
    0: 'entry added',
    1: 'entry added (could not map to standard)',
    2: 'entry added with corresponding standard events',
    3: 'log cleared',
    4: 'log disabled',
    5: 'log enabled',
}

restart_causes = {
    0: 'Unknown',
    1: 'Remote request',
    2: 'Reset button',
    3: 'Power button',
    4: 'Watchdog',
    5: 'OEM',
    6: 'Power restored',
    7: 'Power restored',
    8: 'Reset due to event',
    9: 'Cycle due to event',
    0xa: 'OS reset',
    0xb: 'Timer wake',
}

slot_types = {
    0: 'PCI',
    1: 'Drive Array',
    2: 'External connector',
    3: 'Docking',
    4: 'Other',
    5: 'Entity ID',
    6: 'AdvancedTCA',
    7: 'Memory',
    8: 'Fan',
    9: 'PCIe',
    10: 'SCSI',
    11: 'SATA/SAS',
    12: 'USB',
}

power_states = {
    0: 'S0',
    1: 'S1',
    2: 'S2',
    3: 'S3',
    4: 'S4',
    5: 'S5',
    6: 'S4 or S5',
    7: 'G3',
    8: 'S1, S2, or S3',
    9: 'G1',
    0xa: 'S5',
    0xb: 'on',
    0xc: 'off',
}

watchdog_boot_phases = {
    1: 'Firmware',
    2: 'Firmware',
    3: 'OS Load',
    4: 'OS',
    5: 'OEM',
}

version_changes = {
    1: 'Device ID',
    2: 'Management controller firmware',
    3: 'Management controller revision',
    4: 'Management conroller manufacturer',
    5: 'IPMI version',
    6: 'Management controller firmware',
    7: 'Management controller boot block',
    8: 'Management controller firmware',
    9: 'System Firmware (UEFI/BIOS)',
    0xa: 'SMBIOS',
    0xb: 'OS',
    0xc: 'OS Loader',
    0xd: 'Diagnostics',
    0xe: 'Management agent',
    0xf: 'Management application',
    0x10: 'Management middleware',
    0x11: 'FPGA',
    0x12: 'FRU',
    0x13: 'FRU',
    0x14: 'Equivalent FRU',
    0x15: 'Updated FRU',
    0x16: 'Older FRU',
    0x17: 'Hardware (switch/jumper)',
}

fru_states = {
    0: 'Normal',
    1: 'Externally requested',
    2: 'Latch',
    3: 'Hot swap',
    4: 'Internal action',
    5: 'Lost communication',
    6: 'Lost communication',
    7: 'Unexpected removal',
    8: 'Operator',
    9: 'Unable to compute IPMB address',
    0xa: 'Unexpected deactivation',
}


def decode_eventdata(sensor_type, offset, eventdata, event_consts, sdr):
    """Decode extra event data from an alert or log

    Provide a textual summary of eventdata per descriptions in
    Table 42-3 of the specification.  This is for sensor specific
    offset events only.

    :param sensor_type: The sensor type number from the event
    :param offset:  Sensor specific offset
    :param eventdata: The three bytes from the log or alert
    :param event_consts: event definition including severity.
    :param sdr: The sdr locator entry to help clarify how to parse data
    """
    if sensor_type == 5 and offset == 4:  # link loss, indicates which port
        return 'Port {0}'.format(eventdata[1])
    elif sensor_type == 8 and offset == 6:  # PSU cfg error
        errtype = eventdata[2] & 0b1111
        return psucfg_errors.get(errtype, 'Unknown')
    elif sensor_type == 0xC6:
        return 'PSU Redundancy'
    elif sensor_type == 0xc and offset == 8:  # Memory spare
        return 'Module {0}'.format(eventdata[2])
    elif sensor_type == 0xf:
        if offset == 0:  # firmware error
            return firmware_errors.get(eventdata[1], 'Unknown')
        elif offset in (1, 2):
            return firmware_progress.get(eventdata[1], 'Unknown')
    elif sensor_type == 0x10:
        if offset == 0:  # Correctable error logging on a specific memory part
            return 'Module {0}'.format(eventdata[1])
        elif offset == 1:
            return 'Reading type {0:02X}h, offset {1:02X}h'.format(
                eventdata[1], eventdata[2] & 0b1111)
        elif offset == 5:
            return '{0}%'.format(eventdata[2])
        elif offset == 6:
            return 'Processor {0}'.format(eventdata[1])
    elif sensor_type == 0x12:
        if offset == 3:
            action = (eventdata[1] & 0b1111000) >> 4
            return auxlog_actions.get(action, 'Unknown')
        elif offset == 4:
            sysactions = []
            if eventdata[1] & 0b1 << 5:
                sysactions.append('NMI')
            if eventdata[1] & 0b1 << 4:
                sysactions.append('OEM action')
            if eventdata[1] & 0b1 << 3:
                sysactions.append('Power Cycle')
            if eventdata[1] & 0b1 << 2:
                sysactions.append('Reset')
            if eventdata[1] & 0b1 << 1:
                sysactions.append('Power Down')
            if eventdata[1] & 0b1:
                sysactions.append('Alert')
            return ','.join(sysactions)
        elif offset == 5:  # Clock change event, either before or after
            if eventdata[1] & 0b10000000:
                return 'After'
            else:
                return 'Before'
    elif sensor_type == 0x19 and offset == 0:
        return 'Requested {0} while {1}'.format(eventdata[1], eventdata[2])
    elif sensor_type == 0x1d and offset == 7:
        return restart_causes.get(eventdata[1], 'Unknown')
    elif sensor_type == 0x21:
        return '{0} {1}'.format(slot_types.get(eventdata[1], 'Unknown'),
                                eventdata[2])

    elif sensor_type == 0x23:
        phase = eventdata[1] & 0b1111
        return watchdog_boot_phases.get(phase, 'Unknown')
    elif sensor_type == 0x28:
        if offset == 4:
            return 'Sensor {0}'.format(eventdata[1])
        elif offset == 5:
            islogical = (eventdata[1] & 0b10000000)
            if islogical:
                if eventdata[2] in sdr.fru:
                    return sdr.fru[eventdata[2]].fru_name
                else:
                    return 'FRU {0}'.format(eventdata[2])
    elif sensor_type == 0x2a and offset == 3:
        return 'User {0}'.format(eventdata[1])
    elif sensor_type == 0x2b:
        return version_changes.get(eventdata[1], 'Unknown')
    elif sensor_type == 0x2c:
        cause = (eventdata[1] & 0b11110000) >> 4
        cause = fru_states.get(cause, 'Unknown')
        oldstate = eventdata[1] & 0b1111
        if oldstate != offset:
            try:
                cause += '(change from {0})'.format(
                    event_consts.sensor_type_offsets[0x2c][oldstate]['desc'])
            except KeyError:
                pass


async def _fix_sel_time(records, ipmicmd):
    timefetched = False
    rsp = None
    while not timefetched:
        try:
            rsp = await ipmicmd.raw_command(netfn=0xa, command=0x48)
            timefetched = True
        except pygexc.IpmiException as pi:
            if pi.ipmicode == 0x81:
                continue
            raise
    # The specification declares an epoch and all that, but we really don't
    # care.  We instead just focus on differences from the 'present'
    nowtime = struct.unpack_from('<I', rsp['data'])[0]
    correctednowtime = nowtime
    if nowtime < 0x20000000:
        correctearly = True
        inpreinit = True
    else:
        correctearly = False
        inpreinit = False
    newtimestamp = 0
    lasttimestamp = None
    trimindexes = []
    correctionenabled = True
    for index in reversed(range(len(records))):
        record = records[index]
        if 'timecode' not in record or record['timecode'] == 0xffffffff:
            continue
        if ('event' in record and record['event'] == 'Clock time change'
                and record['event_data'] == 'After'):
            if (lasttimestamp is not None
                    and record['timecode'] > lasttimestamp):
                # if the timestamp did something impossible, declare the rest
                # of history not meaningfully correctable
                correctionenabled = False
                newtimestamp = 0
                continue
            newtimestamp = record['timecode']
            trimindexes.append(index)
        elif ('event' in record and record['event'] == 'Clock time change'
              and record['event_data'] == 'Before'):
            if not correctionenabled:
                continue
            if newtimestamp:
                if record['timecode'] < 0x20000000:
                    correctearly = True
                    nowtime = correctednowtime
                # we want time that occurred before this point to get the delta
                # added to it to catch up
                correctednowtime += newtimestamp - record['timecode']
                newtimestamp = 0
            trimindexes.append(index)
        else:
            # clean up after potentially broken time sync pairs
            newtimestamp = 0
            if record['timecode'] < 0x20000000:  # uptime timestamp
                if not correctearly or not correctionenabled:
                    correctednowtime = nowtime
                    continue
                if (lasttimestamp is not None
                        and record['timecode'] > lasttimestamp):
                    # Time has gone backwards in pre-init, no hope for
                    # accurate time
                    correctearly = False
                    correctionenabled = False
                    correctednowtime = nowtime
                    continue
                inpreinit = True
                lasttimestamp = record['timecode']
                age = correctednowtime - record['timecode']
                record['timestamp'] = time.strftime(
                    '%Y-%m-%dT%H:%M:%S', time.localtime(time.time() - age))
            else:
                # We are in 'normal' time, assume we cannot go to
                # pre-init time and do corrections unless time sync events
                # guide us in safely
                if (lasttimestamp is not None
                        and record['timecode'] > lasttimestamp):
                    # Time has gone backwards, without a clock sync
                    # give up any attempt to correct from this point back...
                    correctionenabled = False
                if inpreinit:
                    inpreinit = False
                    # We were in pre-init, now in real time, reset the
                    # time correction factor to the last stored
                    # 'wall clock' correction
                    correctednowtime = nowtime
                correctearly = False
                lasttimestamp = record['timecode']
                if not correctionenabled or correctednowtime < 0x20000000:
                    # We can't correct time when the correction factor is
                    # rooted in a pre-init timestamp, just convert
                    record['timestamp'] = time.strftime(
                        '%Y-%m-%dT%H:%M:%S', time.localtime(
                            record['timecode']))
                else:
                    age = correctednowtime - record['timecode']
                    record['timestamp'] = time.strftime(
                        '%Y-%m-%dT%H:%M:%S', time.localtime(
                            time.time() - age))
    for index in trimindexes:
        del records[index]


class EventHandler(object):
    """IPMI Event Processor

    This class provides facilities for processing alerts and event log
    data.  This can be used to aid in pulling historical event data
    from a BMC or as part of a trap handler to translate the traps into
    manageable data.

    :param sdr: An SDR object (per aiohmi.ipmi.sdr) matching the target BMC SDR
    :param ipmicmd: An ipmi command object to fetch data live
    """
    @classmethod
    async def create(cls, sdr, ipmicmd):
        self = cls()
        self._sdr = sdr
        self._ipmicmd = ipmicmd
        self.event_consts = await ipmicmd.get_event_constants()
        return self

    def _populate_event(self, deassertion, event, event_data, event_type,
                        sensor_type, sensorid):
        event['component_id'] = sensorid
        try:
            event['component'] = self._sdr.sensors[sensorid].name
        except KeyError:
            if sensorid == 0:
                event['component'] = None
            else:
                event['component'] = 'Sensor {0}'.format(sensorid)
        event['deassertion'] = deassertion
        event['event_data_bytes'] = event_data
        byte2type = (event_data[0] & 0b11000000) >> 6
        byte3type = (event_data[0] & 0b110000) >> 4
        if byte2type == 1:
            event['triggered_value'] = event_data[1]
        evtoffset = event_data[0] & 0b1111
        event['event_type_byte'] = event_type
        if event_type <= 0xc:
            event['component_type_id'] = sensor_type
            event['event_id'] = '{0}.{1}'.format(event_type, evtoffset)
            # use generic offset decode for event description
            event['component_type'] = self.event_consts.sensor_type_codes.get(
                sensor_type, '')
            evreading = self.event_consts.generic_type_offsets.get(
                event_type, {}).get(evtoffset, {})
            if event['deassertion']:
                event['event'] = evreading.get('deassertion_desc', '')
                event['severity'] = evreading.get(
                    'deassertion_severity', pygconst.Health.Ok)
            else:
                event['event'] = evreading.get('desc', '')
                event['severity'] = evreading.get(
                    'severity', pygconst.Health.Ok)
        elif event_type == 0x6f:
            event['component_type_id'] = sensor_type
            event['event_id'] = '{0}.{1}'.format(event_type, evtoffset)
            event['component_type'] = self.event_consts.sensor_type_codes.get(
                sensor_type, '')
            evreading = self.event_consts.sensor_type_offsets.get(
                sensor_type, {}).get(evtoffset, {})
            if event['deassertion']:
                event['event'] = evreading.get('deassertion_desc', '')
                event['severity'] = evreading.get(
                    'deassertion_severity', pygconst.Health.Ok)
            else:
                event['event'] = evreading.get('desc', '')
                event['severity'] = evreading.get(
                    'severity', pygconst.Health.Ok)
        if event_type == 1:  # threshold
            if byte3type == 1:
                event['threshold_value'] = event_data[2]
        if 3 in (byte2type, byte3type) or event_type == 0x6f:
            # sensor specific decode, see sdr module...
            # 2 - 0xc: generic discrete, 0x6f, sensor specific
            additionaldata = decode_eventdata(
                sensor_type, evtoffset, event_data, self.event_consts,
                self._sdr)
            if additionaldata:
                event['event_data'] = additionaldata

    async def decode_pet(self, specifictrap, petdata):
        if isinstance(specifictrap, int):
            specifictrap = struct.unpack('4B', struct.pack('>I', specifictrap))
        if len(specifictrap) != 4:
            raise pygexc.InvalidParameterValue(
                'specifictrap should be integer number or 4 byte array')
        specifictrap = bytearray(specifictrap)
        sensor_type = specifictrap[1]
        event_type = specifictrap[2]
        # Event Offset is in first event data byte, so no need to fetch it here
        # evtoffset = specifictrap[3] & 0b1111
        deassertion = (specifictrap[3] & 0b10000000) == 0b10000000
        # alertseverity = petdata[26]
        sensorid = '{0}.0'.format(petdata[28])
        event_data = petdata[31:34]
        event = {}
        seqnum = struct.unpack_from('>H', buffer(petdata[16:18]))[0]
        ltimestamp = struct.unpack_from('>I', buffer(petdata[18:22]))[0]
        petack = bytearray(struct.pack('<HIBBBBBB', seqnum, ltimestamp,
                                       petdata[25], petdata[27], petdata[28],
                                       *event_data))
        try:
            await self._ipmicmd.raw_command(netfn=4, command=0x17, data=petack)
        except pygexc.IpmiException:  # Ignore failure to ack for now
            pass
        self._populate_event(deassertion, event, event_data, event_type,
                             sensor_type, sensorid)
        event['timecode'] = ltimestamp
        await _fix_sel_time((event,), self._ipmicmd)
        return event

    def _decode_standard_event(self, eventdata, event):
        # Ignore the generator id for now..
        if eventdata[2] not in (3, 4):
            raise pygexc.PyghmiException(
                'Unrecognized Event message version {0}'.format(eventdata[2]))
        sensor_type = eventdata[3]
        sensorid = '{0}.{1}.{2}'.format(
            eventdata[0], eventdata[4], eventdata[1] & 0b11)
        event_data = eventdata[6:]
        deassertion = (eventdata[5] & 0b10000000 == 0b10000000)
        event_type = eventdata[5] & 0b1111111
        self._populate_event(deassertion, event, event_data, event_type,
                             sensor_type, sensorid)

    async def _sel_decode(self, origselentry):
        selentry = bytearray(origselentry)
        event = {}
        event['record_id'] = struct.unpack_from('<H', origselentry[:2])[0]
        if selentry[2] < 0xe0 and len(selentry) >= 7:
            # Either standard, or at least the timestamp is standard
            event['timecode'] = struct.unpack_from('<I', buffer(selentry[3:7])
                                                   )[0]
        if selentry[2] < 0xc0:
            # ipmi defined standard format (0x02); like ipmitool, extend the
            # same treatment to reserved types some BMCs use (e.g. AMI 0x04)
            try:
                self._decode_standard_event(selentry[7:], event)
            except Exception:
                # a record body that does not actually follow the standard
                # layout; discard any partially decoded fields and pass it
                # through raw rather than aborting the fetch
                for key in list(event):
                    if key not in ('record_id', 'timecode'):
                        del event[key]
                event['oemdata'] = selentry[3:]
        elif 0xc0 <= selentry[2] <= 0xdf:
            event['oemid'] = selentry[7:10]
            event['oemdata'] = selentry[10:]
        elif selentry[2] == 0xf0:
            # De facto standard from the Linux kernel ipmi panic logger,
            # also recognized by ipmitool and freeipmi: byte 4 is a chunk
            # sequence number and bytes 5-15 carry a piece of the panic
            # string
            event['event'] = 'Linux kernel panic: {0}'.format(
                selentry[5:16].partition(b'\x00')[0].decode(
                    'utf-8', 'replace'))
            event['severity'] = pygconst.Health.Critical
            # this layout is defined by the kernel convention rather than
            # the BMC vendor, so bypass the OEM handler
            return event
        elif selentry[2] >= 0xe0:
            # In this class of OEM message, all bytes are OEM, interpretation
            # is wholly left up to the OEM layer, using the OEM ID of the BMC
            event['oemdata'] = selentry[3:]
        await self._ipmicmd._oem.process_event(event, self._ipmicmd, selentry)
        if 'event_type_byte' in event:
            del event['event_type_byte']
        if 'event_data_bytes' in event:
            del event['event_data_bytes']
        return event

    async def _fetch_entries(self, ipmicmd, startat, targetlist, rsvid=0):
        curr = startat
        endat = curr
        while curr != 0xffff:
            endat = curr
            reqdata = bytearray(struct.pack('<HHH', rsvid, curr, 0xff00))
            try:
                rsp = await ipmicmd.raw_command(
                    netfn=0xa, command=0x43, data=reqdata)
            except pygexc.IpmiException as pi:
                if pi.ipmicode == 203:
                    break
                else:
                    raise
            curr = struct.unpack_from('<H', buffer(rsp['data'][:2]))[0]
            targetlist.append(await self._sel_decode(rsp['data'][2:]))
        return endat

    async def fetch_sel(self, ipmicmd, clear=False):
        """Fetch SEL entries

        Return an iterable of SEL entries.  If clearing is requested,
    the fetch and clear will be done as an atomic operation, assuring
    no entries are dropped.

    :param ipmicmd: The Command object to use to interrogate
    :param clear: Whether to clear the entries upon retrieval.
    """
        records = []
        # First we do a fetch all without reservation, reducing the risk
        # of having a long lived reservation that gets canceled in the middle
        endat = await self._fetch_entries(ipmicmd, 0, records)
        if clear and records:  # don't bother clearing if there were no records
            # To do clear, we make a reservation first...
            rsp = await ipmicmd.raw_command(netfn=0xa, command=0x42)
            rsvid = struct.unpack_from('<H', rsp['data'])[0]
            # Then we refetch the tail with reservation (check for change)
            del records[-1]  # remove the record that's about to be duplicated
            await self._fetch_entries(ipmicmd, endat, records, rsvid)
            # finally clear the SEL
            # 0XAA means start initiate, 0x524c43 is 'RCL' or 'CLR' backwards
            clrdata = bytearray(struct.pack('<HI', rsvid, 0xAA524C43))
            await ipmicmd.raw_command(netfn=0xa, command=0x47, data=clrdata)
        # Now to fixup the record timestamps... first we need to get the BMC
        # opinion of current time
        await _fix_sel_time(records, ipmicmd)
        for rec in records:
            yield rec
