# © 2010-2013 OpenERP s.a. (<http://openerp.com>).
# © 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# © 2015-Today GRAP
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html

from collections import OrderedDict
from statistics import median

from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import ValidationError, except_orm
from odoo.tools.safe_eval import safe_eval
from odoo.tools.translate import _

FIELD_FUNCTIONS = OrderedDict(
    [
        (
            "count",
            {
                "name": "Count",
                "func": False,  # its hardcoded in _compute_data
                "help": _("Number of records"),
            },
        ),
        (
            "min",
            {
                "name": "Minimum",
                "func": min,
                "help": _("Minimum value of '%s'"),
            },
        ),
        (
            "max",
            {
                "name": "Maximum",
                "func": max,
                "help": _("Maximum value of '%s'"),
            },
        ),
        (
            "sum",
            {"name": "Sum", "func": sum, "help": _("Total value of '%s'")},
        ),
        (
            "avg",
            {
                "name": "Average",
                "func": lambda vals: sum(vals) / len(vals),
                "help": _("Average value of '%s'"),
            },
        ),
        (
            "median",
            {
                "name": "Median",
                "func": median,
                "help": _("Median value of '%s'"),
            },
        ),
    ]
)


FIELD_FUNCTION_SELECTION = [
    (k, FIELD_FUNCTIONS[k].get("name")) for k in FIELD_FUNCTIONS
]


class TileTile(models.Model):
    _name = "tile.tile"
    _description = "Dashboard Tile"
    _order = "sequence, name"

    # Column Section
    name = fields.Char(required=True)

    sequence = fields.Integer(default=0, required=True)

    category_id = fields.Many2one(
        string="Category",
        comodel_name="tile.category",
        required=True,
        ondelete="CASCADE",
    )

    user_id = fields.Many2one(string="User", comodel_name="res.users")

    background_color = fields.Char(default="#0E6C7E")

    font_color = fields.Char(default="#FFFFFF")

    group_ids = fields.Many2many(
        comodel_name="res.groups",
        string="Groups",
        help="If this field is set, only users of this group can view this "
        "tile. Please note that it will only work for global tiles "
        "(that is, when User field is left empty)",
    )

    model_id = fields.Many2one(
        comodel_name="ir.model", string="Model", required=True, ondelete="cascade"
    )

    model_name = fields.Char(string="Model name", related="model_id.model")

    domain = fields.Text(default="[]", required=True)

    domain_error = fields.Char(compute="_compute_data")

    action_id = fields.Many2one(
        comodel_name="ir.actions.act_window",
        string="Action",
        help="Let empty to use the default action related to" " the selected model.",
        domain="[('res_model', '=', model_name)]",
    )

    active = fields.Boolean(
        compute="_compute_active", search="_search_active", readonly=True
    )

    hide_if_null = fields.Boolean(
        string="Hide if null",
        help="If checked, the item will be hidden" " if the primary value is null.",
    )

    hidden = fields.Boolean(compute="_compute_data", search="_search_hidden")

    # Primary Value
    primary_function = fields.Selection(
        required=True,
        selection=FIELD_FUNCTION_SELECTION,
        default="count",
    )

    primary_field_id = fields.Many2one(
        comodel_name="ir.model.fields",
        string="Primary Field",
        domain="[('model_id', '=', model_id),"
        " ('ttype', 'in', ['float', 'integer', 'monetary'])]",
    )

    primary_format = fields.Char(
        help="Python Format String valid with str.format()\n"
        "ie: '{:,} Kgs' will output '1,000 Kgs' if value is 1000.",
    )

    primary_value = fields.Float(compute="_compute_data")

    primary_formated_value = fields.Char(compute="_compute_data")

    primary_helper = fields.Char(compute="_compute_helper", store=True)

    primary_error = fields.Char(compute="_compute_data")

    # Secondary Value
    secondary_function = fields.Selection(
        selection=FIELD_FUNCTION_SELECTION,
    )

    secondary_field_id = fields.Many2one(
        comodel_name="ir.model.fields",
        string="Secondary Field",
        domain="[('model_id', '=', model_id),"
        " ('ttype', 'in', ['float', 'integer', 'monetary'])]",
    )

    secondary_format = fields.Char(
        help="Python Format String valid with str.format()\n"
        "ie: '{:,} Kgs' will output '1,000 Kgs' if value is 1000.",
    )

    secondary_value = fields.Float(compute="_compute_data")

    secondary_formated_value = fields.Char(compute="_compute_data")

    secondary_helper = fields.Char(compute="_compute_helper", store=True)

    secondary_error = fields.Char(compute="_compute_data")

    # Compute Section
    @api.depends(
        "model_id",
        "domain",
        "primary_format",
        "primary_function",
        "primary_field_id",
        "secondary_format",
        "secondary_function",
        "secondary_field_id",
    )
    def _compute_data(self):
        for tile in self:
            # initialize all vals
            tile.hidden = False
            tile.primary_value = False
            tile.primary_formated_value = False
            tile.secondary_value = False
            tile.secondary_formated_value = False
            tile.domain_error = False
            tile.primary_error = False
            tile.secondary_error = False
            if not tile.model_id or not tile.active:
                return

            model = self.env[tile.model_id.model]
            eval_context = self._get_eval_context()
            domain = tile.domain or "[]"
            try:
                count = model.search_count(safe_eval(domain, eval_context))
            except Exception as e:
                tile.primary_formated_value = tile.secondary_formated_value = _(
                    "Domain Error"
                )
                tile.domain_error = str(e)
                return
            fields = [
                f.name for f in [tile.primary_field_id, tile.secondary_field_id] if f
            ]
            read_vals = (
                fields
                and model.search_read(safe_eval(domain, eval_context), fields)
                or []
            )
            for f in ["primary_", "secondary_"]:
                f_function = f + "function"
                f_field_id = f + "field_id"
                f_format = f + "format"
                f_value = f + "value"
                f_formated_value = f + "formated_value"
                f_error = f + "error"

                if not tile[f_function]:
                    continue
                elif tile[f_function] == "count":
                    value = count
                else:
                    func = FIELD_FUNCTIONS[tile[f_function]]["func"]
                    vals = [x[tile[f_field_id].name] for x in read_vals]
                    value = func(vals or [0.0])
                tile[f_value] = value

                try:
                    tile[f_formated_value] = (tile[f_format] or "{:,}").format(value)
                except ValueError as e:
                    tile[f_formated_value] = _("Error")
                    tile[f_error] = str(e)

            tile.hidden = tile.hide_if_null and not tile.primary_value

    @api.depends(
        "primary_function",
        "primary_field_id",
        "secondary_function",
        "secondary_field_id",
    )
    def _compute_helper(self):
        for tile in self:
            for f in ["primary_", "secondary_"]:
                f_function = f + "function"
                f_field_id = f + "field_id"
                f_helper = f + "helper"
                tile[f_helper] = ""
                field_func = FIELD_FUNCTIONS.get(tile[f_function], {})
                help_text = field_func.get("help", False)
                if help_text and tile[f_function] != "count" and tile[f_field_id]:
                    tile[f_helper] = help_text % tile[f_field_id].field_description
                else:
                    tile[f_helper] = help_text

    def _compute_active(self):
        IrModelAccess = self.env["ir.model.access"]
        for tile in self:
            if tile.model_id:
                tile.active = IrModelAccess.check(tile.model_id.model, "read", False)
            else:
                tile.active = True

    # Search Sections
    def _search_hidden(self, operator, operand):
        items = self.search([])
        hidden_tile_ids = [x.id for x in items if x.hidden]
        if (operator == "=" and operand is False) or (
            operator == "!=" and operand is True
        ):
            domain = [("id", "not in", hidden_tile_ids)]
        else:
            domain = [("id", "in", hidden_tile_ids)]
        return domain

    def _search_active(self, operator, value):
        cr = self.env.cr
        if operator != "=":
            raise except_orm(
                _("Unimplemented Feature. Search on Active field disabled.")
            )
        IrModelAccess = self.env["ir.model.access"]
        ids = []
        cr.execute(
            """
            SELECT tt.id, im.model
            FROM tile_tile tt
            INNER JOIN ir_model im
                ON tt.model_id = im.id"""
        )
        for result in cr.fetchall():
            if IrModelAccess.check(result[1], "read", False) == value:
                ids.append(result[0])
        return [("id", "in", ids)]

    # Constraints Sections
    @api.constrains("model_id", "primary_field_id", "secondary_field_id")
    def _check_model_id_field_id(self):
        for tile in self:
            if any(
                [
                    tile.primary_field_id
                    and tile.primary_field_id.model_id.id != tile.model_id.id,
                    tile.secondary_field_id
                    and tile.secondary_field_id.model_id.id != tile.model_id.id,
                ]
            ):
                raise ValidationError(
                    _("Please select a field from the selected model.")
                )

    # Onchange Sections
    @api.onchange("model_id")
    def _onchange_model_id(self):
        self.primary_field_id = False
        self.secondary_field_id = False
        self.action_id = False

    @api.onchange("primary_function", "secondary_function")
    def _onchange_function(self):
        if self.primary_function in [False, "count"]:
            self.primary_field_id = False
        if self.secondary_function in [False, "count"]:
            self.secondary_field_id = False

    # Action methods
    def open_link(self):
        if self.action_id:
            action = self.action_id.read()[0]
        else:
            action = {
                "view_mode": "tree",
                "view_id": False,
                "res_model": self.model_id.model,
                "type": "ir.actions.act_window",
                "target": "current",
                "domain": self.domain,
            }
        action.update(
            {
                "name": self.name,
                "display_name": self.name,
                "context": dict(self.env.context, group_by=False),
                "domain": self.domain,
            }
        )
        return action

    @api.model
    def _get_eval_context(self):
        context = self.env.context.copy()
        context.update(
            {
                "relativedelta": relativedelta,
                "context_today": fields.Date.from_string(
                    fields.Date.context_today(self)
                ),
                "current_date": fields.Date.today(),
            }
        )
        return context