#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import re
import subprocess

from lck.lang import nullify
from django.conf import settings
from lck.django.common import nested_commit_on_success

from ralph.util import network, parse
from ralph.util import plugin, Eth
from ralph.discovery.models import (
    ComponentModel,
    ComponentType,
    Device,
    DeviceType,
    EthernetSpeed,
    IPAddress,
    Memory,
    Processor,
    SERIAL_BLACKLIST,
)


IPMI_SECTION_REGEX = re.compile(r'FRU Device Description : (?P<value>[^(]+) '
                                r'\(ID (?P<id>\d+)\)')
IPMI_OPTION_REGEX = re.compile(r' (?P<key>[^:]+) : (?P<value>.*)')
IPMI_USER = settings.IPMI_USER
IPMI_PASSWORD = settings.IPMI_PASSWORD

SAVE_PRIORITY = 55


class Error(Exception):
    pass


class IPMIToolError(Error):
    pass


class AnswerError(Error):
    pass


class AuthError(IPMIToolError):
    pass


def _clean(raw):
    if raw:
        stripped = raw
        for strip in " .:-_0":
            stripped = stripped.replace(strip, '')
        if stripped:
            return raw, True
    return raw, False


class IPMI(object):

    def __init__(self, host, user=IPMI_USER, password=IPMI_PASSWORD):
        self.host = host
        self.user = user
        self.password = password

    def tool(self, command, subcommand, param=None):
        command = ["ipmitool", "-H", self.host, "-U", self.user,
                   "-P", self.password, command, subcommand]
        if param:
            command.append(param)
        proc = subprocess.Popen(command, stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        out, err = proc.communicate()
        if proc.returncode and err:
            if err.startswith('Invalid user name'):
                raise AuthError('Invalid user name')
            else:
                raise IPMIToolError('Error calling ipmitool: %s' % err)
        return unicode(out, 'utf-8', 'replace')

    def get_fru(self):
        out = self.tool('fru', 'print')
        ipmi = parse.pairs(out)
        # remove (ID XX) from the top-level keys
        ipmi = dict((re.sub(r'\s*[(][^)]*[)]', '', k), v)
                    for (k, v) in ipmi.iteritems())
        return nullify(ipmi)

    def get_lan(self):
        out = self.tool('lan', 'print')
        return parse.pairs(out)

    def get_mc(self):
        out = self.tool('mc', 'info')
        return parse.pairs(out)

    def get_mac(self):
        data = self.get_lan()
        mac = data.get('MAC Address')
        return mac


def _get_ipmi_ethernets(data):
    # Ethernet
    index = 0
    while True:
        ethernet = data['MB/NET{}'.format(index)]
        if not ethernet:
            return
        mac = ethernet['Product Serial']
        label = " ".join((
            ethernet['Product Manufacturer'],
            ethernet['Product Name'])).title()
        if 'GIGABIT' in ethernet['Product Name']:
            speed = EthernetSpeed.s1gbit.id
        else:
            speed = 0
        yield Eth(label=label, mac=mac, speed=speed)
        index += 1


def _add_ipmi_components(device, data):
    # CPUs
    cpu_index = 0
    total_mem_index = 0
    while True:
        cpu = data['MB/P{}'.format(cpu_index)]
        if not cpu:
            break
        if not cpu['Product Name']:
            cpu_index += 1
            continue
        proc, _ = Processor.concurrent_get_or_create(
            index=cpu_index + 1, device=device)
        proc.label = re.sub(' +', ' ', cpu['Product Name']).title()
        speed_match = re.search(r'(\d+\.\d+)GHZ', cpu['Product Name'])
        if speed_match:
            speed = int(float(speed_match.group(1)) * 1000)
        else:
            speed = 0
        cores = 0
        proc.model, c = ComponentModel.create(
            ComponentType.processor,
            family=proc.label,
            speed=speed,
            cores=cores,
            name='CPU %s %dMHz %d-core' % (
                proc.label,
                speed,
                cores,
            ),
            priority=0,   # FIXME: why 0?
        )
        proc.save()
        # Memory
        mem_index = 0
        while True:
            memory = data['MB/P{}/D{}'.format(cpu_index, mem_index)]
            if not memory:
                break
            if not memory['Product Name']:
                mem_index += 1
                total_mem_index += 1
                continue
            size_match = re.search(r'(\d+)GB', memory['Product Name'])
            if not size_match:
                mem_index += 1
                total_mem_index += 1
                continue
            mem, _ = Memory.concurrent_get_or_create(
                index=total_mem_index + 1, device=device)
            mem.label = memory['Product Name']
            size = int(size_match.group(1)) * 1024
            mem.model, c = ComponentModel.create(
                ComponentType.memory,
                size=size,
                priority=0,  # FIXME: why 0?
            )
            mem.save()
            mem_index += 1
            total_mem_index += 1
        cpu_index += 1


def _get_base_device_info(fru_part):
    name, name_clean = _clean(fru_part['Product Name'])
    sn, sn_clean = _clean(fru_part['Product Serial'])
    if sn in SERIAL_BLACKLIST:
        sn = None
    model_type = DeviceType.rack_server
    if name.lower().startswith('ipmi'):
        model_type = DeviceType.unknown
    return name, sn, model_type


@nested_commit_on_success
def _run_ipmi(ip):
    try:
        ipmi = IPMI(ip)
        fru = ipmi.get_fru()
    except AuthError:
        try:
            ipmi = IPMI(ip, 'ADMIN')
            fru = ipmi.get_fru()
        except AuthError:
            ipmi = IPMI(ip, 'ADMIN', 'ADMIN')
            fru = ipmi.get_fru()
    mc = ipmi.get_mc()
    top = fru['/SYS']
    if not top:
        top = fru['Builtin FRU Device']
    if not top:
        raise AnswerError('Incompatible answer.')
    name, sn, model_type = _get_base_device_info(top)
    mac = ipmi.get_mac()
    if mac:
        ethernets = [Eth(label='IPMI MAC', mac=mac, speed=0)]
    else:
        ethernets = []
    ethernets.extend(_get_ipmi_ethernets(fru))
    dev = Device.create(ethernets=ethernets, priority=SAVE_PRIORITY,
                        sn=sn, model_name=name.title(), model_type=model_type)
    firmware = mc.get('Firmware Revision')
    if firmware:
        dev.mgmt_firmware = 'rev %s' % firmware
    _add_ipmi_components(dev, fru)
    dev.save(update_last_seen=True, priority=SAVE_PRIORITY)
    ip_address, created = IPAddress.concurrent_get_or_create(address=str(ip))
    ip_address.device = dev
    ip_address.is_management = True
    if created:
        ip_address.hostname = network.hostname(ip_address.address)
        ip_address.snmp_name = name
    ip_address.save(update_last_seen=True)
    return name


@plugin.register(chain='discovery', requires=['ping', 'http'])
def ipmi(**kwargs):
    if not IPMI_USER or not IPMI_PASSWORD:
        return False, "not configured", kwargs
    ip = str(kwargs['ip'])
    http_family = kwargs.get('http_family')
    if http_family not in ('Sun', 'Thomas-Krenn', 'Oracle-ILOM-Web-Server',
                           'IBM System X'):
        return False, 'no match.', kwargs
    try:
        name = _run_ipmi(ip)
    except (AnswerError, IPMIToolError) as e:
        return False, str(e), kwargs

    return True, name, kwargs


def ipmi_power_on(host, user=IPMI_USER, password=IPMI_PASSWORD):
    ipmi = IPMI(host, user, password)
    response = ipmi.tool('chassis', 'power', 'on')
    return response.strip().lower().endswith('on')


def ipmi_reboot(host, user=IPMI_USER, password=IPMI_PASSWORD,
                power_on_if_disabled=False):
    ipmi = IPMI(host, user, password)

    response = ipmi.tool('chassis', 'power', 'status')
    if response.strip().lower().endswith('on'):
        response = ipmi.tool('chassis', 'power', 'reset')
        if response.strip().lower().endswith('reset'):
            return True
    elif power_on_if_disabled:
        return ipmi_power_on(host, user, password)
    return False
