# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import ast
import base64
import logging
import re
import smtplib

import psycopg2

from odoo import _, fields, models, tools

from odoo.addons.base.models.ir_mail_server import MailDeliveryException

_logger = logging.getLogger(__name__)


def format_emails(partners):
    emails = [
        tools.formataddr((p.name or "False", p.email or "False")) for p in partners
    ]
    return ", ".join(emails)


class MailMail(models.Model):
    _inherit = "mail.mail"

    email_bcc = fields.Char("Bcc", help="Blind Cc message recipients")

    def _send(  # noqa: max-complexity: 4
        self, auto_commit=False, raise_exception=False, smtp_session=None
    ):
        env = self.env
        IrMailServer = env["ir.mail_server"]
        IrAttachment = env["ir.attachment"]
        ICP = env["ir.config_parameter"].sudo()
        # Mail composer only sends 1 mail at a time.
        is_out_of_scope = len(self.ids) > 1
        is_from_composer = self.env.context.get("is_from_composer", False)
        if is_out_of_scope or not is_from_composer:
            return super()._send(
                auto_commit=auto_commit,
                raise_exception=raise_exception,
                smtp_session=smtp_session,
            )
        mail = self
        success_pids = []
        failure_type = None
        # ===== Same with native Odoo =====
        # https://github.com/odoo/odoo/blob/0a3fc96cd51c0aab024207a4608f6ba32d49da36
        # /addons/mail/models/mail_mail.py#L384
        try:
            if mail.state != "outgoing":
                if mail.state != "exception" and mail.auto_delete:
                    mail.sudo().unlink()
                return True

            # remove attachments if user send the link with the access_token
            body = mail.body_html or ""
            attachments = mail.attachment_ids
            for link in re.findall(r"/web/(?:content|image)/([0-9]+)", body):
                attachments = attachments - IrAttachment.browse(int(link))

            # load attachment binary data with a separate read(), as
            # prefetching all `datas` (binary field) could bloat the browse
            # cache, triggerring soft/hard mem limits with temporary data.
            attachments = [
                (a["name"], base64.b64decode(a["datas"]), a["mimetype"])
                for a in attachments.sudo().read(["name", "datas", "mimetype"])
                if a["datas"] is not False
            ]

            # ===== Different than native Odoo =====
            email = mail._send_prepare_values()
            # ===== Same with native Odoo =====
            # headers
            headers = {}
            bounce_alias = ICP.get_param("mail.bounce.alias")
            catchall_domain = ICP.get_param("mail.catchall.domain")
            if bounce_alias and catchall_domain:
                headers["Return-Path"] = "%s@%s" % (bounce_alias, catchall_domain)
            if mail.headers:
                try:
                    headers.update(ast.literal_eval(mail.headers))
                except Exception as e:
                    # ===== Different than native Odoo =====
                    _logger.warning("Error during update headers: %s" % e)

            # ===== Same with native Odoo =====
            # Writing on the mail object may fail (e.g. lock on user) which
            # would trigger a rollback *after* actually sending the email.
            # To avoid sending twice the same email, provoke the failure earlier
            mail.write(
                {
                    "state": "exception",
                    "failure_reason": _(
                        "Error without exception. Probably due do sending an email"
                        " without computed recipients."
                    ),
                }
            )
            # Update notification in a transient exception state to avoid concurrent
            # update in case an email bounces while sending all emails related to current
            # mail record.
            notifs = self.env["mail.notification"].search(
                [
                    ("notification_type", "=", "email"),
                    ("mail_mail_id", "in", mail.ids),
                    ("notification_status", "not in", ("sent", "canceled")),
                ]
            )
            if notifs:
                notif_msg = _(
                    "Error without exception. Probably due do concurrent access"
                    " update of notification records. Please see with an administrator."
                )
                notifs.sudo().write(
                    {
                        "notification_status": "exception",
                        "failure_type": "unknown",
                        "failure_reason": notif_msg,
                    }
                )
                # `test_mail_bounce_during_send`, force immediate update to obtain the lock.
                # see rev. 56596e5240ef920df14d99087451ce6f06ac6d36
                notifs.flush_recordset(
                    ["notification_status", "failure_type", "failure_reason"]
                )

            # protect against ill-formatted email_from when formataddr was used on an already formatted email # noqa: B950
            emails_from = tools.email_split_and_format(mail.email_from)
            email_from = emails_from[0] if emails_from else mail.email_from

            # build an RFC2822 email.message.Message object and send it without queuing
            res = None
            # TDE note: could be great to pre-detect missing to/cc and skip sending it
            # to go directly to failed state update
            # ===== Different than native Odoo =====
            email["email_from"] = email_from
            msg = self.build_email(
                email,
                attachments=attachments,
                headers=headers,
            )
            try:
                res = IrMailServer.send_email(
                    msg,
                    mail_server_id=mail.mail_server_id.id,
                    smtp_session=smtp_session,
                )
                success_pids += mail.recipient_ids.ids
            # ===== Same with native Odoo =====
            except AssertionError as error:
                if str(error) == IrMailServer.NO_VALID_RECIPIENT:
                    # if we have a list of void emails for email_list
                    # -> email missing, otherwise generic email
                    # failure
                    if (
                        not email.get("email_to")
                        and failure_type != "mail_email_invalid"
                    ):
                        failure_type = "mail_email_missing"
                    else:
                        failure_type = "mail_email_invalid"
                    # No valid recipient found for this particular
                    # mail item -> ignore error to avoid blocking
                    # delivery to next recipients, if any. If this is
                    # the only recipient, the mail will show as failed.
                    _logger.info(
                        "Ignoring invalid recipients for mail.mail %s: %s",
                        mail.message_id,
                        email.get("email_to"),
                    )
                else:
                    raise
            if res:  # mail has been sent at least once, no major exception occurred
                mail.write(
                    {"state": "sent", "message_id": res, "failure_reason": False}
                )
                _logger.info(
                    "Mail with ID %r and Message-Id %r successfully sent",
                    mail.id,
                    mail.message_id,
                )
                # /!\ can't use mail.state here, as mail.refresh() will cause an error
                # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
            mail._postprocess_sent_message(
                success_pids=success_pids, failure_type=failure_type
            )
        except MemoryError:
            # prevent catching transient MemoryErrors, bubble up to
            # notify user or abort cron job instead of marking the
            # mail as failed
            _logger.exception(
                "MemoryError while processing mail with ID %r and Msg-Id %r."
                " Consider raising the --limit-memory-hard startup option",
                mail.id,
                mail.message_id,
            )
            # mail status will stay on ongoing since transaction will be rollback
            raise
        except (psycopg2.Error, smtplib.SMTPServerDisconnected):
            # If an error with the database or SMTP session occurs,
            # chances are that the cursor or SMTP session are
            # unusable, causing further errors when trying to save the
            # state.
            _logger.exception(
                "Exception while processing mail with ID %r and Msg-Id %r.",
                mail.id,
                mail.message_id,
            )
            raise
        except Exception as e:
            failure_reason = tools.ustr(e)
            _logger.exception(
                "failed sending mail (id: %s) due to %s", mail.id, failure_reason
            )
            mail.write({"state": "exception", "failure_reason": failure_reason})
            mail._postprocess_sent_message(
                success_pids=success_pids,
                failure_reason=failure_reason,
                failure_type="unknown",
            )
            if raise_exception:
                if isinstance(e, (AssertionError, UnicodeEncodeError)):
                    if isinstance(e, UnicodeEncodeError):
                        value = "Invalid text: %s" % e.object
                    else:
                        value = ". ".join(e.args)
                    raise MailDeliveryException(value) from e
                raise

        # ===== Different than native Odoo =====
        # As we only send one email, auto_commit has no value
        return True

    def build_email(self, email, attachments=None, headers=None):
        env = self.env
        mail = self
        email_from = email.get("email_from")
        IrMailServer = env["ir.mail_server"]
        # ===== Same with native Odoo =====
        # https://github.com/odoo/odoo/blob/0a3fc96cd51c0aab024207a4608f6ba32d49da36
        # /addons/mail/models/mail_mail.py#L458
        msg = IrMailServer.build_email(
            email_from=email_from,
            email_to=email.get("email_to"),
            subject=mail.subject,
            body=email.get("body"),
            body_alternative=email.get("body_alternative"),
            # ===== Different than native Odoo =====
            email_cc=mail.email_cc,
            email_bcc=mail.email_bcc,
            # ===== Same with native Odoo =====
            reply_to=mail.reply_to,
            attachments=attachments,
            message_id=mail.message_id,
            references=mail.references,
            object_id=mail.res_id and ("%s-%s" % (mail.res_id, mail.model)),
            subtype="html",
            subtype_alternative="plain",
            headers=headers,
        )
        return msg

    def _send_prepare_values(self, partner=None):
        res = super()._send_prepare_values(partner=partner)
        is_from_composer = self.env.context.get("is_from_composer", False)
        if not is_from_composer:
            return res
        partners_cc_bcc = self.recipient_cc_ids + self.recipient_bcc_ids
        partner_to_ids = [r.id for r in self.recipient_ids if r not in partners_cc_bcc]
        partner_to = self.env["res.partner"].browse(partner_to_ids)
        res["email_to"] = format_emails(partner_to)
        res["email_cc"] = format_emails(self.recipient_cc_ids)
        res["email_bcc"] = format_emails(self.recipient_bcc_ids)
        return res
