import re

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import html2plaintext

# Matches {{token}} where token is a word (letters, digits, underscore),
# case-sensitive, no spaces inside the braces (Section 2.3).
PLACEHOLDER_RE = re.compile(r'\{\{(\w+)\}\}')

MAX_ATTACHMENTS = 5


class MailQuickTemplate(models.Model):
    _name = 'mail.quick.template'
    _description = 'Chatter Quick Reply Template'
    _inherit = ['mail.thread']
    _order = 'category_id, use_count desc, name, id'

    name = fields.Char(
        string='Template Name', required=True, translate=True, tracking=True,
        index='trigram',
    )
    category_id = fields.Many2one(
        'mail.quick.template.category', string='Category', required=True,
        ondelete='restrict', tracking=True, index=True,
    )
    model_id = fields.Many2one(
        'ir.model', string='Applicable Model', ondelete='cascade',
        tracking=True,
        help="If empty, the template is global and available on every model "
             "that has a chatter.",
    )
    # Stored related used for fast domain filtering in the picker query.
    model = fields.Char(
        string='Model Technical Name', related='model_id.model',
        store=True, index=True,
    )
    body_html = fields.Html(
        string='Message Body', required=True, sanitize=True,
        sanitize_tags=True, strip_style=False, translate=True,
    )
    company_id = fields.Many2one(
        'res.company', string='Company', required=True,
        default=lambda self: self.env.company, index=True,
    )
    group_ids = fields.Many2many(
        'res.groups', 'mail_quick_template_res_groups_rel',
        'template_id', 'group_id', string='Allowed Groups', tracking=True,
        help="If empty, the template is visible to all users. Otherwise only "
             "users belonging to at least one of these groups can see and use "
             "it.",
    )
    attachment_ids = fields.Many2many(
        'ir.attachment', relation='mail_quick_template_ir_attachment_rel',
        column1='template_id', column2='attachment_id',
        string='Default Attachments',
    )
    use_count = fields.Integer(
        string='Total Uses', default=0, readonly=True, copy=False,
        help="Recomputed nightly from the usage log.",
    )
    last_used_date = fields.Datetime(
        string='Last Used', readonly=True, copy=False,
    )
    active = fields.Boolean(string='Active', default=True, tracking=True)
    description = fields.Text(string='Internal Notes', translate=True)
    preview_text = fields.Char(
        string='Preview', compute='_compute_preview_text', store=False,
    )
    is_global = fields.Boolean(
        string='Global Template', compute='_compute_is_global', store=True,
    )
    color = fields.Integer(string='Color Index', default=0)

    favorite_ids = fields.One2many(
        'mail.quick.template.favorite', 'template_id', string='Favorites',
    )
    favorite_user_ids = fields.Many2many(
        'res.users', string='Favorited By',
        compute='_compute_favorite_user_ids', store=False,
    )
    log_ids = fields.One2many(
        'mail.quick.template.log', 'template_id', string='Usage Logs',
    )

    # Uniqueness is enforced by the Python constraint _check_name_unique below
    # (not a SQL UNIQUE): it raises the friendly, SRS-specified ValidationError
    # in all cases AND correctly treats two global templates (NULL model_id) of
    # the same name as duplicates — which a SQL UNIQUE cannot, since NULL != NULL
    # in PostgreSQL (Section 4.2 note).
    # Indexes from Section 4.2 / 15.1 (the picker queries).
    _company_active_idx = models.Index('(company_id, active)')
    _model_active_company_idx = models.Index('(model, active, company_id)')

    # ------------------------------------------------------------------
    # Computes
    # ------------------------------------------------------------------
    @api.depends('body_html')
    def _compute_preview_text(self):
        for rec in self:
            plain = html2plaintext(rec.body_html or '').strip()
            rec.preview_text = (plain[:100] + '...') if len(plain) > 100 else plain

    @api.depends('model_id')
    def _compute_is_global(self):
        for rec in self:
            rec.is_global = not bool(rec.model_id)

    @api.depends('favorite_ids', 'favorite_ids.user_id')
    def _compute_favorite_user_ids(self):
        for rec in self:
            rec.favorite_user_ids = rec.favorite_ids.mapped('user_id')

    # ------------------------------------------------------------------
    # Constraints
    # ------------------------------------------------------------------
    @api.constrains('name', 'model_id', 'company_id')
    def _check_name_unique(self):
        """BR-06: unique per (name, model_id, company_id). NULL model_id is
        treated distinctly so two global templates with the same name in the
        same company ARE duplicates (cross-PostgreSQL-version safe)."""
        for rec in self:
            domain = [
                ('id', '!=', rec.id),
                ('name', '=', rec.name),
                ('company_id', '=', rec.company_id.id),
                ('model_id', '=', rec.model_id.id if rec.model_id else False),
            ]
            if self.with_context(active_test=False).search_count(domain):
                raise ValidationError(_(
                    "A template with name '%(name)s' already exists for this "
                    "model and company. Please choose a unique name.",
                    name=rec.name,
                ))

    @api.constrains('name')
    def _check_name_not_empty(self):
        """BR-01: name is mandatory and must not be blank/whitespace (a
        required Char still accepts an empty string at the ORM level)."""
        for rec in self:
            if not rec.name or not rec.name.strip():
                raise ValidationError(_("Template name cannot be empty or whitespace."))

    @api.constrains('body_html')
    def _check_body_not_empty(self):
        for rec in self:
            if not rec.body_html or not html2plaintext(rec.body_html).strip():
                raise ValidationError(_("Template body cannot be empty."))

    @api.constrains('attachment_ids')
    def _check_attachment_limit(self):
        for rec in self:
            if len(rec.attachment_ids) > MAX_ATTACHMENTS:
                raise ValidationError(_(
                    "A template can have at most %(num)s default attachments.",
                    num=MAX_ATTACHMENTS,
                ))

    # ------------------------------------------------------------------
    # Onchange (UX hint only, non-blocking)
    # ------------------------------------------------------------------
    @api.onchange('model_id')
    def _onchange_model_id(self):
        if self.model_id:
            return {
                'warning': {
                    'title': _('Model Changed'),
                    'message': _(
                        'Verify that all {{placeholders}} in the body are '
                        'valid fields of the new model.'
                    ),
                }
            }

    # ------------------------------------------------------------------
    # ORM overrides
    # ------------------------------------------------------------------
    def write(self, vals):
        """BR-09: last_used_date can only be set by the server and must never
        be in the future."""
        if 'last_used_date' in vals and vals['last_used_date']:
            lud = fields.Datetime.to_datetime(vals['last_used_date'])
            if lud and lud > fields.Datetime.now():
                raise ValidationError(_(
                    "The 'Last Used' date cannot be set in the future."
                ))
        # Section 17.4: only System Administrators may move a template to a
        # different company once it exists.
        if 'company_id' in vals and not self.env.su \
                and not self.env.user.has_group('base.group_system'):
            for rec in self:
                if rec.company_id.id != vals['company_id']:
                    raise UserError(_(
                        "Only an Administrator can change the company of an "
                        "existing template."
                    ))
        return super().write(vals)

    @api.model_create_multi
    def create(self, vals_list):
        """Suppress the chatter "record created" log (Section 18: avoid chatter
        spam). This also avoids the core reply-to/company computation that can
        raise an AccessError for restricted Template Managers when accounting
        is installed. Field tracking on later edits is unaffected."""
        return super(
            MailQuickTemplate, self.with_context(mail_create_nolog=True)
        ).create(vals_list)

    def copy_data(self, default=None):
        """TP-15: a duplicate keeps the body but gets a unique name so the
        uniqueness constraint is satisfied."""
        default = dict(default or {})
        vals_list = super().copy_data(default=default)
        if 'name' not in default:
            for rec, vals in zip(self, vals_list):
                vals['name'] = _("%(name)s (copy)", name=rec.name)
        return vals_list

    def unlink(self):
        """BR-11: templates can only be deleted if use_count == 0, unless the
        user is a System Administrator."""
        if not self.env.user.has_group('base.group_system'):
            for rec in self:
                if rec.use_count > 0:
                    raise UserError(_(
                        "Template '%(name)s' has been used and cannot be "
                        "deleted. Please archive it instead.",
                        name=rec.name,
                    ))
        return super().unlink()

    # ------------------------------------------------------------------
    # Actions
    # ------------------------------------------------------------------
    def action_archive(self):
        return self.write({'active': False})

    def action_unarchive(self):
        return self.write({'active': True})

    def action_toggle_active(self):
        for rec in self:
            rec.active = not rec.active
        return True

    def action_view_logs(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': _('Usage Logs'),
            'res_model': 'mail.quick.template.log',
            'view_mode': 'list,form',
            'domain': [('template_id', '=', self.id)],
        }

    def action_open_preview(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': _('Template Preview'),
            'res_model': 'mail.quick.template.preview.wizard',
            'view_mode': 'form',
            'target': 'new',
            'context': {'default_template_id': self.id},
        }

    # ------------------------------------------------------------------
    # Feature flag (Section 2.9 / 12) — company-specific config param.
    # ------------------------------------------------------------------
    @api.model
    def _is_feature_enabled(self, company=None):
        company = company or self.env.company
        icp = self.env['ir.config_parameter'].sudo()
        key = 'do_mail_quick_templates.enabled_%s' % company.id
        # NB: get_param() returns False (not None) for a missing key, so test
        # for falsy — an unset company key falls back to the global parameter,
        # which itself defaults to enabled.
        val = icp.get_param(key)
        if not val:
            val = icp.get_param('do_mail_quick_templates.enabled', default='True')
        return str(val).strip().lower() not in ('false', '0', '')

    # ------------------------------------------------------------------
    # Placeholder resolution (Section 2.3 / 10.3)
    # ------------------------------------------------------------------
    def _resolve_placeholders(self, res_model, res_id):
        """Resolve {{placeholder}} tokens in body_html against a record.

        Returns the resolved HTML string. Never raises: unknown tokens and
        failed field reads are left/skipped silently (Section 2.3, EC-01/02).
        """
        self.ensure_one()
        body = self.body_html or ''
        model_obj = None
        if res_model and res_model in self.env:
            model_obj = self.env[res_model]
        record = None
        if model_obj is not None and res_id:
            try:
                record = model_obj.browse(int(res_id)).exists()
            except ValueError:
                record = None

        def replacer(match):
            token = match.group(1).strip()
            # System tokens.
            if token == 'today':
                # Render in the user's timezone & locale (Section 18.3/18.4).
                lang = self.env.user.lang or 'en_US'
                date_format = self.env['res.lang']._lang_get(lang).date_format
                today = fields.Datetime.context_timestamp(
                    self, fields.Datetime.now()
                ).date()
                return today.strftime(date_format)
            if token == 'company_name':
                return self.env.company.name or ''
            if token == 'current_user':
                return self.env.user.name or ''
            # Record field tokens: resolve any token that is a real field of the
            # active model. On an unsaved/new record (res_id=0) a valid field
            # resolves to '' (EC-02). Unknown tokens are left untouched (TB-07).
            if model_obj is not None and token in model_obj._fields:
                if record:
                    try:
                        val = record[token]
                    except Exception:
                        return ''
                    if hasattr(val, 'name'):  # Many2one / recordset (EC-01/TB-08)
                        return val.name or ''
                    if val is False or val is None:
                        return ''
                    return str(val)
                return ''
            # Unknown token: leave it as-is as a visual indicator (Section 2.3).
            return match.group(0)

        return PLACEHOLDER_RE.sub(replacer, body)

    # ------------------------------------------------------------------
    # Scheduled actions (Section 10.1)
    # ------------------------------------------------------------------
    @api.model
    def _cron_recompute_use_counts(self):
        """Synchronise use_count with the actual usage-log counts.

        Raw SQL is used intentionally: this is a pure counter synchronisation
        with no business logic. Doing it through the ORM would trigger per-row
        write events and cache invalidation and be orders of magnitude slower
        (Section 10.1 / 15.6).
        """
        # Flush any pending ORM writes so the raw SQL below sees current data.
        self.env.flush_all()
        self.env.cr.execute("""
            UPDATE mail_quick_template t
            SET use_count = COALESCE(l.cnt, 0)
            FROM (
                SELECT template_id, COUNT(*) AS cnt
                FROM mail_quick_template_log
                WHERE template_id IS NOT NULL
                GROUP BY template_id
            ) l
            WHERE t.id = l.template_id
        """)
        self.env.cr.execute("""
            UPDATE mail_quick_template
            SET use_count = 0
            WHERE id NOT IN (
                SELECT DISTINCT template_id
                FROM mail_quick_template_log
                WHERE template_id IS NOT NULL
            )
        """)
        # Invalidate the ORM cache so subsequent reads see the new counters.
        self.env.invalidate_all()
