# -*- coding: utf-8 -*-
#
# Copyright (C) 2015-2015: Alignak team, see AUTHORS.txt file for contributors
#
# This file is part of Alignak.
#
# Alignak is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Alignak is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Alignak.  If not, see <http://www.gnu.org/licenses/>.
#
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
#  Copyright (C) 2009-2014:
#     Hartmut Goebel, h.goebel@goebel-consult.de
#     Nicolas Dupeux, nicolas@dupeux.net
#     Gerhard Lausser, gerhard.lausser@consol.de
#     Grégory Starck, g.starck@gmail.com
#     Frédéric Pégé, frederic.pege@gmail.com
#     Sebastien Coavoux, s.coavoux@free.fr
#     Olivier Hanesse, olivier.hanesse@gmail.com
#     Jean Gabes, naparuba@gmail.com
#     Zoran Zaric, zz@zoranzaric.de
#     David Gil, david.gil.marcos@gmail.com

#  This file is part of Shinken.
#
#  Shinken is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Shinken is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with Shinken.  If not, see <http://www.gnu.org/licenses/>.

"""
This class resolve Macro in commands by looking at the macros list
in Class of elements. It give a property that call be callable or not.
It not callable, it's a simple property and replace the macro with the value
If callable, it's a method that is called to get the value. for example, to
get the number of service in a host, you call a method to get the
len(host.services)
"""

import re
import time

from alignak.borg import Borg


class MacroResolver(Borg):
    """MacroResolver class is used to resolve macros (in command call). See above for details"""

    my_type = 'macroresolver'
    # Global macros
    macros = {
        'TOTALHOSTSUP':         '_get_total_hosts_up',
        'TOTALHOSTSDOWN':       '_get_total_hosts_down',
        'TOTALHOSTSUNREACHABLE': '_get_total_hosts_unreachable',
        'TOTALHOSTSDOWNUNHANDLED': '_get_total_hosts_unhandled',
        'TOTALHOSTSUNREACHABLEUNHANDLED': '_get_total_hosts_unreachable_unhandled',
        'TOTALHOSTPROBLEMS':    '_get_total_host_problems',
        'TOTALHOSTPROBLEMSUNHANDLED': '_get_total_host_problems_unhandled',
        'TOTALSERVICESOK':      '_get_total_service_ok',
        'TOTALSERVICESWARNING': '_get_total_services_warning',
        'TOTALSERVICESCRITICAL': '_get_total_services_critical',
        'TOTALSERVICESUNKNOWN': '_get_total_services_unknown',
        'TOTALSERVICESWARNINGUNHANDLED': '_get_total_services_warning_unhandled',
        'TOTALSERVICESCRITICALUNHANDLED': '_get_total_services_critical_unhandled',
        'TOTALSERVICESUNKNOWNUNHANDLED': '_get_total_services_unknown_unhandled',
        'TOTALSERVICEPROBLEMS': '_get_total_service_problems',
        'TOTALSERVICEPROBLEMSUNHANDLED': '_get_total_service_problems_unhandled',
        'LONGDATETIME':         '_get_long_date_time',
        'SHORTDATETIME':        '_get_short_date_time',
        'DATE':                 '_get_date',
        'TIME':                 '_get_time',
        'TIMET':                '_get_timet',
        'PROCESSSTARTTIME':     '_get_process_start_time',
        'EVENTSTARTTIME':       '_get_events_start_time',
    }

    output_macros = [
        'HOSTOUTPUT',
        'HOSTPERFDATA',
        'HOSTACKAUTHOR',
        'HOSTACKCOMMENT',
        'SERVICEOUTPUT',
        'SERVICEPERFDATA',
        'SERVICEACKAUTHOR',
        'SERVICEACKCOMMENT'
    ]

    def init(self, conf):
        """Init macroresolver instance with conf.
        Must be called once.

        :param conf: conf to load
        :type conf:
        :return: None
        """

        # For searching class and elements for ondemand
        # we need link to types
        self.conf = conf
        self.lists_on_demand = []
        self.hosts = conf.hosts
        # For special void host_name handling...
        self.host_class = self.hosts.inner_class
        self.lists_on_demand.append(self.hosts)
        self.services = conf.services
        self.contacts = conf.contacts
        self.lists_on_demand.append(self.contacts)
        self.hostgroups = conf.hostgroups
        self.lists_on_demand.append(self.hostgroups)
        self.commands = conf.commands
        self.servicegroups = conf.servicegroups
        self.lists_on_demand.append(self.servicegroups)
        self.contactgroups = conf.contactgroups
        self.lists_on_demand.append(self.contactgroups)
        self.illegal_macro_output_chars = conf.illegal_macro_output_chars

        # Try cache :)
        # self.cache = {}

    def _get_macros(self, chain):
        """Get all macros of a chain
        Cut '$' char and create a dict with the following structure::

        { 'MacroSTR1' : {'val': '', 'type': 'unknown'}
          'MacroSTR2' : {'val': '', 'type': 'unknown'}
        }

        :param chain: chain to parse
        :type chain: str
        :return: dict with macro parsed as key
        :rtype: dict
        """
        # if chain in self.cache:
        #    return self.cache[chain]

        regex = re.compile(r'(\$)')
        elts = regex.split(chain)
        macros = {}
        in_macro = False
        for elt in elts:
            if elt == '$':
                in_macro = not in_macro
            elif in_macro:
                macros[elt] = {'val': '', 'type': 'unknown'}

        # self.cache[chain] = macros
        if '' in macros:
            del macros['']
        return macros

    def _get_value_from_element(self, elt, prop):
        """Get value from a element's property
        the property may be a function to call.

        :param elt: element
        :type elt: object
        :param prop: element property
        :type prop: str
        :return: getattr(elt, prop) or getattr(elt, prop)() (call)
        :rtype: str
        """
        try:
            value = getattr(elt, prop)
            if callable(value):
                return unicode(value())
            else:
                return unicode(value)
        except AttributeError, exp:
            # Return no value
            return ''
        except UnicodeError, exp:
            if isinstance(value, str):
                return unicode(value, 'utf8', errors='ignore')
            else:
                return ''

    def _delete_unwanted_caracters(self, chain):
        """Remove not wanted char from chain
        unwanted char are illegal_macro_output_chars attribute

        :param chain: chain to remove char from
        :type chain: str
        :return: chain cleaned
        :rtype: str
        """
        for char in self.illegal_macro_output_chars:
            chain = chain.replace(char, '')
        return chain

    def get_env_macros(self, data):
        """Get all environment macros from data
        For each object in data ::

        * Fetch all macros in object.__class__.macros
        * Fetch all customs macros in o.custom

        :param data: data to get macro
        :type data:
        :return: dict with macro name as key and macro value as value
        :rtype: dict
        """
        env = {}

        for obj in data:
            cls = obj.__class__
            macros = cls.macros
            for macro in macros:
                if macro.startswith("USER"):
                    break

                prop = macros[macro]
                value = self._get_value_from_element(obj, prop)
                env['NAGIOS_%s' % macro] = value
            if hasattr(obj, 'customs'):
                # make NAGIOS__HOSTMACADDR from _MACADDR
                for cmacro in obj.customs:
                    new_env_name = 'NAGIOS__' + obj.__class__.__name__.upper() + cmacro[1:].upper()
                    env[new_env_name] = obj.customs[cmacro]

        return env

    def resolve_simple_macros_in_string(self, c_line, data, args=None):
        """Replace macro in the command line with the real value

        :param c_line: command line to modify
        :type c_line: str
        :param data: objects list, use to look for a specific macro
        :type data:
        :param args: args given to the command line, used to get "ARGN" macros.
        :type args:
        :return: command line with '$MACRO$' replaced with values
        :rtype: str
        """
        # Now we prepare the classes for looking at the class.macros
        data.append(self)  # For getting global MACROS
        if hasattr(self, 'conf'):
            data.append(self.conf)  # For USERN macros
        clss = [d.__class__ for d in data]

        # we should do some loops for nested macros
        # like $USER1$ hiding like a ninja in a $ARG2$ Macro. And if
        # $USER1$ is pointing to $USER34$ etc etc, we should loop
        # until we reach the bottom. So the last loop is when we do
        # not still have macros :)
        still_got_macros = True
        nb_loop = 0
        while still_got_macros:
            nb_loop += 1
            # Ok, we want the macros in the command line
            macros = self._get_macros(c_line)

            # We can get out if we do not have macros this loop
            still_got_macros = (len(macros) != 0)
            # print "Still go macros:", still_got_macros

            # Put in the macros the type of macro for all macros
            self._get_type_of_macro(macros, clss)
            # Now we get values from elements
            for macro in macros:
                # If type ARGN, look at ARGN cutting
                if macros[macro]['type'] == 'ARGN' and args is not None:
                    macros[macro]['val'] = self._resolve_argn(macro, args)
                    macros[macro]['type'] = 'resolved'
                # If class, get value from properties
                if macros[macro]['type'] == 'class':
                    cls = macros[macro]['class']
                    for elt in data:
                        if elt is not None and elt.__class__ == cls:
                            prop = cls.macros[macro]
                            macros[macro]['val'] = self._get_value_from_element(elt, prop)
                            # Now check if we do not have a 'output' macro. If so, we must
                            # delete all special characters that can be dangerous
                            if macro in self.output_macros:
                                macros[macro]['val'] = \
                                    self._delete_unwanted_caracters(macros[macro]['val'])
                if macros[macro]['type'] == 'CUSTOM':
                    cls_type = macros[macro]['class']
                    # Beware : only cut the first _HOST value, so the macro name can have it on it..
                    macro_name = re.split('_' + cls_type, macro, 1)[1].upper()
                    # Ok, we've got the macro like MAC_ADDRESS for _HOSTMAC_ADDRESS
                    # Now we get the element in data that have the type HOST
                    # and we check if it got the custom value
                    for elt in data:
                        if elt is not None and elt.__class__.my_type.upper() == cls_type:
                            if '_' + macro_name in elt.customs:
                                macros[macro]['val'] = elt.customs['_' + macro_name]
                            # Then look on the macromodulations, in reserver order, so
                            # the last to set, will be the firt to have. (yes, don't want to play
                            # with break and such things sorry...)
                            mms = getattr(elt, 'macromodulations', [])
                            for macromod in mms[::-1]:
                                # Look if the modulation got the value,
                                # but also if it's currently active
                                if '_' + macro_name in macromod.customs and macromod.is_active():
                                    macros[macro]['val'] = macromod.customs['_' + macro_name]
                if macros[macro]['type'] == 'ONDEMAND':
                    macros[macro]['val'] = self._resolve_ondemand(macro, data)

            # We resolved all we can, now replace the macro in the command call
            for macro in macros:
                c_line = c_line.replace('$' + macro + '$', macros[macro]['val'])

            # A $$ means we want a $, it's not a macro!
            # We replace $$ by a big dirty thing to be sure to not misinterpret it
            c_line = c_line.replace("$$", "DOUBLEDOLLAR")

            if nb_loop > 32:  # too much loop, we exit
                still_got_macros = False

        # We now replace the big dirty token we made by only a simple $
        c_line = c_line.replace("DOUBLEDOLLAR", "$")

        # print "Retuning c_line", c_line.strip()
        return c_line.strip()

    def resolve_command(self, com, data):
        """Resolve command macros with data

        :param com: check / event handler or command call object
        :type com: object
        :param data: objects list, use to look for a specific macro
        :type data:
        :return: command line with '$MACRO$' replaced with values
        :rtype: str
        """
        c_line = com.command.command_line
        return self.resolve_simple_macros_in_string(c_line, data, args=com.args)

    def _get_type_of_macro(self, macros, clss):
        r"""Set macros types

        Example::

        ARG\d -> ARGN,
        HOSTBLABLA -> class one and set Host in class)
        _HOSTTOTO -> HOST CUSTOM MACRO TOTO
        SERVICESTATEID:srv-1:Load$ -> MACRO SERVICESTATEID of the service Load of host srv-1

        :param macros: macros list
        :type macros: list[str]
        :param clss: classes list, used to tag class macros
        :type clss:
        :return: None
        """
        for macro in macros:
            # ARGN Macros
            if re.match(r'ARG\d', macro):
                macros[macro]['type'] = 'ARGN'
                continue
            # USERN macros
            # are managed in the Config class, so no
            # need to look that here
            elif re.match(r'_HOST\w', macro):
                macros[macro]['type'] = 'CUSTOM'
                macros[macro]['class'] = 'HOST'
                continue
            elif re.match(r'_SERVICE\w', macro):
                macros[macro]['type'] = 'CUSTOM'
                macros[macro]['class'] = 'SERVICE'
                # value of macro: re.split('_HOST', '_HOSTMAC_ADDRESS')[1]
                continue
            elif re.match(r'_CONTACT\w', macro):
                macros[macro]['type'] = 'CUSTOM'
                macros[macro]['class'] = 'CONTACT'
                continue
            # On demand macro
            elif len(macro.split(':')) > 1:
                macros[macro]['type'] = 'ONDEMAND'
                continue
            # OK, classical macro...
            for cls in clss:
                if macro in cls.macros:
                    macros[macro]['type'] = 'class'
                    macros[macro]['class'] = cls
                    continue

    def _resolve_argn(self, macro, args):
        """Get argument from macro name
        ie : $ARG3$ -> args[2]

        :param macro: macro to parse
        :type macro:
        :param args: args given to command line
        :type args:
        :return: argument at position N-1 in args table (where N is the int parsed)
        :rtype: None | str
        """
        # first, get the number of args
        _id = None
        matches = re.search(r'ARG(?P<id>\d+)', macro)
        if matches is not None:
            _id = int(matches.group('id')) - 1
            try:
                return args[_id]
            except IndexError:
                return ''

    def _resolve_ondemand(self, macro, data):
        """Get on demand macro value

        :param macro: macro to parse
        :type macro:
        :param data: data to get value from
        :type data:
        :return: macro value
        :rtype: str
        """
        # print "\nResolving macro", macro
        elts = macro.split(':')
        nb_parts = len(elts)
        macro_name = elts[0]
        # Len 3 == service, 2 = all others types...
        if nb_parts == 3:
            val = ''
            # print "Got a Service on demand asking...", elts
            (host_name, service_description) = (elts[1], elts[2])
            # host_name can be void, so it's the host in data
            # that is important. We use our self.host_class to
            # find the host in the data :)
            if host_name == '':
                for elt in data:
                    if elt is not None and elt.__class__ == self.host_class:
                        host_name = elt.host_name
            # Ok now we get service
            serv = self.services.find_srv_by_name_and_hostname(host_name, service_description)
            if serv is not None:
                cls = serv.__class__
                prop = cls.macros[macro_name]
                val = self._get_value_from_element(serv, prop)
                # print "Got val:", val
                return val
        # Ok, service was easy, now hard part
        else:
            val = ''
            elt_name = elts[1]
            # Special case: elt_name can be void
            # so it's the host where it apply
            if elt_name == '':
                for elt in data:
                    if elt is not None and elt.__class__ == self.host_class:
                        elt_name = elt.host_name
            for od_list in self.lists_on_demand:
                cls = od_list.inner_class
                # We search our type by looking at the macro
                if macro_name in cls.macros:
                    prop = cls.macros[macro_name]
                    i = od_list.find_by_name(elt_name)
                    if i is not None:
                        val = self._get_value_from_element(i, prop)
                        # Ok we got our value :)
                        break
            return val
        return ''

    def _get_long_date_time(self):
        """Get long date time

        Example : Fri 15 May 11:42:39 CEST 2009

        :return: long date local time
        :rtype: str
        TODO: Should be moved to util
        TODO: Should consider timezone
        """
        return time.strftime("%a %d %b %H:%M:%S %Z %Y").decode('UTF-8', 'ignore')

    def _get_short_date_time(self):
        """Get short date time

        Example : 10-13-2000 00:30:28

        :return: short date local time
        :rtype: str
        TODO: Should be moved to util
        TODO: Should consider timezone
        """
        return time.strftime("%d-%m-%Y %H:%M:%S")

    def _get_date(self):
        """Get date

        Example : 10-13-2000

        :return: local date
        :rtype: str
        TODO: Should be moved to util
        TODO: Should consider timezone
        """
        return time.strftime("%d-%m-%Y")

    def _get_time(self):
        """Get date time

        Example : 00:30:28

        :return: date local time
        :rtype: str
        TODO: Should be moved to util
        TODO: Should consider timezone
        """
        return time.strftime("%H:%M:%S")

    def _get_timet(self):
        """Get epoch time

        Example : 1437143291

        :return: timestamp
        :rtype: str
        TODO: Should be moved to util
        TODO: Should consider timezone
        """
        return str(int(time.time()))

    def _tot_hosts_by_state(self, state):
        """Generic function to get the number of host in the specified state

        :param state: state to filter on
        :type state:
        :return: number of host in state *state*
        :rtype: int
        TODO: Should be moved
        """
        return sum(1 for h in self.hosts if h.state == state)

    _get_total_hosts_up = lambda s: s._tot_hosts_by_state('UP')
    _get_total_hosts_down = lambda s: s._tot_hosts_by_state('DOWN')
    _get_total_hosts_unreachable = lambda s: s._tot_hosts_by_state('UNREACHABLE')

    def _get_total_hosts_unreachable_unhandled(self):
        """DOES NOTHING( Should get the number of unreachable hosts not handled)

        :return: 0 always
        :rtype: int
        TODO: Implement this
        """
        return 0

    def _get_total_hosts_problems(self):
        """Get the number of hosts that are a problem

        :return: number of hosts with is_problem attribute True
        :rtype: int
        """
        return sum(1 for h in self.hosts if h.is_problem)

    def _get_total_hosts_problems_unhandled(self):
        """DOES NOTHING( Should get the number of host problems not handled)

        :return: 0 always
        :rtype: int
        TODO: Implement this
        """
        return 0

    def _tot_services_by_state(self, state):
        """Generic function to get the number of service in the specified state

        :param state: state to filter on
        :type state:
        :return: number of service in state *state*
        :rtype: int
        TODO: Should be moved
        """
        return sum(1 for s in self.services if s.state == state)

    _get_total_service_ok = lambda s: s._tot_services_by_state('OK')

    _get_total_service_warning = lambda s: s._tot_services_by_state('WARNING')

    _get_total_service_critical = lambda s: s._tot_services_by_state('CRITICAL')

    _get_total_service_unknown = lambda s: s._tot_services_by_state('UNKNOWN')

    def _get_total_services_warning_unhandled(self):
        """DOES NOTHING (Should get the number of warning services not handled)

        :return: 0 always
        :rtype: int
        TODO: Implement this
        """
        return 0

    def _get_total_services_critical_unhandled(self):
        """DOES NOTHING (Should get the number of critical services not handled)

        :return: 0 always
        :rtype: int
        TODO: Implement this
        """
        return 0

    def _get_total_services_unknown_unhandled(self):
        """DOES NOTHING (Should get the number of unknown services not handled)

        :return: 0 always
        :rtype: int
        TODO: Implement this
        """
        return 0

    def _get_total_service_problems(self):
        """Get the number of services that are a problem

        :return: number of services with is_problem attribute True
        :rtype: int
        """
        return sum(1 for s in self.services if s.is_problem)

    def _get_total_service_problems_unhandled(self):
        """DOES NOTHING (Should get the number of service problems not handled)

        :return: 0 always
        :rtype: int
        TODO: Implement this
        """
        return 0

    def _get_process_start_time(self):
        """DOES NOTHING ( Should get process start time)

        :return: 0 always
        :rtype: int
        TODO: Implement this
        """
        return 0

    def _get_events_start_time(self):
        """DOES NOTHING ( Should get events start time)

        :return: 0 always
        :rtype: int
        TODO: Implement this
        """
        return 0
