# Copyright 2022 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 asyncio

import confluent.util as util
import confluent.messages as msg
import aiohmi.util.webclient as wc
import confluent.tasks as tasks
import time


def simplify_name(name):
    return name.lower().replace(' ', '_').replace('/', '-').replace('_-_', '-')

pdupool = tasks.TaskPool(128)

_pduclients = {}


class EnlogicClient(object):
    def __init__(self, pdu, configmanager):
        self.node = pdu
        self.configmanager = configmanager
        self._token = None
        self._wc = None
        self.username = None

    async def token(self):
        if not self._token:
            self._token = await self.login(self.configmanager)
        return self._token

    @property
    def wc(self):
        if self._wc:
            return self._wc
        targcfg = self.configmanager.get_node_attributes(
            self.node, ['hardwaremanagement.manager'], decrypt=True
        )
        targcfg = targcfg.get(self.node, {})
        target = targcfg.get('hardwaremanagement.manager', {}).get('value', None)
        if not target:
            target = self.node
        target = target.split('/', 1)[0]
        cv = util.TLSCertVerifier(
            self.configmanager, self.node, 'pubkeys.tls_hardwaremanager'
        ).verify_cert
        self._wc = wc.WebConnection(target, port=443, verifycallback=cv)
        return self._wc

    async def grab_json_response(self, url, body=None):
        rsp, status = await self.wc.grab_json_response_with_status(url, body)
        if status == 401:
            self._token = None
        if body and 'cookie' in body:
            body['cookie'] = await self.token()
        rsp, status = await self.wc.grab_json_response_with_status(url, body)
        if status < 300:
            return rsp
        return {}

    async def login(self, configmanager):
        credcfg = configmanager.get_node_attributes(
            self.node,
            ['secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'],
            decrypt=True,
        )
        credcfg = credcfg.get(self.node, {})
        username = credcfg.get('secret.hardwaremanagementuser', {}).get('value', None)
        passwd = credcfg.get('secret.hardwaremanagementpassword', {}).get('value', None)

        if not isinstance(username, str):
            username = username.decode('utf8')
        if not isinstance(passwd, str):
            passwd = passwd.decode('utf8')
        if not username or not passwd:
            raise Exception('Missing username or password')
        self.username = username
        rsp = await self.wc.grab_json_response(
            '/xhrlogin.jsp',
            {'username':  username, 'password': passwd, 'cookie': 0}
        )
        #print(repr(rsp))
        self.authtoken = rsp['cookie']
        self.wc.set_header('Authorization', self.authtoken)
        return self.authtoken

    async def logout(self):
        if self._token:
            await self.wc.grab_json_response(
                '/xhrlogout.jsp',
                {'timeout': 0, 'cookie': self._token},
            )
            self._token = None

    async def get_outlet(self, outlet):
        rsp = await self.grab_json_response(
            '/xhroutgetgrid.jsp', {
                'cookie': await self.token(),
                'pduid': 1
                })
        outlets = rsp['outlet']
        for olet in outlets:
            if olet['id'] == int(outlet):
                state = "on" if olet['powstat'] == 1 else "off"
                return state

    async def set_outlet(self, outlet, state):
        bitflags = 2**(int(outlet) - 1)
        outlet1 = bitflags & (2**24-1)
        outlet2 = bitflags >> 24
        if state == 'off':
            state = 0
        elif state == 'on':
            state = 1
        else:
            raise Exception("Unrecognized state " + repr(state))
        request = {
            'cookie': await self.token(),
            'outlet1': outlet1,
            'outlet2': outlet2,
            'pduid': 1,
            'powstat': state
            }
        await self.grab_json_response('/xhroutpowstatset.jsp', request)


_sensors_by_node = {}


async def read_sensors(element, node, configmanager):
    category, name = element[-2:]
    justnames = False
    msgs = []
    if len(element) == 3:
        # just get names
        category = name
        name = 'all'
        justnames = True
    if category in ('leds, fans', 'temperature'):
        return
    if justnames:
        msgs.append(msg.ChildCollection('total_energy'))
        msgs.append(msg.ChildCollection('total_apparent_power'))
        msgs.append(msg.ChildCollection('total_real_power'))
        return msgs
    sn = _sensors_by_node.get(node, None)
    if not sn or sn[1] < time.time():
        gc = get_client(node, configmanager)
        adev = await gc.grab_json_response('/energy_get', {'cookie': await gc.token(), 'end': 1, 'start': 1})
        _sensors_by_node[node] = (adev, time.time() + 1)
        sn = _sensors_by_node.get(node, None)
    if sn:
        sn = sn[0]
    readings = [
        {
            'name': 'Total Energy',
            'value': float(sn[0]['total_energy']) * 0.001,
            'units': 'kWh',
            'type': 'Energy',
        },
        {
            'name': 'Total Real Power',
            'value': float(sn[0]['active_power']),
            'units': 'W',
            'type': 'Power',
        },
        {
            'name': 'Total Apparent Power',
            'value': float(sn[0]['apparent_power']),
            'units': 'W',
            'type': 'Power',
        },
    ]
    return [msg.SensorReadings(readings, name=node)]

def get_client(node, configmanager):
    if node not in _pduclients:
        _pduclients[node] = EnlogicClient(node, configmanager)
    return _pduclients[node]

async def get_outlet(element, node, configmanager):
    gc = get_client(node, configmanager)
    state = await gc.get_outlet(element[-1])
    return msg.PowerState(node=node, state=state)


async def read_firmware(node, configmanager):
    gc = get_client(node, configmanager)
    adev = await gc.grab_json_response('/xhrgetuserlist.jsp')
    myversion = adev[0]['fwver']
    return msg.Firmware([{'PDU Firmware': {'version': myversion}}], node)


async def read_inventory(element, node, configmanager):
    _inventory = {}
    inventory = {}
    gc = get_client(node, configmanager)
    adev = await gc.grab_json_response('/sys_info_get', {
        'cookie': await gc.token(), 'pduid': 1
        })
    inventory['present'] = True
    inventory['name'] = 'PDU'
    info = {}
    info['Serial Number'] = adev['pdu'][0]['serial_number']
    info['Product Name'] = adev['pdu'][0]['model']
    info['Model'] = adev['pdu'][0]['part_number']
    inventory['information'] = info
    return msg.KeyValueData({'inventory': [inventory]}, node)

async def retrieve(nodes, element, configmanager, inputdata):

    if 'outlets' in element:
        gp = tasks.TaskPile(pdupool)
        for node in nodes:

            gp.spawn(get_outlet, element, node, configmanager)
        async for res in gp:
            yield res

        return
    elif element[0] == 'sensors':
        gp = tasks.TaskPile(pdupool)
        for node in nodes:
            gp.spawn(read_sensors, element, node, configmanager)
        async for rsp in gp:
            for msg in rsp:
                yield msg
        return
    elif '/'.join(element).startswith('inventory/firmware/all'):
        gp = tasks.TaskPile(pdupool)
        for node in nodes:
            gp.spawn(read_firmware, node, configmanager)
        async for rsp in gp:
            yield rsp

    elif '/'.join(element).startswith('inventory/hardware/all'):
        gp = tasks.TaskPile(pdupool)
        for node in nodes:
            gp.spawn(read_inventory, element, node, configmanager)
        async for rsp in gp:
            yield rsp
    else:
        for node in nodes:
            yield msg.ConfluentResourceUnavailable(node, 'Not implemented')
        return


async def update(nodes, element, configmanager, inputdata):
    if 'outlets' not in element:
        for node in nodes:
            yield msg.ConfluentResourceUnavailable(node, 'Not implemented')
        return
    for node in nodes:
        gc = get_client(node, configmanager)
        newstate = inputdata.powerstate(node)
        await gc.set_outlet(element[-1], newstate)
    await asyncio.sleep(1)
    async for res in retrieve(nodes, element, configmanager, inputdata):
        yield res
