#  -*- encoding: utf-8 -*-
##############################################################################
#
#    OpenERP, Open Source Management Solution
#    Copyright (C) 2012 Savoir-faire Linux (<http://www.savoirfairelinux.com>).
#
#    This program 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.
#
#    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

from datetime import datetime, timedelta
from odoo import fields, models, api
from odoo.tools.translate import _
from odoo.tools.safe_eval import safe_eval
from odoo.exceptions import ValidationError
from odoo.tools import (
    DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT,
)
import time
import re
import logging
_logger = logging.getLogger(__name__)


def is_one_value(result):
    # check if sql query returns only one value
    if type(result) is dict and 'value' in result.dictfetchone():
        return True
    elif type(result) is list and 'value' in result[0]:
        return True
    else:
        return False


RE_SELECT_QUERY = re.compile('.*(' + '|'.join((
    'INSERT',
    'UPDATE',
    'DELETE',
    'CREATE',
    'ALTER',
    'DROP',
    'GRANT',
    'REVOKE',
    'INDEX',
)) + ')')


def is_select_query(query):
    """Check if sql query is a SELECT statement"""
    return not RE_SELECT_QUERY.match(query.upper())


class mgmtsystem_kpi_category(models.Model):
    """
    KPI Category
    """
    _name = "mgmtsystem.kpi.category"
    _description = "KPI Category"
    
    name = fields.Char('Name', size=50, required=True)
    description = fields.Text('Description')
    


class mgmtsystem_kpi_threshold_range(models.Model):
    """
    KPI Threshold Range
    """
    _name = "mgmtsystem.kpi.threshold.range"
    _description = "KPI Threshold Range"

    @api.depends('min_type', 'min_code', 'min_dbsource_id', 'min_fixed_value')
    def compute_min_value(self):
        context = self._context
        if context is None:
            context = {}
        result = {}
        cr = self._cr
        for obj in self:
            value = None
            if obj.min_type == 'local' and is_select_query(obj.min_code):
                cr.execute(obj.min_code)
                dic = cr.dictfetchall()
                if is_one_value(dic):
                    value = dic[0]['value']
            elif (obj.min_type == 'external'
                  and obj.min_dbsource_id.id
                  and is_select_query(obj.min_code)):
                dbsrc_obj = obj.min_dbsource_id
                res = dbsrc_obj.execute(obj.min_code)
                if is_one_value(res):
                    value = res[0]['value']
            elif obj.min_type == 'python':
                value = safe_eval(obj.min_code)
            else:
                value = obj.min_fixed_value
            obj.min_value = value

    @api.depends('min_type', 'min_code', 'min_dbsource_id', 'min_fixed_value')
    def compute_max_value(self):
        context = self._context
        cr = self._cr
        if context is None:
            context = {}
        result = {}
        for obj in self:
            value = None
            if obj.max_type == 'local' and is_select_query(obj.max_code):
                cr.execute(obj.max_code)
                dic = cr.dictfetchall()
                if is_one_value(dic):
                    value = dic[0]['value']
            elif obj.max_type == 'python':
                value = safe_eval(obj.max_code)
            elif (obj.max_type == 'external'
                  and obj.max_dbsource_id.id
                  and is_select_query(obj.max_code)):
                dbsrc_obj = obj.max_dbsource_id
                res = dbsrc_obj.execute(obj.max_code)
                if is_one_value(res):
                    value = res[0]['value']
            else:
                value = obj.max_fixed_value
            max_value = value

    @api.depends('max_value', 'min_value')
    def _is_valid_range(self):
        context = self._context
        cr = self._cr
        if context is None:
            context = {}
        flag = True
        for obj in self:
            if obj.max_value < obj.min_value:
                flag = False
            else:
                flag = True
            obj.valid = flag

    @api.depends('valid')
    def _generate_invalid_message(self):
        context = self._context
        cr = self._cr
        if context is None:
            context = {}
        result = ''
        for obj in self:
            if obj.valid:
                result = ""
            else:
                result = ("Minimum value is greater than the maximum "
                                  "value! Please adjust them.")
            obj.invalid_message = result

    @api.model
    def _get_company(self):
        return self.env.user.company_id

    name = fields.Char('Name', size=50, required=True)
    valid = fields.Boolean(compute='_is_valid_range', string='Valid', store = True, default=True)
    invalid_message = fields.Char(compute='_generate_invalid_message', string='Message', store=True)
    min_type = fields.Selection((
            ('static', 'Fixed value'),
            ('python', 'Python Code'),
            ('local', 'SQL - Local DB'),
            ('external', 'SQL - Externa DB'),
        ), 'Min Type', required=True)
    min_value = fields.Float(compute='compute_min_value', string='Minimum', store=True)
    
    min_fixed_value = fields.Float('Minimum')
    min_code = fields.Text('Minimum Computation Code')
    min_dbsource_id = fields.Many2one('base.external.dbsource', 'External DB Source')
    max_type = fields.Selection((
            ('static', 'Fixed value'),
            ('python', 'Python Code'),
            ('local', 'SQL - Local DB'),
            ('external', 'SQL - External DB'),
        ), 'Max Type', required=True)
    max_value = fields.Float(compute='compute_max_value', string='Maximum', store=True)
        
    max_fixed_value = fields.Float('Maximum')
    max_code = fields.Text('Maximum Computation Code')
    max_dbsource_id = fields.Many2one('base.external.dbsource', 'External DB Source')
    color = fields.Char('Color', help='RGB code with #', size=7, required=True)
    threshold_ids = fields.Many2many('mgmtsystem.kpi.threshold', 'mgmtsystem_kpi_threshold_range_rel', 'range_id', 'threshold_id', 'Thresholds')
    company_id = fields.Many2one('res.company', 'Company', default=_get_company)


class mgmtsystem_kpi_threshold(models.Model):
    """
    KPI Threshold
    """
    _name = "mgmtsystem.kpi.threshold"
    _description = "KPI Threshold"

    @api.depends('range_ids')
    def _is_valid_threshold(self):
        context = self._context
        if context is None:
            context = {}
        result = False
        for obj in self:
            # check if ranges overlap
            # TODO: This code can be done better
            for range1 in obj.range_ids:
                for range2 in obj.range_ids:
                    if (range1.valid and range2.valid
                            and range1.min_value < range2.min_value):
                        result = range1.max_value <= range2.min_value
            obj.valid = result

    @api.depends('valid')
    def _generate_invalid_message(self):
        context = self._context
        if context is None:
            context = {}
        result = ''
        for obj in self:
            if obj.valid:
                result = ""
            else:
                result = ("2 of your ranges are overlapping! Please "
                                  "make sure your ranges do not overlap.")
            obj.invalid_message = result

    @api.model
    def _get_company(self):
        return self.env.user.company_id

    name = fields.Char('Name', size=50, required=True)
    range_ids = fields.Many2many('mgmtsystem.kpi.threshold.range', 'mgmtsystem_kpi_threshold_range_rel', 'threshold_id', 'range_id', 'Ranges')
    valid = fields.Boolean(compute='_is_valid_threshold', string='Valid', required=True, store=True, default=True)
    invalid_message = fields.Char(compute='_generate_invalid_message', string='Message', size=100, store=True)
    kpi_ids = fields.One2many('mgmtsystem.kpi', 'threshold_id', 'KPIs')
    company_id = fields.Many2one('res.company', 'Company', default=_get_company)
    
    @api.model
    def create(self, data):
        context = self._context
        if context is None:
            context = {}

        # check if ranges overlap
        # TODO: This code can be done better
        range_obj1 = self.env['mgmtsystem.kpi.threshold.range']
        range_obj2 = self.env['mgmtsystem.kpi.threshold.range']
        for range1 in data['range_ids'][0][2]:
            range_obj1 = range_obj1.browse(range1)
            for range2 in data['range_ids'][0][2]:
                range_obj2 = range_obj2.browse(range2)
                if (range_obj1.valid and range_obj2.valid
                        and range_obj1.min_value < range_obj2.min_value):
                    if range_obj1.max_value > range_obj2.min_value:
                        raise ValidationError("2 of your ranges are overlapping!\n\nPlease make sure your ranges do not overlap!")
                range_obj2 = self.env['mgmtsystem.kpi.threshold.range']
            range_obj1 = self.env['mgmtsystem.kpi.threshold.range']
        return super(mgmtsystem_kpi_threshold, self).create(data)

    @api.multi
    def get_color(self, kpi_value=None):
        context = self._context
        if context is None:
            context = {}

        color = '#FFFFFF'
        for obj in self:
            for range_obj in obj.range_ids:
                if (range_obj.min_value <= kpi_value <= range_obj.max_value
                        and range_obj.valid):
                    color = range_obj.color
        return color


class mgmtsystem_kpi_history(models.Model):
    """
    History of the KPI
    """
    _name = "mgmtsystem.kpi.history"
    _description = "History of the KPI"

    _order = "date desc"

    @api.model
    def _get_company(self):
        return self.env.user.company_id

    name = fields.Char('Name', size=150, required=True, default=time.strftime('%d %B %Y'))
    kpi_id = fields.Many2one('mgmtsystem.kpi', 'KPI', required=True)
    date = fields.Datetime('Execution Date', required=True, readonly=True, default=time.strftime(DATETIME_FORMAT))
    value = fields.Float('Value', required=True, readonly=True)
    color = fields.Text('Color', required=True, readonly=True, default='#FFFFFF')
    company_id = fields.Many2one('res.company', 'Company', default=_get_company)


class mgmtsystem_kpi(models.Model):
    """
    Key Performance Indicators
    """
    _name = "mgmtsystem.kpi"
    _description = "Key Performance Indicator"

    @api.depends('history_ids')
    def _display_last_kpi_value(self):
        context = self._context
        if context is None:
            context = {}

        result = False
        for obj in self:
            if obj.history_ids:
                result = obj.history_ids[0].value
            else:
                result = 0
            obj.value = result

    @api.multi
    def compute_kpi_value(self):
        context = self._context
        cr = self._cr
        if context is None:
            context = {}
        for obj in self:
            kpi_value = 0
            if obj.kpi_type == 'local' and is_select_query(obj.kpi_code):
                cr.execute(obj.kpi_code)
                dic = cr.dictfetchall()
                if is_one_value(dic):
                    kpi_value = dic[0]['value']
            elif (obj.kpi_type == 'external'
                    and obj.dbsource_id.id
                    and is_select_query(obj.kpi_code)):
                dbsrc_obj = obj.dbsource_id
                res = dbsrc_obj.execute(obj.kpi_code)
                if is_one_value(res):
                    kpi_value = res[0]['value']
            elif obj.kpi_type == 'python':
                kpi_value = safe_eval(obj.kpi_code)

            threshold_obj = obj.threshold_id
            values = {
                'kpi_id': obj.id,
                'value': kpi_value,
                'color': threshold_obj.get_color(kpi_value),
            }

            history_obj = self.env['mgmtsystem.kpi.history']
            history_id = history_obj.create(values)
            obj.history_ids.append(history_id.id)

        return True

    @api.multi
    def update_next_execution_date(self):
        cr = self._cr
        context = self._context
        if context is None:
            context = {}

        for obj in self:
            if obj.periodicity_uom == 'hour':
                delta = timedelta(hours=obj.periodicity)
            elif obj.periodicity_uom == 'day':
                delta = timedelta(days=obj.periodicity)
            elif obj.periodicity_uom == 'week':
                delta = timedelta(weeks=obj.periodicity)
            elif obj.periodicity_uom == 'month':
                delta = timedelta(months=obj.periodicity)
            else:
                delta = timedelta()
            new_date = datetime.now() + delta

            values = {
                'next_execution_date': new_date.strftime(DATETIME_FORMAT),
            }

            obj.write(values)

        return True

    # Method called by the scheduler
    @api.multi
    def update_kpi_value(self):
        cr = self._cr
        context = self._context
        ids = self or False
        if context is None:
            context = {}
        if not ids:
            filters = [
                '&',
                '|',
                ('active', '=', True),
                ('next_execution_date', '<=',
                    datetime.now().strftime(DATETIME_FORMAT)),
                ('next_execution_date', '=', False),
            ]
            if 'filters' in context:
                filters.extend(context['filters'])
            ids = self.search(filters)
            if ids: ids = ids[0]
        res = None

        try:
            res = ids.compute_kpi_value()
            ids.update_next_execution_date()
        except Exception:
            _logger.exception("Failed updating KPI values")

        return res

    @api.model
    def _get_company(self):
        return self.env.user.company_id


    name = fields.Char('Name', size=50, required=True)
    description = fields.Text('Description')
    category_id = fields.Many2one('mgmtsystem.kpi.category', 'Category', required=True)
    threshold_id = fields.Many2one('mgmtsystem.kpi.threshold', 'Threshold', required=True)
    periodicity = fields.Integer('Periodicity', default=1)
    periodicity_uom = fields.Selection((
            ('hour', 'Hour'),
            ('day', 'Day'),
            ('week', 'Week'),
            ('month', 'Month')
        ), 'Periodicity UoM', required=True, default='day')
    next_execution_date = fields.Datetime(
            'Next execution date',
            readonly=True,
        )
    value = fields.Float(compute='_display_last_kpi_value', string='Value')
    kpi_type = fields.Selection((
            ('python', 'Python'),
            ('local', 'SQL - Local DB'),
            ('external', 'SQL - External DB')
        ), 'KPI Computation Type')
    dbsource_id = fields.Many2one(
            'base.external.dbsource',
            'External DB Source',
        )
    kpi_code = fields.Text(
            'KPI Code',
            help=("SQL code must return the result as 'value' "
                  "(i.e. 'SELECT 5 AS value')."),
        )
    history_ids = fields.One2many(
            'mgmtsystem.kpi.history',
            'kpi_id',
            'History',
        )
    active = fields.Boolean(
            'Active',
            help=("Only active KPIs will be updated by the scheduler based on"
                  " the periodicity configuration."), default=True
        )
    company_id = fields.Many2one('res.company', 'Company', default=_get_company)

