import base64

from odoo import _, api, fields, models, exceptions

from .card_template import TEMPLATE_DIMENSIONS


class CardCampaign(models.Model):
    _name = 'card.campaign'
    _description = 'Marketing Card Campaign'
    _inherit = ['mail.activity.mixin', 'mail.render.mixin', 'mail.thread']
    _order = 'id DESC'
    _unrestricted_rendering = True

    def _default_card_template_id(self):
        return self.env['card.template'].search([], limit=1)

    def _get_model_selection(self):
        """Hardcoded list of models, checked against actually-present models."""
        allowed_models = ['res.partner', 'event.track', 'event.booth', 'event.registration']
        models = self.env['ir.model'].sudo().search_fetch([('model', 'in', allowed_models)], ['model', 'name'])
        return [(model.model, model.name) for model in models]

    name = fields.Char(required=True)
    active = fields.Boolean(default=True)
    body_html = fields.Html(related='card_template_id.body', render_engine="qweb")

    card_count = fields.Integer(compute='_compute_card_stats')
    card_click_count = fields.Integer(compute='_compute_card_stats')
    card_share_count = fields.Integer(compute='_compute_card_stats')

    mailing_ids = fields.One2many('mailing.mailing', 'card_campaign_id')
    mailing_count = fields.Integer(compute='_compute_mailing_count')

    card_ids = fields.One2many('card.card', inverse_name='campaign_id')
    card_template_id = fields.Many2one('card.template', string="Design", default=_default_card_template_id, required=True)
    image_preview = fields.Image(compute='_compute_image_preview', compute_sudo=False, readonly=True, store=True, attachment=False)
    link_tracker_id = fields.Many2one('link.tracker', ondelete="restrict")
    res_model = fields.Selection(
        string="Model Name", compute='_compute_res_model', selection='_get_model_selection',
        precompute=True, readonly=True, required=True, store=True,
    )

    post_suggestion = fields.Text(help="Description below the card and default text when sharing on X")
    preview_record_ref = fields.Reference(string="Preview On", selection="_get_model_selection", required=True)
    tag_ids = fields.Many2many('card.campaign.tag', string='Tags')
    target_url = fields.Char(string='Post Link')
    target_url_click_count = fields.Integer(related="link_tracker_id.count")

    user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user, domain="[('share', '=', False)]")

    reward_message = fields.Html(string='Thank You Message')
    reward_target_url = fields.Char(string='Reward Link')
    request_title = fields.Char('Request', default=lambda self: _('Help us share the news'))
    request_description = fields.Text('Request Description')

    # Static Content fields
    content_background = fields.Image('Background')
    content_button = fields.Char('Button')

    # Dynamic Content fields
    content_header = fields.Char('Header')
    content_header_dyn = fields.Boolean('Is Dynamic Header')
    content_header_path = fields.Char('Header Path')
    content_header_color = fields.Char('Header Color')

    content_sub_header = fields.Char('Sub-Header')
    content_sub_header_dyn = fields.Boolean('Is Dynamic Sub-Header')
    content_sub_header_path = fields.Char('Sub-Header Path')
    content_sub_header_color = fields.Char('Sub Header Color')

    content_section = fields.Char('Section')
    content_section_dyn = fields.Boolean('Is Dynamic Section')
    content_section_path = fields.Char('Section Path')

    content_sub_section1 = fields.Char('Sub-Section 1')
    content_sub_section1_dyn = fields.Boolean('Is Dynamic Sub-Section 1')
    content_sub_section1_path = fields.Char('Sub-Section 1 Path')

    content_sub_section2 = fields.Char('Sub-Section 2')
    content_sub_section2_dyn = fields.Boolean('Is Dynamic Sub-Section 2')
    content_sub_section2_path = fields.Char('Sub-Section 2 Path')

    # images are always dynamic
    content_image1_path = fields.Char('Dynamic Image 1')
    content_image2_path = fields.Char('Dynamic Image 2')

    @api.depends('card_ids')
    def _compute_card_stats(self):
        cards_by_status_count = self.env['card.card']._read_group(
            domain=[('campaign_id', 'in', self.ids)],
            groupby=['campaign_id', 'share_status'],
            aggregates=['__count'],
            order='campaign_id ASC',
        )
        self.update({
            'card_count': 0,
            'card_click_count': 0,
            'card_share_count': 0,
        })
        for campaign, status, card_count in cards_by_status_count:
            # shared cards are implicitly visited
            if status == 'shared':
                campaign.card_share_count += card_count
            if status in ('shared', 'visited'):
                campaign.card_click_count += card_count
            campaign.card_count += card_count

    @api.model
    def _get_render_fields(self):
        return [
            'body_html', 'content_background', 'content_image1_path', 'content_image2_path', 'content_button', 'content_header',
            'content_header_dyn', 'content_header_path', 'content_header_color', 'content_sub_header',
            'content_sub_header_dyn', 'content_sub_header_path', 'content_section', 'content_section_dyn',
            'content_section_path', 'content_sub_section1', 'content_sub_section1_dyn', 'content_sub_header_color',
            'content_sub_section1_path', 'content_sub_section2', 'content_sub_section2_dyn', 'content_sub_section2_path',
            'card_template_id',
        ]

    def _check_access_right_dynamic_template(self):
        """ `_unrestricted_rendering` being True means we trust the value on model
        when rendering. This means once created, rendering is done without restriction.
        But this attribute triggers a check at create / write / translation update that
        current user is an admin or has full edition rights (group_mail_template_editor).

         However here a Marketing Card Manager must be able to edit the fields other
         than the rendering fields. The qweb rendered field `body_html` cannot be
         modified by users other than the `base.group_system` users, as
        - it's a related field to `card.template.body`,
        - store=False
        - the model `card.template` can only be altered by `base.group_system`

        Hence the security is delegated to the 'card.template' model, hence the
        check done by `_check_access_right_dynamic_template` can be bypassed.
        """
        return

    @api.depends(lambda self: self._get_render_fields() + ['preview_record_ref'])
    def _compute_image_preview(self):
        for campaign in self:
            if campaign.preview_record_ref and campaign.preview_record_ref.exists():
                image = campaign._get_image_b64(campaign.preview_record_ref)
            else:
                image = False
            campaign.image_preview = image

    @api.depends('mailing_ids')
    def _compute_mailing_count(self):
        self.mailing_count = 0
        mailing_counts = self.env['mailing.mailing']._read_group(
            [('card_campaign_id', 'in', self.ids)], ['card_campaign_id'], ['__count']
        )
        for campaign, mailing_count in mailing_counts:
            campaign.mailing_count = mailing_count

    @api.depends('preview_record_ref')
    def _compute_res_model(self):
        for campaign in self:
            preview_model = campaign.preview_record_ref and campaign.preview_record_ref._name
            campaign.res_model = preview_model or campaign.res_model or 'res.partner'

    @api.model_create_multi
    def create(self, create_vals):
        utm_source = self.env.ref('marketing_card.utm_source_marketing_card', raise_if_not_found=False)
        link_trackers = self.env['link.tracker'].sudo().create([
            {
                'url': vals.get('target_url') or self.env['card.campaign'].get_base_url(),
                'title': vals['name'],  # not having this will trigger a request in the create
                'source_id': utm_source.id if utm_source else None,
                'label': f"marketing_card_campaign_{vals.get('name', '')}_{fields.Datetime.now()}",
            }
            for vals in create_vals
        ])
        return super().create([{
            **vals,
            'link_tracker_id': link_tracker_id,
        } for vals, link_tracker_id in zip(create_vals, link_trackers.ids)])

    def write(self, vals):
        link_tracker_vals = {}
        if vals.keys() & set(self._get_render_fields()):
            self.env['card.card'].search([('campaign_id', 'in', self.ids)]).requires_sync = True
        if 'target_url' in vals:
            link_tracker_vals['url'] = vals['target_url'] or self.env['card.campaign'].get_base_url()
        if link_tracker_vals:
            self.link_tracker_id.sudo().write(link_tracker_vals)

        # write and detect model changes on actively-used campaigns
        original_models = self.mapped('res_model')

        write_res = super().write(vals)

        updated_model_campaigns = self.env['card.campaign'].browse([
            campaign.id for campaign, new_model, old_model
            in zip(self, self.mapped('res_model'), original_models)
            if new_model != old_model
        ])
        for campaign in updated_model_campaigns:
            if campaign.card_count:
                raise exceptions.ValidationError(_(
                    "Model of campaign %(campaign)s may not be changed as it already has cards",
                    campaign=campaign.display_name,
                ))
        return write_res

    def action_view_cards(self):
        self.ensure_one()
        return self.env["ir.actions.actions"]._for_xml_id("marketing_card.cards_card_action") | {
            'context': {},
            'domain': [('campaign_id', '=', self.id)],
        }

    def action_view_cards_clicked(self):
        self.ensure_one()
        return self.env["ir.actions.actions"]._for_xml_id("marketing_card.cards_card_action") | {
            'context': {'search_default_filter_visited': True},
            'domain': [('campaign_id', '=', self.id)],
        }

    def action_view_cards_shared(self):
        self.ensure_one()
        return self.env["ir.actions.actions"]._for_xml_id("marketing_card.cards_card_action") | {
            'context': {'search_default_filter_shared': True},
            'domain': [('campaign_id', '=', self.id)],
        }

    def action_view_mailings(self):
        self.ensure_one()
        return {
            'name': _('%(card_campaign_name)s Mailings', card_campaign_name=self.name),
            'type': 'ir.actions.act_window',
            'res_model': 'mailing.mailing',
            'domain': [('card_campaign_id', '=', self.id)],
            'view_mode': 'list,form',
            'target': 'current',
        }

    def action_preview(self):
        self.ensure_one()
        card = self.env['card.card'].with_context(active_test=False).search([
            ('campaign_id', '=', self.id),
            ('res_id', '=', self.preview_record_ref.id),
        ])
        if card:
            card.image = self.image_preview
        else:
            card = self.env['card.card'].create({
                'campaign_id': self.id,
                'res_id': self.preview_record_ref.id,
                'image': self.image_preview,
                'active': False,
            })
        return {'type': 'ir.actions.act_url', 'url': card._get_path('preview'), 'target': 'new'}

    def action_share(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': _('Send Cards'),
            'res_model': 'mailing.mailing',
            'context': {
                'default_subject': self.name,
                'default_card_campaign_id': self.id,
                'default_mailing_model_id': self.env['ir.model']._get_id(self.res_model),
                'default_body_arch': f"""
<div class="o_layout oe_unremovable oe_unmovable bg-200 o_empty_theme" data-name="Mailing">
<style id="design-element"></style>
<div class="container o_mail_wrapper o_mail_regular oe_unremovable">
<div class="row">
<div class="col o_mail_no_options o_mail_wrapper_td bg-white oe_structure o_editable theme_selection_done">

<div class="s_text_block o_mail_snippet_general pt24 pb24" style="padding-left: 15px; padding-right: 15px;" data-snippet="s_text_block" data-name="Text">
    <div class="container s_allow_columns">
        <p class="o_default_snippet_text">Hello everyone</p>
        <p class="o_default_snippet_text">Here's the link to advertise your participation.
        <br> Your help with this promotion would be greatly appreciated!`</p>
        <p class="o_default_snippet_text">Many thanks</p>
    </div>
</div>

<div class="s_call_to_share_card o_mail_snippet_general" style="padding-top: 10px; padding-bottom: 10px;">
    <table width="100%" border="0" cellspacing="0" cellpadding="0">
        <tbody>
            <tr>
                <td align="center">
                    <a href="/cards/{self.id}/preview" style="padding-left: 3px !important; padding-right: 3px !important">
                        <img src="/web/image/card.campaign/{self.id}/image_preview" alt="Card Preview" class="img-fluid" style="width: 540px;"/>
                    </a>
                </td>
            </tr>
        </tbody>
    </table>
</div>

</div></div></div></div>
""",
            },
            'views': [[False, 'form']],
            'target': 'new',
        }

    # ==========================================================================
    # Image generation
    # ==========================================================================

    def _get_image_b64(self, record):
        if not self.card_template_id.body:
            return ''

        image_bytes = self.env['ir.actions.report']._run_wkhtmltoimage(
            [self._render_field('body_html', record.ids, add_context={'card_campaign': self})[record.id]],
            *TEMPLATE_DIMENSIONS
        )[0]
        return image_bytes and base64.b64encode(image_bytes)

    # ==========================================================================
    # Card creation
    # ==========================================================================

    def _update_cards(self, domain, auto_commit=False):
        """Create missing cards and update cards if necessary based for the domain."""
        self.ensure_one()
        TargetModel = self.env[self.res_model]
        res_ids = TargetModel.search(domain).ids
        cards = self.env['card.card'].with_context(active_test=False).search_fetch([
            ('campaign_id', '=', self.id),
            ('res_id', 'in', res_ids),
        ], ['res_id', 'requires_sync'])
        # update active and res_model for preview cards
        cards.active = True
        self.env['card.card'].create([
            {'campaign_id': self.id, 'res_id': res_id}
            for res_id in set(res_ids) - set(cards.mapped('res_id'))
        ])

        # render by batch of 100 to avoid losing progress in case of time out
        updated_cards = self.env['card.card']
        while cards := self.env['card.card'].search_fetch([
            ('requires_sync', '=', True),
            ('campaign_id', '=', self.id),
            ('res_id', 'in', res_ids),
        ], ['res_id'], limit=100):
            # no need to autocommit if it can be done in one batch
            if auto_commit and updated_cards:
                self.env.cr.commit()
                # avoid keeping hundreds of jpegs in memory
                self.env['card.card'].invalidate_model(['image'])
            TargetModelPrefetch = TargetModel.with_prefetch(cards.mapped('res_id'))
            for card in cards.filtered('requires_sync'):
                card.write({
                    'image': self._get_image_b64(TargetModelPrefetch.browse(card.res_id)),
                    'requires_sync': False,
                    'active': True,
                })
            cards.flush_recordset()
            updated_cards += cards
        return updated_cards

    def _get_url_from_res_id(self, res_id, suffix='preview'):
        card = self.env['card.card'].search([('campaign_id', '=', self.id), ('res_id', '=', res_id)])
        return card and card._get_path(suffix) or self.target_url

    # ==========================================================================
    # Mail render mixin / Render utils
    # ==========================================================================

    @api.depends('res_model')
    def _compute_render_model(self):
        """ override for mail.render.mixin """
        for campaign in self:
            campaign.render_model = campaign.res_model

    def _get_card_element_values(self, record):
        """Helper to get the right value for dynamic fields."""
        self.ensure_one()
        result = {
            'image1': images[0] if (images := self.content_image1_path and self.content_image1_path in record and record.mapped(self.content_image1_path)) else False,
            'image2': images[0] if (images := self.content_image2_path and self.content_image2_path in record and record.mapped(self.content_image2_path)) else False,
        }
        campaign_text_element_fields = (
            ('header', 'content_header', 'content_header_dyn', 'content_header_path'),
            ('sub_header', 'content_sub_header', 'content_sub_header_dyn', 'content_sub_header_path'),
            ('section', 'content_section', 'content_section_dyn', 'content_section_path'),
            ('sub_section1', 'content_sub_section1', 'content_sub_section1_dyn', 'content_sub_section1_path'),
            ('sub_section2', 'content_sub_section2', 'content_sub_section2_dyn', 'content_sub_section2_path'),
        )
        for el, text_field, dyn_field, path_field in campaign_text_element_fields:
            if not self[dyn_field]:
                result[el] = self[text_field]
            else:
                try:
                    m = record.mapped(self[path_field])
                    result[el] = m and m[0] or False
                except (AttributeError, KeyError):
                    # for generic image, or if field incorrect, return name of field
                    result[el] = self[path_field]
        return result
