#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai

# Copyright(C) 2013  Romain Bignon, Florent Fourcot
#
# This file is part of weboob.
#
# weboob 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.
#
# weboob 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 weboob. If not, see <http://www.gnu.org/licenses/>.

### Installation ###
# 1) Create a symlink from /etc/munin/plugins/yourchoice to the script
# 2) Configure the plugin in /etc/munin/plugin-conf.d/ See below for the options
# 3) Restart/reload munin-node
# 4) Note that cached values are stored in folder ~/.config/weboob/munin/

### Configuration ###
## Mandatory options ##
# env.capa: The Weboob capability to load
# Example: env.capa CapBank
#
# env.do: The Weboob command to call. It can take more than one argument.
#         With two argument, the second is used as parameter for the command.
#         The third is used to restrict backends.
# Example: env.do get_balance
#
# env.import: The import line to import the capabilities
# Example: from weboob.capabilities.bank import CapBank
#
# env.attribvalue: The attribut name of objects returned by the do command.
#                  For example, the "balance" member of Account objects
#                  If the attribut is itself one object, a hierarchical call can be done
#                  with the "/" operators.
# Example: env.attribvalue balance
# Example: env.attribvalue temp/value

## Optionals -- more configuration ##
# env.id_monitored: Restrict the results to a list of ids (space is used as separator)
# Example: env.id_monitored account1@backend1 account2@backend2
#
# env.exclude: Exclude some results (space is used as separator)
# Example: env.exclude 550810@sachsen
#
# env.cache_expire: To avoid site flooding, results are cached in folder
#                   /.config/weboob/munin/. The default lifetime of a cache value is 3600s
# Example: env.cache_expire 7200
#
# env.cumulate: Display data in Area mode (default) or in line mode.
# Example:  env.cumulate 0
#
# env.get_object_list: optional pre-call to get a list of objects, to applied
#                      the do function on it.
# Exemple: env.get_object_list="iter_subscriptions"
#
# env.attribid: Munin needs an id for each value. The default is to use the id of results,
#               but another attribute can be used. "/" can be used as separator for
#               hierarchical calls
# Example: env.attribid id
#
# env.title: A title for the graph (default: nothing)
# Example: env.title a wonderful graph
#
# env.vlabel: A vertical label for the graph
# Example: env.vlabel Balance
#
# env.label: Each data in munin as a label. Per default, the script takes the
#            "label" attribute of objects. However, it does not always exist,
#            and a better choice can be possible
# Example: env.label id
#
# env.category: set the graph category (default: weboob)
# Example: env.category bank
# For some running examples, see at the end of the script

from __future__ import print_function

import os
import sys
import locale
import time
import logging
from weboob.capabilities.base import NotAvailable
from weboob.core import Weboob, CallErrors
from weboob.exceptions import BrowserIncorrectPassword


class GenericMuninPlugin(object):
    def __init__(self):
        if 'weboob_path' in os.environ:
            self.weboob = Weboob(os.environ['weboob_path'])
        else:
            self.weboob = Weboob()
        self.cache_expire = long(os.environ.get('cache_expire', 3600))
        self.cumulate = int(os.environ.get('cumulate', 1))
        self.cache = None
        self.name = sys.argv[0]
        if "/" in self.name:
            self.name = self.name.split('/')[-1]

        # Capability to load
        self.capa = os.environ['capa']
        # Command to pass to Weboob
        self.do = os.environ['do'].split(',')
        # Not easy to load modules automatically...
        self.mimport = os.environ["import"]
        exec(self.mimport)
        # We can monitore only some objects
        self.object_list = None
        if 'get_object_list' in os.environ:
            self.object_list = os.environ["get_object_list"]
        self.tomonitore = None
        if 'id_monitored' in os.environ:
            self.tomonitore = os.environ['id_monitored'].decode('utf-8').split(' ')
        self.exclude = None
        if 'exclude' in os.environ:
            self.exclude = os.environ['exclude'].split(' ')
        # Attribut of object to use as ID (default: id)
        self.attribid = "id"
        if 'attribid' in os.environ:
            self.attribid = os.environ['attribid']
        self.attribvalue = os.environ['attribvalue']
        self.title = ''
        if 'title' in os.environ:
            self.title = os.environ['title'].decode('utf-8')
        self.attriblabel = "label"
        if 'label' in os.environ:
            self.attriblabel = os.environ['label']
        self.vlabel = self.attribvalue
        if 'vlabel' in os.environ:
            self.vlabel = os.environ['vlabel'].decode('utf-8')
        self.category = "weboob"
        if 'category' in os.environ:
            self.category = os.environ['category'].decode('utf-8')


    def display_help(self):
        print('generic-munin is a plugin for munin')
        print('')
        print('Copyright(C) 2013 Romain Bignon, Florent Fourcot')
        print('')
        print('To use it, create a symlink /etc/munin/plugins/nameyouwant to this script')
        print('and add this section in /etc/munin/plugin-conf.d/munin-node:')
        print('')
        print('[nameyouwant]')
        print('user romain')
        print('group romain')
        print('env.HOME /home/romain')
        print('# The weboob directory path.')
        print('env.weboob_path /home/romain/.config/weboob/')
        print('# Monitored objects. If this parameter is missing, all objects')
        print('# will be displayed.')
        print('env.id_monitored myid@backend1 otherid@backend2')
        print('# To prevent mass connections to websites, results are cached.')
        print('# You can set here the expiration delay (in seconds).')
        print('env.cache_expire 7200')
        print('# Cumulate values')
        print('env.cumulate 1')
        print('')

    def cachepath(self, name):
        tmpdir = os.path.join(self.weboob.workdir, "munin")
        if not os.path.isdir(tmpdir):
            os.makedirs(tmpdir)

        return os.path.join(tmpdir, name)

    def check_cache(self, name):
        return self.print_cache(name, check=True)

    def print_cache(self, name, check=False):
        try:
            f = open(self.cachepath(name), 'r')
        except IOError:
            return False

        try:
            last = int(f.readline().strip())
        except ValueError:
            return False

        if check and (last + self.cache_expire) < time.time():
            return False

        for line in f:
            sys.stdout.write(line)
        return True

    def new_cache(self, name):
        os.umask(0o077)
        new_name = '%s.new' % name
        filename = self.cachepath(new_name)
        try:
            f = open(filename, 'w')
        except IOError as e:
            print('Unable to create the cache file %s: %s' % (filename, e), file=sys.stderr)
            return

        self.cache = f
        self.cache.write('%d\n' % time.time())

    def flush_cache(self):
        old_name = self.cache.name
        new_name = self.cache.name[:-4]
        self.cache.close()
        os.rename(old_name, new_name)

    def write_output(self, line):
        sys.stdout.write('%s\n' % line)
        if self.cache:
            self.cache.write('%s\n' % line)

    def build_do(self):
        if self.object_list:
            results = []
            for result in self.weboob.do(self.object_list):
                results.append(result)
            for result in results:
                try:
                    for i in self.weboob.do(self.do[0], result.id, backends=result.backend):
                        yield i
                # Do not crash if one module does not implement the feature
                except CallErrors:
                    pass
        elif len(self.do) == 1:
            for i in self.weboob.do(self.do[0]):
                yield i
        elif len(self.do) == 2:
            for i in self.weboob.do(self.do[0], self.do[1]):
                yield i
        elif len(self.do) == 3:
            for i in self.weboob.do(self.do[0], self.do[1], backends=self.do[2]):
                yield i

    def get_value(self, result):
        attribs = self.attribvalue.split('/')
        for attrib in attribs:
            result = getattr(result, attrib)
            if type(result) is list:
                result = result[0]
        return result

    def monitored(self, result):
        id = self.result2weboobid(result)
        if self.exclude and id in self.exclude:
            return False
        return not self.tomonitore or id in self.tomonitore

    def result2weboobid(self, result):
        attribs = self.attribid.split('/')
        id = '%s@%s' % (getattr(result, attribs[0]), result.backend)
        return id

    def result2id(self, result):
        attribs = self.attribid.split('/')
        id = result
        for attrib in attribs:
            id = getattr(id, attrib)
        return '%s_%s' % (result.backend, id)


    def config(self):
        if self.check_cache('%s-config' % self.name):
            return

        self.new_cache('%s-config' % self.name)
        self.weboob.load_backends(self.capa)
        self.write_output('graph_title %s' % self.title.encode('iso-8859-15'))
        self.write_output('graph_vlabel %s' % self.vlabel.encode('iso-8859-15'))
        self.write_output('graph_category %s' % self.category)
        self.write_output('graph_args --rigid')
        if self.cumulate:
            self.write_output('graph_total Total')
        try:
            objects = []
            if self.tomonitore or self.exclude:
                d = {}
                for result in self.build_do():
                    if self.monitored(result):
                        d[self.result2weboobid(result)] = result

                if self.tomonitore:
                    for id in self.tomonitore:
                        try:
                            objects.append(d[id])
                        except KeyError:
                            pass
                else:
                    for id in d:
                        objects.append(d[id])
            else:
                objects = reversed([a for a in self.build_do()])

            first = True
            for result in objects:
                id = self.result2id(result)
                type = 'STACK'
                if first:
                    type = 'AREA'
                    first = False
                self.write_output('%s.label %s' % (id.encode('iso-8859-15'), getattr(result, self.attriblabel).encode('iso-8859-15')))
                if self.cumulate:
                    self.write_output('%s.draw %s' % (id, type))
        except CallErrors as errors:
            self.print_errors(errors)
            self.print_cache('%s-config' % self.name)
        else:
            self.flush_cache()

    def print_errors(self, errors):
        for backend, err, backtrace in errors:
            print((u'%s(%s): %s' % (type(err).__name__, backend.name, err)).encode(sys.stdout.encoding or locale.getpreferredencoding(), 'replace'), file=sys.stderr)
            if isinstance(err, BrowserIncorrectPassword):
                self.weboob.backends_config.edit_backend(backend.name, backend.NAME, {'_enabled': 'false'})

    def execute(self):
        if self.check_cache(self.name):
            return

        self.new_cache(self.name)
        self.weboob.load_backends(self.capa)
        try:
            for result in self.build_do():
                if self.monitored(result):
                    value = self.get_value(result)
                    if value is not NotAvailable:
                        self.write_output('%s.value %f' % (self.result2id(result).encode('iso-8859-15'), value))
        except CallErrors as errors:
            self.print_errors(errors)
            self.print_cache(self.name)
        else:
            self.flush_cache()

    def run(self):
        cmd = (len(sys.argv) > 1 and sys.argv[1]) or "execute"
        if cmd == 'execute':
            self.execute()
        elif cmd == 'config':
            self.config()
        elif cmd == 'autoconf':
            print('no')
            sys.exit(1)
        elif cmd == 'suggest':
            sys.exit(1)
        elif cmd == 'help' or cmd == '-h' or cmd == '--help':
            self.display_help()

        if self.cache:
            self.cache.close()

        sys.exit(0)

if __name__ == '__main__':
    logging.basicConfig()
    GenericMuninPlugin().run()

### Examples ###
## Like boobank-munin does
## Only for the example, you should use boobank-munin instead
#[bank]
#user florent
#group florent
#env.cache_expire 7200
#env.HOME /home/flo
#env.capa CapBank
#env.do iter_accounts
#env.import from weboob.capabilities.bank import CapBank
#env.attribvalue balance
#env.title Solde des comptes
#
#
## Balance of your leclercmobile subscription
#[leclercmobile]
#user florent
#group florent
#env.cache_expire 16800
#env.HOME /home/flo
#env.capa CapBill
#env.do get_balance,06XXXXXXXX,leclercmobile
#env.import from weboob.capabilities.bill import CapBill
#env.attribvalue price
#env.title Forfait leclercmobile
#env.vlabel Solde
#
#Result: http://fourcot.fr/weboob/leclercmobile-day.png
#
## Monitor water level in Dresden
#[leveldresden]
#user florent
#group florent
#env.cache_expire 7200
#env.HOME /home/flo
#env.capa CapGauge
#env.do get_last_measure,501060-level
#env.import from weboob.capabilities.gauge import CapGauge
#env.attribvalue level
#env.title Niveau de l'elbe
#env.label id
#
#
## The level of the elbe in all Sachsen's cities
#[levelelbesachsen]
#user florent
#env.cache_expire 800
#env.HOME /home/flo
#env.cumulate 0
#env.capa CapGauge
#env.do iter_gauges,Elbe,sachsen
#env.import from weboob.capabilities.gauge import CapGauge
#env.attribvalue sensors/lastvalue/level
#env.title Niveau de l'elbe en Saxe
#env.label name
#env.vlabel Hauteur du fleuve (cm)
#env.exclude 550810@sachsen
#
#Result: http://fourcot.fr/weboob/elbesachsen-day.png
#
## Temperature in Rennes
#[temprennes]
#user florent
#env.HOME /home/flo
#env.cumulate 0
#env.capa CapWeather
#env.do get_current,619163,yahoo
#env.import from weboob.capabilities.weather import CapWeather
#env.attribvalue temp/value
#env.attribid temp/id
#env.title Température à Rennes
#env.vlabel Température
#env.label id
#
#Result: http://fourcot.fr/weboob/temprennes-day.png
