# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import defaultdict
from markupsafe import Markup

from odoo import api, fields, models, tools, _
from odoo.addons.phone_validation.tools import phone_validation


class EventRegistration(models.Model):
    _inherit = 'event.registration'

    lead_ids = fields.Many2many(
        'crm.lead', string='Leads', copy=False, readonly=True,
        groups='sales_team.group_sale_salesman')
    lead_count = fields.Integer(
        '# Leads', compute='_compute_lead_count', compute_sudo=True)

    @api.depends('lead_ids')
    def _compute_lead_count(self):
        for record in self:
            record.lead_count = len(record.lead_ids)

    @api.model_create_multi
    def create(self, vals_list):
        """ Trigger rules based on registration creation, and check state for
        rules based on confirmed / done attendees. """
        registrations = super(EventRegistration, self).create(vals_list)

        # handle triggers based on creation, then those based on confirm and done
        # as registrations can be automatically confirmed, or even created directly
        # with a state given in values
        if not self.env.context.get('event_lead_rule_skip'):
            registrations._apply_lead_generation_rules()
        return registrations

    def write(self, vals):
        """ Update the lead values depending on fields updated in registrations.
        There are 2 main use cases

          * first is when we update the partner_id of multiple registrations. It
            happens when a public user fill its information when they register to
            an event;
          * second is when we update specific values of one registration like
            updating question answers or a contact information (email, phone);

        Also trigger rules based on confirmed and done attendees (state written
        to open and done).
        """
        to_update, event_lead_rule_skip = False, self.env.context.get('event_lead_rule_skip')
        if not event_lead_rule_skip:
            to_update = self.filtered(lambda reg: reg.lead_count)
        if to_update:
            lead_tracked_vals = to_update._get_lead_tracked_values()

        res = super(EventRegistration, self).write(vals)

        if not event_lead_rule_skip and to_update:
            self.env.flush_all()  # compute notably partner-based fields if necessary
            to_update.sudo()._update_leads(vals, lead_tracked_vals)

        # handle triggers based on state
        if not event_lead_rule_skip:
            if vals.get('state') == 'open':
                self.env['event.lead.rule'].search([('lead_creation_trigger', '=', 'confirm')]).sudo()._run_on_registrations(self)
            elif vals.get('state') == 'done':
                self.env['event.lead.rule'].search([('lead_creation_trigger', '=', 'done')]).sudo()._run_on_registrations(self)

        return res

    def _load_records_create(self, values):
        """ In import mode: do not run rules those are intended to run when customers
        buy tickets, not when bootstrapping a database. """
        return super(EventRegistration, self.with_context(event_lead_rule_skip=True))._load_records_create(values)

    def _load_records_write(self, values):
        """ In import mode: do not run rules those are intended to run when customers
        buy tickets, not when bootstrapping a database. """
        return super(EventRegistration, self.with_context(event_lead_rule_skip=True))._load_records_write(values)

    def _apply_lead_generation_rules(self):
        leads = self.env['crm.lead']
        open_registrations = self.filtered(lambda reg: reg.state == 'open')
        done_registrations = self.filtered(lambda reg: reg.state == 'done')

        leads += self.env['event.lead.rule'].search(
            [('lead_creation_trigger', '=', 'create')]
        ).sudo()._run_on_registrations(self)
        if open_registrations:
            leads += self.env['event.lead.rule'].search(
                [('lead_creation_trigger', '=', 'confirm')]
            ).sudo()._run_on_registrations(open_registrations)
        if done_registrations:
            leads += self.env['event.lead.rule'].search(
                [('lead_creation_trigger', '=', 'done')]
            ).sudo()._run_on_registrations(done_registrations)
        return leads

    def _update_leads(self, new_vals, lead_tracked_vals):
        """ Update leads linked to some registrations. Update is based depending
        on updated fields, see ``_get_lead_contact_fields()`` and ``_get_lead_
        description_fields()``. Main heuristic is

          * check attendee-based leads, for each registration recompute contact
            information if necessary (changing partner triggers the whole contact
            computation); update description if necessary;
          * check order-based leads, for each existing group-based lead, only
            partner change triggers a contact and description update. We consider
            that group-based rule works mainly with the main contact and less
            with further details of registrations. Those can be found in stat
            button if necessary.

        :param new_vals: values given to write. Used to determine updated fields;
        :param lead_tracked_vals: dict(registration_id, registration previous values)
          based on new_vals;
        """
        for registration in self:
            leads_attendee = registration.lead_ids.filtered(
                lambda lead: lead.event_lead_rule_id.lead_creation_basis == 'attendee'
            )
            if not leads_attendee:
                continue

            old_vals = lead_tracked_vals[registration.id]
            # if partner has been updated -> update registration contact information
            # as they are computed (and therefore not given to write values)
            if 'partner_id' in new_vals:
                new_vals.update(**dict(
                    (field, registration[field])
                    for field in self._get_lead_contact_fields()
                    if field != 'partner_id')
                )

            lead_values = {}
            # update contact fields: valid for all leads of registration
            upd_contact_fields = [field for field in self._get_lead_contact_fields() if field in new_vals.keys()]
            if any(new_vals[field] != old_vals[field] for field in upd_contact_fields):
                lead_values = registration._get_lead_contact_values()

            # update description fields: each lead has to be updated, otherwise
            # update in batch
            upd_description_fields = [field for field in self._get_lead_description_fields() if field in new_vals.keys()]
            if any(new_vals[field] != old_vals[field] for field in upd_description_fields):
                for lead in leads_attendee:
                    lead_values['description'] = "%s<br/>%s" % (
                        lead.description,
                        registration._get_lead_description(_("Updated registrations"), line_counter=True)
                    )
                    lead.write(lead_values)
            elif lead_values:
                leads_attendee.write(lead_values)

        leads_order = self.lead_ids.filtered(lambda lead: lead.event_lead_rule_id.lead_creation_basis == 'order')
        for lead in leads_order:
            lead_values = {}
            if new_vals.get('partner_id'):
                lead_values.update(lead.registration_ids._get_lead_contact_values())
                if not lead.partner_id:
                    lead_values['description'] = lead.registration_ids._get_lead_description(_("Participants"), line_counter=True)
                elif new_vals['partner_id'] != lead.partner_id.id:
                    lead_values['description'] = lead.description + "<br/>" + lead.registration_ids._get_lead_description(_("Updated registrations"), line_counter=True, line_suffix=_("(updated)"))
            if lead_values:
                lead.write(lead_values)

    def _get_lead_values(self, rule):
        """ Get lead values from registrations. Self can contain multiple records
        in which case first found non void value is taken. Note that all
        registrations should belong to the same event.

        :return dict lead_values: values used for create / write on a lead
        """
        sorted_self = self.sorted("id")
        lead_values = {
            # from rule
            'type': rule.lead_type,
            'user_id': rule.lead_user_id.id,
            'team_id': rule.lead_sales_team_id.id,
            'tag_ids': rule.lead_tag_ids.ids,
            'event_lead_rule_id': rule.id,
            # event and registration
            'event_id': self.event_id.id,
            'referred': self.event_id.name,
            'registration_ids': self.ids,
            'campaign_id': sorted_self._find_first_notnull('utm_campaign_id'),
            'source_id': sorted_self._find_first_notnull('utm_source_id'),
            'medium_id': sorted_self._find_first_notnull('utm_medium_id'),
        }
        lead_values.update(sorted_self._get_lead_contact_values())
        lead_values['description'] = sorted_self._get_lead_description(_("Participants"), line_counter=True)
        return lead_values

    def _get_lead_contact_values(self):
        """ Specific management of contact values. Rule creation basis has some
        effect on contact management

          * in attendee mode: keep registration partner only if partner phone and
            email match. Indeed lead are synchronized with their contact and it
            would imply rewriting on partner, and therefore on other documents;
          * in batch mode: if a customer is found use it as main contact. Registrations
            details are included in lead description;

        :return dict: values used for create / write on a lead
        """
        sorted_self = self.sorted("id")
        valid_partner = next(
            (reg.partner_id for reg in sorted_self if reg.partner_id != self.env.ref('base.public_partner')),
            self.env['res.partner']
        )  # CHECKME: broader than just public partner

        # mono registration mode: keep partner only if email and phone matches;
        # otherwise registration > partner. Note that email format and phone
        # formatting have to taken into account in comparison
        if len(self) == 1 and valid_partner:
            # compare emails: email_normalized or raw
            if self.email and valid_partner.email:
                if valid_partner.email_normalized and tools.email_normalize(self.email) != valid_partner.email_normalized:
                    valid_partner = self.env['res.partner']
                elif not valid_partner.email_normalized and valid_partner.email != self.email:
                    valid_partner = self.env['res.partner']

            # compare phone, taking into account formatting
            if valid_partner and self.phone and valid_partner.phone:
                phone_formatted = self._phone_format(fname='phone', country=valid_partner.country_id)
                partner_phone_formatted = valid_partner._phone_format(fname='phone')
                if phone_formatted and partner_phone_formatted and phone_formatted != partner_phone_formatted:
                    valid_partner = self.env['res.partner']
                if (not phone_formatted or not partner_phone_formatted) and self.phone != valid_partner.phone:
                    valid_partner = self.env['res.partner']

        registration_phone = sorted_self._find_first_notnull('phone')
        if valid_partner:
            contact_vals = self.env['crm.lead']._prepare_values_from_partner(valid_partner)
            # force email_from / phone only if not set on partner because those fields are now synchronized automatically
            if not valid_partner.email:
                contact_vals['email_from'] = sorted_self._find_first_notnull('email')
            if not valid_partner.phone:
                contact_vals['phone'] = registration_phone
        else:
            # don't force email_from + partner_id because those fields are now synchronized automatically
            contact_vals = {
                'contact_name': sorted_self._find_first_notnull('name'),
                'email_from': sorted_self._find_first_notnull('email'),
                'phone': registration_phone,
                'lang_id': False,
            }
        contact_vals.update({
            'name': "%s - %s" % (self.event_id.name, valid_partner.name or sorted_self._find_first_notnull('name') or sorted_self._find_first_notnull('email')),
            'partner_id': valid_partner.id,
        })
        # try to avoid copying registration_phone on both phone and mobile fields
        # as would be noise; pay attention partner.hone is propagated through compute
        mobile = valid_partner.mobile or registration_phone
        if mobile != contact_vals.get('phone', valid_partner.phone):
            contact_vals['mobile'] = valid_partner.mobile or registration_phone

        return contact_vals

    def _get_lead_description(self, prefix='', line_counter=True, line_suffix=''):
        """ Build the description for the lead using a prefix for all generated
        lines. For example to enumerate participants or inform of an update in
        the information of a participant.

        :return string description: complete description for a lead taking into
          account all registrations contained in self
        """
        reg_lines = [
            registration._get_lead_description_registration(
                line_suffix=line_suffix
            ) for registration in self
        ]
        description = (prefix if prefix else '') + Markup("<br/>")
        if line_counter:
            description += Markup("<ol>") + Markup('').join(reg_lines) + Markup("</ol>")
        else:
            description += Markup("<ul>") + Markup('').join(reg_lines) + Markup("</ul>")
        return description

    def _get_lead_description_registration(self, line_suffix=''):
        """ Build the description line specific to a given registration. """
        self.ensure_one()
        return Markup("<li>") + "%s (%s)%s" % (
            self.name or self.partner_id.name or self.email,
            " - ".join(self[field] for field in ('email', 'phone') if self[field]),
            f" {line_suffix}" if line_suffix else "",
        ) + Markup("</li>")

    def _get_lead_tracked_values(self):
        """ Tracked values are based on two subset of fields to track in order
        to fill or update leads. Two main use cases are

          * description fields: registration contact fields: email, phone, ...
            on registration. Other fields are added by inheritance like
            question answers;
          * contact fields: registration contact fields + partner_id field as
            contact of a lead is managed specifically. Indeed email and phone
            synchronization of lead / partner_id implies paying attention to
            not rewrite partner values from registration values.

        Tracked values are therefore the union of those two field sets. """
        tracked_fields = list(set(self._get_lead_contact_fields()) | set(self._get_lead_description_fields()))
        return dict(
            (registration.id,
             dict((field, self._convert_value(registration[field], field)) for field in tracked_fields)
            ) for registration in self
        )

    def _get_lead_grouping(self, rules, rule_to_new_regs):
        """ Perform grouping of registrations in order to enable order-based
        lead creation and update existing groups with new registrations.

        Heuristic in event is the following. Registrations created in multi-mode
        are grouped by event and creation_date. Customer use case: website_event
        flow creates several registrations in a create-multi. Cron use case:
        when running a rule on existing registrations, grouping on event only
        is not sufficient, create_date is a safe bet for registration groups.

        Update is not supported as there is no way to determine if a registration
        is part of an existing batch.

        :param rules: lead creation rules to run on registrations given by self;
        :param rule_to_new_regs: dict: for each rule, subset of self matching
          rule conditions. Used to speedup batch computation;

        :return dict: for each rule, rule (key of dict) gives a list of groups.
          Each group is a tuple (
            existing_lead: existing lead to update;
            group_record: record used to group;
            registrations: sub record set of self, containing registrations
                           belonging to the same group;
          )
        """
        grouped_registrations = {
            (create_date, event): sub_registrations
            for event, registrations in self.grouped('event_id').items()
            for create_date, sub_registrations in registrations.grouped('create_date').items()
        }

        return dict(
            (rule, [(False, key, (registrations & rule_to_new_regs[rule]).sorted('id'))
                    for key, registrations in grouped_registrations.items()])
            for rule in rules
        )

    # ------------------------------------------------------------
    # TOOLS
    # ------------------------------------------------------------

    @api.model
    def _get_lead_contact_fields(self):
        """ Get registration fields linked to lead contact. Those are used notably
        to see if an update of lead is necessary or to fill contact values
        in ``_get_lead_contact_values())`` """
        return ['name', 'email', 'phone', 'partner_id']

    @api.model
    def _get_lead_description_fields(self):
        """ Get registration fields linked to lead description. Those are used
        notably to see if an update of lead is necessary or to fill description
        in ``_get_lead_description())`` """
        return ['name', 'email', 'phone']

    def _find_first_notnull(self, field_name):
        """ Small tool to extract the first not nullvalue of a field: its value
        or the ids if this is a relational field. """
        value = next((reg[field_name] for reg in self if reg[field_name]), False)
        return self._convert_value(value, field_name)

    def _convert_value(self, value, field_name):
        """ Small tool because convert_to_write is touchy """
        if isinstance(value, models.BaseModel) and self._fields[field_name].type in ['many2many', 'one2many']:
            return value.ids
        if isinstance(value, models.BaseModel) and self._fields[field_name].type == 'many2one':
            return value.id
        return value
