#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# __author__ = 'maximus'

import ruamel.yaml as yaml3ed
from collections import OrderedDict
import os
import logging
from zabbix import ZabbixAgent
from mapping import Node, Link, Map, Table, Palette, Singleton
from PIL import Image
import base64
import random
from io import BytesIO

log = logging.getLogger(__name__)


class ConfigException(Exception):
    def __init__(self, message):
        self.message = message

        # def __str__(self):
        #     return str(self.message).format(self.error)


class ConfigTemplate(metaclass=Singleton):  # noqa
    """ This is config template. DO NOT MODIFY THIS OBJECT."""

    def __init__(self):
        self.template = {'zabbix': {'url': str(), 'login': str(), 'password': str()},
                         'map': {'name': str(), 'bgcolor': str(), 'fontsize': 10, 'width': int(), 'height': int()},
                         'table': {'show': False, 'x': int(), 'y': int()},
                         'link': {'width': 10, 'bandwidth': 100},
                         'palette': Palette().palette,
                         'node-': {'name': str(), 'label': str(), 'icon': str(), 'x': int(), 'y': int()},
                         'link-': {'node1': str(), 'node2': str(), 'name1': str(), 'name2': str(), 'copy': 0,
                                   'hostname': str(), 'itemin': str(), 'itemout': str(), 'width': int(),
                                   'bandwidth': int()}
                         }
        log.debug('Object singleton ConfigTemplate created')


class ConfigLoader(object):
    def __init__(self, path_cfg: str):

        self.template = ConfigTemplate().template
        self.cfg_dict = {}
        self.obj_nodes = {}
        self.obj_links = {}
        self.zbx = None
        self.load(path_cfg)
        log.debug('Object ConfigLoader created')

    def load(self, path_cfg: str):
        with open(path_cfg, 'r') as stream:
            try:
                self.cfg_dict = yaml3ed.safe_load(stream)
            except yaml3ed.YAMLError as exc:
                print(exc)
        self.check()
        self.zbx = ZabbixAgent(self.cfg_dict['zabbix']['url'], self.cfg_dict['zabbix']['login'],
                               self.cfg_dict['zabbix']['password'])
        log.debug('Config loaded')

    def check(self):
        for cfg_sect in self.template:
            if cfg_sect == 'node-':
                for node in sorted([node for node in self.cfg_dict.keys() if cfg_sect in node]):
                    for cfg_opt in self.template[cfg_sect]:
                        try:
                            self.cfg_dict[node][cfg_opt]
                        except KeyError:
                            if cfg_opt == 'icon':
                                # self.cfg_dict[node][cfg_opt] = str()
                                continue
                            if cfg_opt == 'label':
                                # self.cfg_dict[node][cfg_opt] = str()
                                continue
                            raise ConfigException('The option: {0} is missing in section: [{1}]'
                                                  .format(cfg_sect, cfg_opt))

            if cfg_sect == 'link-':
                for link in sorted([link for link in self.cfg_dict.keys() if cfg_sect in link]):
                    for cfg_opt in self.template[cfg_sect]:
                        try:
                            self.cfg_dict[link][cfg_opt]
                        except KeyError:
                            if cfg_opt == 'copy':
                                # self.cfg_dict[link][cfg_opt] = False
                                continue
                            if cfg_opt == 'width':
                                # self.cfg_dict[link][cfg_opt] = int()
                                continue
                            if cfg_opt == 'bandwidth':
                                # self.cfg_dict[link][cfg_opt] = int()
                                continue
                            raise ConfigException('The option: {0} is missing in section: [{1}]'
                                                  .format(cfg_sect, cfg_opt))

            if cfg_sect == 'palette':
                if len(self.cfg_dict[cfg_sect]) != 9:
                    raise ConfigException('Error in section {0}, number elements not equal 9'.format(cfg_sect))
                continue

            for cfg_opt in self.template[cfg_sect]:
                try:
                    self.cfg_dict[cfg_sect][cfg_opt]
                except KeyError:
                    # if cfg_sect == 'map' and cfg_opt == 'bgcolor':
                    # self.cfg_dict[cfg_sect][cfg_opt] = str()
                    if cfg_sect == 'link-' or cfg_sect == 'node-':
                        continue
                    raise ConfigException('The option: {0} is missing in section: [{1}]'.format(cfg_sect, cfg_opt))
        log.debug('Config check: Ok')

    def create_map(self, font_path_fn: str, icon_path: str):
        self.obj_nodes = {section: None for section in self.cfg_dict if 'node-' in section}
        self.obj_links = {section: None for section in self.cfg_dict if 'link-' in section}
        palette = self.cfg_dict['palette']
        fontsize = int(self.cfg_dict['map']['fontsize'])

        for node in self.obj_nodes.keys():
            x = int(self.cfg_dict[node]['x'])
            y = int(self.cfg_dict[node]['y'])

            if self.cfg_dict[node].get('label'):
                label = self.cfg_dict[node]['label']
            else:
                label = None

            if self.cfg_dict[node].get('icon'):
                icon = self.cfg_dict[node]['icon']
            else:
                icon = None

            self.obj_nodes[node] = (Node(font_path_fn, icon_path, x=x, y=y, label=label, icon=icon, fontsize=fontsize))

        for link in self.obj_links.keys():
            node1 = self.obj_nodes[self.cfg_dict[link]['node1']]
            node2 = self.obj_nodes[self.cfg_dict[link]['node2']]

            if self.cfg_dict[link].get('bandwidth'):
                bandwidth = self.cfg_dict[link]['bandwidth']
            else:
                bandwidth = self.cfg_dict['link']['bandwidth']

            if self.cfg_dict[link].get('width'):
                width = self.cfg_dict[link]['width']
            else:
                width = self.cfg_dict['link']['width']

            self.obj_links[link] = (Link(font_path_fn, node1, node2, bandwidth=bandwidth, width=width,
                                         palette=palette, fontsize=fontsize))

            hostname = self.cfg_dict[link]['hostname']
            item_in = self.cfg_dict[link]['itemin']
            item_out = self.cfg_dict[link]['itemout']

            if hostname and item_in and item_out:
                data_in, data_out = self.zbx.get_item_data2(hostname, item_in, item_out)
                self.obj_links[link].data(in_bps=data_in, out_bps=data_out)
            elif hostname and item_in:
                data_in = self.zbx.get_item_data(hostname, item_in)
                self.obj_links[link].data(in_bps=data_in, out_bps=0)
            elif hostname and item_out:
                data_out = self.zbx.get_item_data(hostname, item_out)
                self.obj_links[link].data(in_bps=0, out_bps=data_out)
            else:
                self.obj_links[link].data(in_bps=0, out_bps=0)

        if int(self.cfg_dict['table']['show']):
            table = Table(font_path_fn, x=int(self.cfg_dict['table']['x']), y=int(self.cfg_dict['table']['y']),
                          palette=palette)
        else:
            table = None

        map_width = int(self.cfg_dict['map']['width'])
        map_height = int(self.cfg_dict['map']['height'])
        if self.cfg_dict['map']['bgcolor']:
            map_bgcolor = self.cfg_dict['map']['bgcolor']
        else:
            map_bgcolor = None
        map_obj = Map(self.obj_links.values(), self.obj_nodes.values(), table=table, len_x=map_width,
                      len_y=map_height, bgcolor=map_bgcolor)
        return map_obj

    def upload(self, img_path_fn: str):
        self.zbx.image_to_zabbix(img_path_fn, self.cfg_dict['map']['name'])


class ConfigCreate(object):
    def __init__(self, map_data: dict, zbx_agent: ZabbixAgent):
        self.zbx = zbx_agent
        self.map_data = map_data
        self.template = ConfigTemplate().template
        self.map_config = {}
        self.dict_call = [self.zbx.get_hostname, self.zbx.get_mapname,
                          self.zbx.get_triggername, self.zbx.get_hostgroupname,
                          self.zbx.get_imagename]
        self.cfg_loader_obj = None
        self.setup_yaml()
        log.debug('Object ConfigCreate created')

    @staticmethod
    def setup_yaml():
        """ StackOverflow Driven Development
        https://stackoverflow.com/a/31609484/4709370
        http://stackoverflow.com/a/8661021 """

        def represent_dict_order(yaml_self, data):
            return yaml_self.represent_mapping('tag:yaml.org,2002:map', data.items())

        yaml3ed.add_representer(OrderedDict, represent_dict_order)

    @staticmethod
    def random_label():
        return ''.join(random.SystemRandom().choice('abcdefgijklmnoprstuvwxyz1234567890') for _ in range(8))

    def create(self):
        elemid_dict = {}
        self.map_config['zabbix'] = {'url': self.zbx.url,
                                     'login': self.zbx.login,
                                     'password': self.zbx.password
                                     }
        self.map_config['map'] = {'name': self.map_data['name'],
                                  'bgcolor': self.template['map']['bgcolor'],
                                  'fontsize': self.template['map']['fontsize'],
                                  'width': int(self.map_data['width']),
                                  'height': int(self.map_data['height'])
                                  }
        self.map_config['table'] = {'show': self.template['table']['show'],
                                    'x': int(self.map_data['width']) - 100,
                                    'y': int(self.map_data['height']) - 300
                                    }

        self.map_config['palette'] = self.template['palette']
        self.map_config['link'] = self.template['link']

        for node in self.map_data['selements']:
            nodeid = node['selementid']

            if int(node['elementtype']) == 4:
                nodename = self.dict_call[int(node['elementtype'])](node['iconid_off'])
            elif int(node['elementtype']) == 3:
                nodename = self.dict_call[int(node['elementtype'])](node['elements'][0]['groupid'])
            elif int(node['elementtype']) == 2:
                nodename = self.dict_call[int(node['elementtype'])](node['elements'][0]['triggerid'])
            elif int(node['elementtype']) == 1:
                nodename = self.dict_call[int(node['elementtype'])](node['elements'][0]['sysmapid'])
            elif int(node['elementtype']) == 0:
                nodename = self.dict_call[int(node['elementtype'])](node['elements'][0]['hostid'])

            elemid_dict[node['selementid']] = nodename

            self.map_config['node-' + nodeid] = {'name': nodename, 'label': str(), 'icon': str()}

            image_b64code = self.zbx.image_get(node['iconid_off'])
            im = Image.open(BytesIO(base64.b64decode(image_b64code)))
            width, height = im.size
            self.map_config['node-' + nodeid] = {
                'name': nodename,
                'x': int(node['x']) + int(width // 2),
                'y': int(node['y']) + int(height // 2)
            }

        for link in self.map_data['links']:
            self.map_config['link-' + link['linkid']] = {'node1': 'node-' + link['selementid2'],
                                                         'node2': 'node-' + link['selementid1'],
                                                         'name1': elemid_dict[link['selementid2']],
                                                         'name2': elemid_dict[link['selementid1']],
                                                         'hostname': str(),
                                                         'itemin': str(),
                                                         'itemout': str()
                                                         }
        del elemid_dict

    @staticmethod
    def _dict_to_orderdict(cfg: dict) -> OrderedDict:
        cfg_order = OrderedDict()
        cfg_templ = OrderedDict([('map', ('name', 'bgcolor', 'fontsize', 'width', 'height')),
                                 ('zabbix', ('url', 'login', 'password')),
                                 ('table', ('show', 'x', 'y')),
                                 ('palette', None),
                                 ('link', ('bandwidth', 'width')),
                                 ('node-', ('name', 'label', 'icon', 'x', 'y')),
                                 ('link-', ('node1', 'node2', 'name1', 'name2', 'copy', 'width', 'bandwidth',
                                            'hostname', 'itemin', 'itemout'))
                                 ])

        for cfg_sect in cfg_templ:

            if cfg_sect == 'node-':
                for node in sorted([node for node in cfg.keys() if cfg_sect in node]):
                    cfg_order[node] = OrderedDict()
                    for cfg_opt in cfg_templ[cfg_sect]:
                        if cfg_opt == 'icon' and cfg_opt not in cfg[node]:
                            continue
                        if cfg_opt == 'label' and cfg_opt not in cfg[node]:
                            continue
                        cfg_order[node][cfg_opt] = cfg[node][cfg_opt]
                continue

            if cfg_sect == 'link-':
                for link in sorted([link for link in cfg.keys() if cfg_sect in link]):
                    cfg_order[link] = OrderedDict()
                    for cfg_opt in cfg_templ[cfg_sect]:
                        if cfg_opt == 'copy' and cfg_opt not in cfg[link]:
                            continue
                        if cfg_opt == 'width' and cfg_opt not in cfg[link]:
                            continue
                        if cfg_opt == 'bandwidth' and cfg_opt not in cfg[link]:
                            continue
                        cfg_order[link][cfg_opt] = cfg[link][cfg_opt]
                continue

            cfg_order[cfg_sect] = OrderedDict()
            if cfg_sect == 'palette':
                cfg_order[cfg_sect] = cfg[cfg_sect]
                continue

            for cfg_opt in cfg_templ[cfg_sect]:
                if cfg_sect == 'map' and cfg_opt == 'bgcolor' and cfg_opt not in cfg[cfg_sect]:
                    continue
                cfg_order[cfg_sect][cfg_opt] = cfg[cfg_sect][cfg_opt]
        return cfg_order

    def save(self, path: str):
        cfg = self._dict_to_orderdict(self.map_config)
        with open(path + '/' + self.map_data['name'] + '.yaml', 'w') as cfg_file:
            try:
                yaml3ed.dump(cfg, cfg_file, explicit_start=True, explicit_end=True,
                             default_flow_style=False, allow_unicode=True, version=(1, 2))
            except yaml3ed.YAMLError as exc:
                print(exc)

    def check_map(self, old_cfg_path: str):
        old_cfg_path_fn = old_cfg_path + '/' + self.map_data['name'] + '.yaml'
        exist = os.path.exists(old_cfg_path_fn)
        if exist:
            self._compare(old_cfg_path_fn)

    def _compare(self, old_cfg_path_file: str):

        self.cfg_loader_obj = ConfigLoader(old_cfg_path_file)
        config_old = self.cfg_loader_obj.cfg_dict

        for section in [sect for sect in self.template.keys()
                        if sect != 'zabbix' and sect != 'map' and sect != 'node-' and sect != 'link-']:

            if section == 'palette':
                self.map_config[section] = config_old[section]
                continue

            for option in self.template[section]:
                self.map_config[section][option] = config_old[section][option]

        for section in self.map_config:
            if 'node-' in section:
                if config_old.get(section):
                    if config_old[section].get('label'):
                        self.map_config[section]['label'] = config_old[section]['label']
                    if config_old[section].get('icon'):
                        self.map_config[section]['icon'] = config_old[section]['icon']
            if 'link-' in section:
                if config_old.get(section):
                    self.map_config[section]['hostname'] = config_old[section]['hostname']
                    self.map_config[section]['itemin'] = config_old[section]['itemin']
                    self.map_config[section]['itemout'] = config_old[section]['itemout']
                    if config_old[section].get('width'):
                        self.map_config[section]['width'] = config_old[section]['width']
                    if config_old[section].get('bandwidth'):
                        self.map_config[section]['bandwidth'] = config_old[section]['bandwidth']

        for section in [link for link in config_old.keys() if 'link-' in link]:

            if config_old[section].get('copy'):
                new_section = 'link-' + self.random_label()
                node1_sect = config_old[section]['node1']
                node2_sect = config_old[section]['node2']
                node1_new_sect = 'node-' + self.random_label()
                node2_new_sect = 'node-' + self.random_label()

                self.map_config.update({new_section: dict()})
                for option in config_old[section]:
                    if 'node1' in option:
                        self.map_config[new_section][option] = node1_new_sect
                    elif 'node2' in option:
                        self.map_config[new_section][option] = node2_new_sect
                    else:
                        self.map_config[new_section][option] = config_old[section][option]

                self.map_config.update({node1_new_sect: dict()})
                self.map_config.update({node2_new_sect: dict()})
                for option in config_old[node1_sect]:
                    self.map_config[node1_new_sect][option] = config_old[node1_sect][option]
                for option in config_old[node2_sect]:
                    self.map_config[node2_new_sect][option] = config_old[node2_sect][option]