# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# @author Guewen Baconnier <guewen.baconnier@camptocamp.com>
# @author Simone Orsi <simahawk@gmail.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import logging
from datetime import datetime, timedelta

from odoo import api, fields, models, tools

_logger = logging.getLogger(__name__)


class RESTLog(models.Model):
    _name = "rest.log"
    _description = "REST API Logging"
    _order = "id desc"

    DEFAULT_RETENTION = 30  # days
    EXCEPTION_SEVERITY_MAPPING = {
        "odoo.exceptions.UserError": "functional",
        "odoo.exceptions.ValidationError": "functional",
        # something broken somewhere
        "ValueError": "severe",
        "AttributeError": "severe",
        "UnboundLocalError": "severe",
    }

    collection = fields.Char(index=True)
    collection_id = fields.Integer(index=True, string="Collection ID")
    request_url = fields.Char(readonly=True, string="Request URL")
    request_method = fields.Char(readonly=True)
    params = fields.Text(readonly=True)
    # TODO: make these fields serialized and use a computed field for displaying
    headers = fields.Text(readonly=True)
    result = fields.Text(readonly=True)
    error = fields.Text(readonly=True)
    exception_name = fields.Char(readonly=True, string="Exception")
    exception_message = fields.Text(readonly=True)
    state = fields.Selection(
        selection=[("success", "Success"), ("failed", "Failed")], readonly=True
    )
    severity = fields.Selection(
        selection=[
            ("functional", "Functional"),
            ("warning", "Warning"),
            ("severe", "Severe"),
        ],
        compute="_compute_severity",
        store=True,
        # Grant specific override services' dispatch_exception override
        # or via UI: user can classify errors as preferred on demand
        # (maybe using mass_edit)
        readonly=False,
    )

    @api.depends("state", "exception_name", "error")
    def _compute_severity(self):
        for rec in self:
            rec.severity = rec.severity or rec._get_severity()

    def _get_severity(self):
        if not self.exception_name:
            return False
        mapping = self._get_exception_severity_mapping()
        return mapping.get(self.exception_name, "warning")

    def _get_exception_severity_mapping_param(self):
        param = (
            self.env["ir.config_parameter"]
            .sudo()
            .get_param("rest.log.severity.exception.mapping")
        )
        return param.strip() if param else ""

    @tools.ormcache("self._get_exception_severity_mapping_param()")
    def _get_exception_severity_mapping(self):
        mapping = self.EXCEPTION_SEVERITY_MAPPING.copy()
        param = self._get_exception_severity_mapping_param()
        if not param:
            return mapping
        # param should be in the form
        # `[module.dotted.path.]ExceptionName:severity,ExceptionName:severity`
        for rule in param.split(","):
            if not rule.strip():
                continue
            exc_name = severity = None
            try:
                exc_name, severity = [x.strip() for x in rule.split(":")]
                if not exc_name or not severity:
                    raise ValueError
            except ValueError:
                _logger.info(
                    "Could not convert System Parameter"
                    " 'rest.log.severity.exception.mapping' to mapping."
                    " The following rule will be ignored: %s",
                    rule,
                )
            if exc_name and severity:
                mapping[exc_name] = severity
        return mapping

    def _logs_retention_days(self):
        retention = self.DEFAULT_RETENTION
        param = (
            self.env["ir.config_parameter"].sudo().get_param("rest.log.retention.days")
        )
        if param:
            try:
                retention = int(param)
            except ValueError:
                _logger.exception(
                    "Could not convert System Parameter"
                    " 'rest.log.retention.days' to integer,"
                    " reverting to the"
                    " default configuration."
                )
        return retention

    def logging_active(self):
        retention = self._logs_retention_days()
        return retention > 0

    def autovacuum(self):
        """Delete logs which have exceeded their retention duration

        Called from a cron.
        """
        deadline = datetime.now() - timedelta(days=self._logs_retention_days())
        logs = self.search([("create_date", "<=", deadline)])
        if logs:
            logs.unlink()
        return True

    def _get_log_active_param(self):
        param = self.env["ir.config_parameter"].sudo().get_param("rest.log.active")
        return param.strip() if param else ""

    @tools.ormcache("self._get_log_active_param()")
    def _get_log_active_conf(self):
        """Compute log active configuration.

        Possible configuration contains a CSV like this:

            `collection_name` -> enable for all endpoints of the collection
            `collection_name.usage` -> enable for specific endpoints
            `collection_name.usage.endpoint` -> enable for specific endpoints
            `collection_name*:state` -> enable only for specific state (success, failed)

        By default matching keys are enabled for all states.

        :return: mapping by matching key / enabled states
        """
        param = self._get_log_active_param()
        conf = {}
        lines = [x.strip() for x in param.split(",") if x.strip()]
        for line in lines:
            bits = [x.strip() for x in line.split(":") if x.strip()]
            if len(bits) > 1:
                match_key = bits[0]
                # fmt: off
                states = (bits[1], )
                # fmt: on
            else:
                match_key = line
                states = ("success", "failed")
            conf[match_key] = states
        return conf

    @api.model
    def _get_matching_active_conf(self, collection, usage, method_name):
        """Retrieve conf matching current service and method."""
        conf = self._get_log_active_conf()
        candidates = (
            collection + "." + usage + "." + method_name,
            collection + "." + usage,
            collection,
        )
        for candidate in candidates:
            if conf.get(candidate):
                return conf.get(candidate)

    def action_view_collection(self):
        """Open collection if we have a real record.

        NOTE: use an action instead of a `Reference` field with computed method
        because it would force use to have glue modules to provide a selection
        for every model we want to support.
        """
        # TODO: on the next round, compute the collection name
        # to be used for the button label.
        # No ID or no real model
        if self.collection not in self.env or not self.collection_id:
            return
        action = self.env[self.collection].get_formview_action()
        action["res_id"] = self.collection_id
        return action
