# Copyright 2015-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from collections import defaultdict

from odoo import api, models, registry

NO_BI_MODELS = [
    'fetchmail.server'
]

NO_BI_TTYPES = [
    'many2many',
    'one2many',
    'html',
    'binary',
    'reference'
]


def dict_for_field(field):
    return {
        'id': field.id,
        'name': field.name,
        'description': field.field_description,
        'type': field.ttype,
        'relation': field.relation,
        'custom': False,
        'model_id': field.model_id.id,
        'model': field.model_id.model,
        'model_name': field.model_id.name
    }


def dict_for_model(model):
    return {
        'id': model.id,
        'name': model.name,
        'model': model.model
    }


class IrModel(models.Model):
    _inherit = 'ir.model'

    @api.model
    def _filter_bi_models(self, model):

        def _check_name(model_model):
            if model_model in NO_BI_MODELS:
                return 1
            return 0

        def _check_startswith(model_model):
            if model_model.startswith('workflow') or \
                    model_model.startswith('ir.') or \
                    model_model.startswith('base_'):
                return 1
            return 0

        def _check_contains(model_model):
            if 'mail' in model_model or \
                    'report' in model_model or \
                    'edi.' in model_model:
                return 1
            return 0

        def _check_unknown(model_name):
            if model_name == 'Unknown' or '.' in model_name:
                return 1
            return 0

        model_model = model['model']
        model_name = model['name']
        count_check = 0
        count_check += _check_name(model_model)
        count_check += _check_startswith(model_model)
        count_check += _check_contains(model_model)
        count_check += _check_unknown(model_name)
        if not count_check:
            return self.env['ir.model.access'].check(
                model['model'], 'read', False)
        return False

    def get_model_list(self, model_table_map):
        if not model_table_map:
            return []
        domain = [('model_id', 'in', list(model_table_map.keys())),
                  ('store', '=', True),
                  ('ttype', '=', 'many2one')]
        fields = self.env['ir.model.fields'].sudo().search(domain)
        model_list = []
        for field in fields:
            for table_alias in model_table_map[field.model_id.id]:
                model_list.append(dict(
                    dict_for_field(field),
                    table_alias=table_alias,
                    join_node=-1,
                ))
        return model_list

    def get_relation_list(self, model_table_map):
        if not model_table_map:
            return []
        model_names = {}
        for model in self.sudo().browse(model_table_map.keys()):
            model_names.update({model.model: model.id})

        domain = [('relation', 'in', list(model_names.keys())),
                  ('store', '=', True),
                  ('ttype', '=', 'many2one')]
        fields = self.env['ir.model.fields'].sudo().search(domain)
        relation_list = []
        for field in fields:
            model_id = model_names[field.relation]
            for join_node in model_table_map[model_id]:
                relation_list.append(dict(
                    dict_for_field(field),
                    join_node=join_node,
                    table_alias=-1
                ))
        return relation_list

    @api.model
    def _get_related_models_domain(self, model_table_map):
        domain = [('transient', '=', False)]
        if model_table_map:
            model_list = self.get_model_list(model_table_map)
            relation_list = self.get_relation_list(model_table_map)
            model_ids = [f['model_id'] for f in relation_list + model_list]
            model_ids += list(model_table_map.keys())
            relations = [f['relation'] for f in model_list]
            domain += [
                '|', ('id', 'in', model_ids), ('model', 'in', relations)]
        return domain

    @api.model
    def get_related_models(self, model_table_map):
        """ Return list of model dicts for all models that can be
            joined with the already selected models.
        """
        domain = self._get_related_models_domain(model_table_map)
        return self.sudo().search(domain, order='name asc')

    @api.model
    def get_models(self, table_model_map=None):
        """ Return list of model dicts for all available models.
        """
        self = self.with_context(lang=self.env.user.lang)
        model_table_map = defaultdict(list)
        for k, v in (table_model_map or {}).items():
            model_table_map[v].append(k)

        models = self.get_related_models(model_table_map)

        # filter out abstract models (they do not have DB tables)
        non_abstract_models = self.env.registry.models.keys()
        models = models.filtered(lambda m: m.model in non_abstract_models)

        return list(map(dict_for_model, models))

    @api.model
    def get_join_nodes(self, field_data, new_field):
        """ Return list of field dicts of join nodes

            Return all possible join nodes to add new_field to the query
            containing model_ids.
        """
        def remove_duplicate_nodes(join_nodes):
            seen = set()
            nodes_list = []
            for node in join_nodes:
                node_tuple = tuple(node.items())
                if node_tuple not in seen:
                    seen.add(node_tuple)
                    nodes_list.append(node)
            return nodes_list

        self = self.with_context(lang=self.env.user.lang)

        keys = []
        model_table_map = defaultdict(list)
        for field in field_data:
            model_table_map[field['model_id']].append(field['table_alias'])
            if field.get('join_node', -1) != -1:
                keys.append((field['table_alias'], field['id']))

        # nodes in current model
        existing_aliases = model_table_map[new_field['model_id']]
        join_nodes = [{'table_alias': alias} for alias in existing_aliases]

        # nodes in past selected models
        for field in self.get_model_list(model_table_map):
            if new_field['model'] == field['relation']:
                if (field['table_alias'], field['id']) not in keys:
                    join_nodes.append(field)

        # nodes in new model
        for field in self.get_relation_list(model_table_map):
            if new_field['model_id'] == field['model_id']:
                if (field['table_alias'], field['id']) not in keys:
                    join_nodes.append(field)

        return remove_duplicate_nodes(join_nodes)

    @api.model
    def get_fields(self, model_id):
        self = self.with_context(lang=self.env.user.lang)

        fields = self.env['ir.model.fields'].sudo().search([
            ('model_id', '=', model_id),
            ('store', '=', True),
            ('name', 'not in', models.MAGIC_COLUMNS),
            ('ttype', 'not in', NO_BI_TTYPES)
        ], order='field_description desc')
        fields_dict = list(map(dict_for_field, fields))
        return fields_dict

    @api.model
    def create(self, vals):
        if self.env.context and self.env.context.get('bve'):
            vals['state'] = 'base'
        res = super().create(vals)

        # this sql update is necessary since a write method here would
        # be not working (an orm constraint is restricting the modification
        # of the state field while updating ir.model)
        q = "UPDATE ir_model SET state = 'manual' WHERE id = %s"
        self.env.cr.execute(q, (res.id, ))

        # # update registry
        if self.env.context.get('bve'):
            # setup models; this reloads custom models in registry
            self.pool.setup_models(self._cr)

            # signal that registry has changed
            registry(self.env.cr.dbname).signal_changes()

        return res