# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime as dt
import functools
import logging
import warnings

import trytond.config as config
from trytond.exceptions import UserError, UserWarning
from trytond.model import (
    Index, ModelSQL, ModelView, Workflow, dualmethod, fields)
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.tools import firstline
from trytond.transaction import Transaction

logger = logging.getLogger(__name__)


def set_user(field):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(cls, records, *args, **kwargs):
            result = func(cls, records, *args, **kwargs)
            cls.write(
                [r for r in records
                    if not getattr(r, field)], {
                    field: Transaction().user,
                    })
            return result
        return wrapper
    return decorator


def reset_user(*fields):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(cls, records, *args, **kwargs):
            result = func(cls, records, *args, **kwargs)
            cls.write(records, {f: None for f in fields})
            return result
        return wrapper
    return decorator


class Error(Workflow, ModelView, ModelSQL):
    __name__ = 'ir.error'

    origin = fields.Reference("Origin", [
            ('ir.cron', "Action"),
            ('ir.queue', "Task"),
            ], readonly=True)
    origin_string = origin.translated('origin')
    message = fields.Text("Message", readonly=True)
    description = fields.Text("Description", readonly=True)
    summary = fields.Function(
        fields.Char("Summary"), 'on_change_with_summary',
        searcher='search_summary')

    processed_by = fields.Many2One(
        'res.user', "Processed by",
        states={
            'readonly': Eval('state').in_(['processing', 'solved']),
            },
        depends=['state'])
    solved_by = fields.Many2One(
        'res.user', "Solved by",
        states={
            'readonly': Eval('state').in_(['solved']),
            },
        depends=['state'])

    state = fields.Selection([
            ('open', "Open"),
            ('processing', "Processing"),
            ('solved', "Solved"),
            ], "State", readonly=True, sort=False)

    @classmethod
    def __setup__(cls):
        super().__setup__()
        table = cls.__table__()

        cls._sql_indexes.add(
            Index(
                table,
                (table.state, Index.Equality(cardinality='low')),
                where=table.state.in_(['open', 'processing'])))
        cls._order = [
            ('create_date', 'DESC'),
            ('id', 'DESC'),
            ]
        cls._transitions |= {
            ('open', 'processing'),
            ('processing', 'solved'),
            ('processing', 'open'),
            }
        cls._buttons.update({
                'open': {
                    'invisible': Eval('state') != 'processing',
                    'depends': ['state'],
                    },
                'process': {
                    'invisible': Eval('state') != 'open',
                    'depends': ['state'],
                    },
                'solve': {
                    'invisible': Eval('state') != 'processing',
                    'depends': ['state'],
                    },
                })

    @classmethod
    def default_state(cls):
        return 'open'

    @fields.depends('message')
    def on_change_with_summary(self, name=None):
        return firstline(self.message or '')

    @classmethod
    def search_summary(cls, name, clause):
        return [('message', *clause[1:])]

    def get_rec_name(self, name):
        if self.origin:
            return "%s - %s" % (self.origin_string, self.origin.rec_name)
        return super().get_rec_name(name)

    @dualmethod
    def log(cls, *args, **kwargs):
        # Test if it is a ModelStorage.log call
        if len(args) <= 1 or not isinstance(args[1], Exception):
            return super().log(*args, **kwargs)
        warnings.warn(
            "Call report instead of log to store exception",
            DeprecationWarning)
        cls.report(*args, **kwargs)

    @classmethod
    def report(cls, origin, exception):
        try:
            assert isinstance(exception, (UserError, UserWarning))
            with Transaction().new_transaction(autocommit=True):
                if not cls.search([
                            ('origin', '=', str(origin)),
                            ('message', '=', exception.message),
                            ('description', '=', exception.description),
                            ('state', '!=', 'solved'),
                            ]):
                    cls.create([{
                                'origin': str(origin),
                                'message': exception.message,
                                'description': exception.description,
                                }])
        except Exception:
            logger.critical(
                "failed to store exception %s of %s", exception, origin,
                exc_info=True)

    @classmethod
    def clean(cls, date=None):
        if date is None:
            clean_days = config.getint('error', 'clean_days', default=90)
            date = (
                dt.datetime.now() - dt.timedelta(days=clean_days))
        errors = cls.search([('create_date', '<', date)])
        cls.delete(errors)

    @classmethod
    @ModelView.button
    @Workflow.transition('open')
    @reset_user('processed_by')
    def open(cls, errors):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('processing')
    @set_user('processed_by')
    def process(cls, errors):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('solved')
    @set_user('solved_by')
    def solve(cls, errors):
        pool = Pool()
        Cron = pool.get('ir.cron')
        Queue = pool.get('ir.queue')
        for error in errors:
            if isinstance(error.origin, Cron):
                Cron.__queue__.run_once([error.origin])
            elif isinstance(error.origin, Queue):
                task = error.origin
                Queue.push(task.name, task.data)
