import json
import logging
import math
import operator
import os
import uuid
from functools import reduce

from dateutil import parser
from flask import Blueprint, request, send_file
import datetime
from common.status import HTTP_200_OK, HTTP_400_BAD_REQUEST
from flask_jwt_extended import jwt_required
from median.models.log import LogDefaut, DefDefaut
from median.models import Magasin

from peewee import fn, JOIN
import numpy as np

from ressources.astus.utils import generate_excel_file
from ressources.blueprint.stats_utils import estimate_coef, lineary_regression

acced_message_blueprint = Blueprint('acced_message', __name__)

logger = logging.getLogger('median')

MIN_DURATION = 60  # Durée mininum d'un message


def weeks_between(end_date, start_date):
    nb_weeks = None
    if start_date is not None and end_date is not None:
        s = parser.parse(start_date)
        e = parser.parse(end_date)
        monday_start = (s - datetime.timedelta(days=s.weekday()))
        monday_end = (e - datetime.timedelta(days=e.weekday()))
        nb_weeks = math.ceil((monday_end - monday_start).days / 7)

        if nb_weeks == 0:
            nb_weeks = 1

    return nb_weeks


def get_obj_history_message_export(i):
    obj = {
        'num_default': i['num_default'],
        'message': i['message'],
        'default': i['default'],
        'lvl': i['lvl'],
        'count': i['count'],
        'duration': i['duration'],
        'mean': i['mean'],
        'quantile': i['quantile'],
        'score': i['score']['value']
    }

    return obj


def get_obj_history_message(i, nb_weeks):
    mean_duration = np.mean(np.array(i['durations'])) if len(i['durations']) > 0 else None
    seriousness = seriousness_score(i['lvl'])
    duration = duration_score(mean_duration) if mean_duration is not None and not math.isnan(
        mean_duration) else 0

    frequency = frequency_score(nb_defects=i['count'], nb_weeks=nb_weeks) if nb_weeks is not None else 0
    score = {
        'value': seriousness * duration * frequency,
        'duration': duration,
        'frequency': frequency,
        'seriousness': seriousness
    }
    q = np.quantile(np.array(i['durations']), 0.75) if len(i['durations']) > 0 else None

    obj = {
        'mean': int(mean_duration) if mean_duration is not None and not math.isnan(mean_duration) else None,
        'score': score,
        'quantile': q,
        'default': i['default'],
        'lvl': i['lvl'],
        'num_default': i['num_defaut'],
        'message': i['message'],
        'count': i['count'],
        'duration': i['duration'],
        'msg_duration': i['msg_duration'],
        'num_week': i.get('num_week', None),
        'date_hour': i.get('date_hour', None),
        'date': i.get('date', None)
    }

    return obj


@acced_message_blueprint.route('/history/export', methods=['POST'])
def get_messages_history_export():
    args = json.loads(request.data)
    v_sort_by = args['sortby']
    nb_weeks = weeks_between(args['end_date'], args['start_date'])
    req_default = _getHistoryRequest(args=args)
    res = getList(request=req_default, start=None, length=None, sort_by=v_sort_by, group_by=None,
                  nb_weeks=nb_weeks)
    headers = args['headers']
    name = os.sep.join(
        [os.getcwd(), "tmp_export", "%s.xlsx" % uuid.uuid4()])

    generate_excel_file(name=name, headers=headers, items=res[1], transform_function=get_obj_history_message_export)

    return send_file(name, as_attachment=True)


def _getHistoryRequest(args):
    v_type_mag = args['type_mag']
    v_type_machine = args['type_machine']
    v_date_debut = args['start_date']
    v_date_fin = args['end_date']
    v_search_list = args['criterias']
    v_complexities = args['complexity']
    v_default_types = args['default_type']

    andexpr = ((LogDefaut.poste == v_type_mag) &
               (LogDefaut.chrono >= v_date_debut) &
               (LogDefaut.chrono <= v_date_fin))

    if v_complexities is not None and len(v_complexities) > 0:
        andexpr = reduce(operator.and_, [andexpr, DefDefaut.niveau << v_complexities])

    if v_default_types is not None and len(v_default_types) > 0:
        andexpr = reduce(operator.and_, [andexpr, DefDefaut.defaut << v_default_types])

    if len(v_search_list) > 0:
        lst = list(map(lambda s: (
            (LogDefaut.num_defaut.contains(s.strip()))), v_search_list))

        search = reduce(operator.and_, lst)

        expr = reduce(operator.and_, [andexpr, search])
    else:
        expr = andexpr

    return LogDefaut.select(LogDefaut.num_defaut, LogDefaut.message,
                            DefDefaut,
                            LogDefaut.poste, LogDefaut.chrono, LogDefaut.valeur) \
        .join(DefDefaut, on=(LogDefaut.num_defaut == DefDefaut.num_defaut) &
                            (DefDefaut.type_machine == v_type_machine)) \
        .where(expr) \
        .order_by(
        LogDefaut.num_defaut,
        LogDefaut.chrono.asc())


@acced_message_blueprint.route('/history', methods=['POST'])
def get_messages_history():
    args = json.loads(request.data)
    req_default = _getHistoryRequest(args=args)
    v_sort_by = args['sortby']
    v_start = args['start']
    v_length = args['length']
    nb_weeks = weeks_between(args['end_date'], args['start_date'])
    res = getList(request=req_default,
                  start=v_start, length=v_length,
                  sort_by=v_sort_by, group_by=None,
                  nb_weeks=nb_weeks)

    return {
        'count': res[0],
        'list': res[1],
    }


def getList(request, start, length, sort_by, group_by, nb_weeks):
    res = []
    old_key = None
    default = None

    for item in request:
        min_duration = item.defdefaut.duration if item.defdefaut.duration is not None else 0
        if group_by is None:
            key = item.num_defaut
        elif group_by == 1:
            key = item.chrono.isocalendar().week
        elif group_by == 0:
            key = datetime.datetime.combine(item.chrono.date(), datetime.time.min).timestamp()
        else:
            key = datetime.datetime(year=item.chrono.year,
                                    month=item.chrono.month,
                                    day=item.chrono.day,
                                    hour=item.chrono.hour).timestamp()

        if key != old_key and item.valeur == 1:
            default = {
                'default': None,
                'msg_duration': min_duration,
                'module': item.poste,
                'message': item.message,
                'lvl': None,
                'count': 0,
                'duration': 0,
                'durations': [],
                'start': None,
                'num_defaut': item.num_defaut
            }
            if group_by is None:
                default['num_defaut'] = item.num_defaut
            elif group_by == 1:
                default['num_week'] = key
            elif group_by == 0:
                default['date'] = key
            else:
                default['date_hour'] = key

            if hasattr(item, 'defdefaut'):
                default['default'] = item.defdefaut.defaut
                default['lvl'] = item.defdefaut.niveau
                default['msg_duration'] = min_duration

            res.append(default)

        if item.valeur == 1:
            default['start'] = item.chrono
            old_key = key
        else:
            if default is not None and default.get('start', None) is not None:
                message_duration = (item.chrono - default['start']).total_seconds()
                if message_duration > min_duration:
                    default['count'] = default['count'] + 1
                    default['duration'] = default['duration'] + message_duration
                    default['durations'].append(message_duration)
                default['start'] = None

    res = [get_obj_history_message(i, nb_weeks) for i in res]

    if sort_by is not None and sort_by == 1:
        sorted_list = sorted(res, key=lambda d: d['duration'], reverse=True)
    elif sort_by is not None and sort_by == 0:
        sorted_list = sorted(res, key=lambda d: d['count'], reverse=True)
    elif sort_by is not None and sort_by == 2:
        sorted_list = sorted(res, key=lambda d: d['score']['value'], reverse=True)
    else:
        sorted_list = res

    if start is not None and length is not None:
        sorted_list = sorted_list[start:start + length]

    return len(res), sorted_list


def _getDetailRequest(num_default, args):
    v_date_debut = args['start_date']
    v_date_fin = args['end_date']
    v_type_mag = args['type_mag']

    andexpr = ((LogDefaut.poste == v_type_mag) &
               (LogDefaut.num_defaut == num_default) &
               (LogDefaut.chrono >= v_date_debut) &
               (LogDefaut.chrono <= v_date_fin))

    req_default = LogDefaut.select(LogDefaut.num_defaut, LogDefaut.message,
                                   LogDefaut.poste, LogDefaut.chrono, LogDefaut.valeur,
                                   DefDefaut.niveau, DefDefaut.duration) \
        .join(DefDefaut, JOIN.LEFT_OUTER, on=DefDefaut.num_defaut == LogDefaut.num_defaut) \
        .where(andexpr) \
        .order_by(
        LogDefaut.num_defaut,
        LogDefaut.chrono.asc())

    return req_default


def get_obj_message(i):
    if i.get('num_week', None) is not None:
        key = i['num_week']
    elif i.get('date', None) is not None:
        key = datetime.datetime.fromtimestamp(i['date'])
    else:
        key = datetime.datetime.fromtimestamp(i['date_hour']).strftime('%d-%m-%Y %H:%M')

    obj = {
        'key': key,
        'count': i['count'],
        'duration': i['duration']
    }

    return obj


@acced_message_blueprint.route('/history/detail/<string:num_default>/export', methods=['POST'])
def get_detail_messages_history_export(num_default):
    args = json.loads(request.data)
    req = _getDetailRequest(num_default=num_default, args=args)
    nb_weeks = weeks_between(args['end_date'], args['start_date'])
    res = getList(request=req, start=None, length=None, sort_by=None, group_by=args['groupby'],
                  nb_weeks=nb_weeks)

    headers = args['headers']
    custom_lines = args['custom_lines']
    name = os.sep.join(
        [os.getcwd(), "tmp_export", "%s.xlsx" % uuid.uuid4()])

    generate_excel_file(name=name, headers=headers, items=res[1],
                        custom_lines=custom_lines,
                        transform_function=get_obj_message)

    return send_file(name, as_attachment=True)


SCORE_OK = 1
SCORE_WARNING = 2
SCORE_ALERT = 3
SCORE_ERROR = 4


def frequency_score(nb_defects, nb_weeks):
    score = SCORE_OK
    moy_defects = nb_defects / nb_weeks

    if 1 <= moy_defects <= 5:
        score = SCORE_WARNING
    elif 5 < moy_defects <= 10:
        score = SCORE_ALERT
    elif moy_defects > 10:
        score = SCORE_ERROR

    return score


def duration_score(median):
    score = SCORE_OK
    if (5 * 60) < median <= (15 * 60):
        score = SCORE_WARNING
    elif (15 * 60) < median <= (60 * 60):
        score = SCORE_ALERT
    elif median > (60 * 60):
        score = SCORE_ERROR

    return score


def seriousness_score(level):
    score = SCORE_OK

    if level == 2:
        score = SCORE_WARNING
    elif level == 3:
        score = SCORE_ALERT

    return score


@acced_message_blueprint.route('/history/detail/<string:num_default>', methods=['POST'])
def get_detail_messages_history(num_default):
    args = json.loads(request.data)

    # defect = DefDefaut.select(DefDefaut.duration).where(DefDefaut.num_defaut == num_default).get()
    # min_duration = int(args.get('duration', MIN_DURATION))

    v_group_by = args['groupby']  # 0 : jour / 1 : semaine / -1 : heure
    nb_weeks = weeks_between(args['end_date'], args['start_date'])
    req_default = _getDetailRequest(num_default=num_default, args=args)
    res = []
    old_week = None
    default = None
    durations = []
    lvl = 0
    for item in req_default:
        min_duration = item.defdefaut.duration if item.defdefaut.duration is not None else 0
        lvl = item.defdefaut.niveau if hasattr(item, 'defdefaut') else 0
        if v_group_by == 1:
            num_week = item.chrono.isocalendar().week
        elif v_group_by == 0:
            num_week = datetime.datetime.combine(item.chrono.date(), datetime.time.min).timestamp()
        else:
            num_week = datetime.datetime(year=item.chrono.year,
                                         month=item.chrono.month,
                                         day=item.chrono.day,
                                         hour=item.chrono.hour).timestamp()

        if num_week != old_week and item.valeur == 1:
            default = {
                'numWeek': num_week,
                'count': 0,
                'duration': 0,
                'start': None
            }
            res.append(default)

        if item.valeur == 1:
            default['start'] = item.chrono
        else:
            if default is not None and default['start'] is not None:
                message_duration = (item.chrono - default['start']).total_seconds()
                if message_duration > min_duration:
                    default['count'] = default['count'] + 1
                    default['duration'] = default['duration'] + message_duration
                    durations.append(message_duration)
                default['start'] = None

        if item.valeur == 1:
            old_week = num_week

    weeks = np.array(list(map(lambda r: r['numWeek'], res)))
    counts = np.array(list(map(lambda r: r['count'], res)))

    count_coeff = estimate_coef(weeks, counts)
    count_r2 = lineary_regression(count_coeff, weeks, counts) if count_coeff[0] is not None and count_coeff[
        1] is not None else None

    duration = np.array(list(map(lambda r: r['duration'], res)))

    duration_coeff = estimate_coef(weeks, duration)
    duration_r2 = lineary_regression(duration_coeff, weeks, duration) \
        if duration_coeff[0] is not None and duration_coeff[1] is not None \
        else None

    duration_median = np.mean(durations)
    q = np.quantile(durations, 0.75)
    score = {
        'median_duration': duration_median,
        'min_duration': min(durations),
        'max_duration': max(durations),
        'quantile': q,
        'frequency': frequency_score(nb_defects=sum(counts), nb_weeks=nb_weeks),
        'duration': duration_score(median=duration_median),
        'seriousness': seriousness_score(level=lvl),
    }

    return {'list': res,
            'score': score,
            'countr2': count_r2,
            'durationr2': duration_r2,
            'counttrends': (count_coeff[0] / abs(count_coeff[1])) * 100.0
            if count_r2 is not None and count_r2 > 0.65 else None,
            'durationtrends': (duration_coeff[0] / abs(duration_coeff[1])) * 100.0
            if duration_r2 is not None and duration_r2 > 0.65 else None}


def _get_message_request(mag):
    subquery = LogDefaut.select(
        LogDefaut.num_defaut.alias('num_defaut'), fn.MAX(LogDefaut.pk).alias('max_pk')) \
        .where(LogDefaut.poste == mag) \
        .group_by(LogDefaut.num_defaut)

    return (LogDefaut
            .select(LogDefaut.chrono, LogDefaut.num_defaut, LogDefaut.valeur,
                    LogDefaut.message, DefDefaut.niveau, DefDefaut.defaut)
            .join(subquery,
                  on=(subquery.c.num_defaut == LogDefaut.num_defaut) & (subquery.c.max_pk == LogDefaut.pk))
            .switch(LogDefaut)
            .join(Magasin, on=Magasin.type_mag == LogDefaut.poste)
            .switch(LogDefaut)
            .join(DefDefaut, JOIN.LEFT_OUTER, on=(DefDefaut.num_defaut == LogDefaut.num_defaut) &
                                                 (DefDefaut.type_machine == Magasin.type_machine))

            .where((LogDefaut.poste == mag) & (LogDefaut.valeur != 0))
            .order_by(LogDefaut.chrono.desc()))


@acced_message_blueprint.route('<string:mag>', methods=['GET'])
@jwt_required()
def getMessage(mag):
    try:
        req = _get_message_request(mag)
        list = []
        for msg in req:
            obj = {
                "date": msg.chrono,
                "num_default": msg.num_defaut,
                "label": msg.message,
                "lvl": msg.defdefaut.niveau if hasattr(msg, 'defdefaut') else None,
                "default": msg.defdefaut.defaut if hasattr(msg, 'defdefaut') else None
            }
            list.append(obj)

        return {"data": list}, HTTP_200_OK

    except Exception as error:
        logger.error(error.args)
        return {'message': error.args}, HTTP_400_BAD_REQUEST
