# -*- coding: utf-8 -*-
# Copyright 2016 Tecnativa - Antonio Espinosa
# Copyright 2017 Tecnativa - David Vidal
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import hashlib
import hmac
import requests
from datetime import datetime
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import email_split

import logging
_logger = logging.getLogger(__name__)


class MailTrackingEmail(models.Model):
    _inherit = "mail.tracking.email"

    def _country_search(self, country_code):
        country = False
        if country_code:
            country = self.env['res.country'].search([
                ('code', '=', country_code.upper()),
            ])
        if country:
            return country.id
        return False

    @property
    def _mailgun_mandatory_fields(self):
        return ('event', 'timestamp', 'token', 'signature',
                'tracking_email_id', 'odoo_db')

    @property
    def _mailgun_event_type_mapping(self):
        return {
            # Mailgun event type: tracking event type
            'delivered': 'delivered',
            'opened': 'open',
            'clicked': 'click',
            'unsubscribed': 'unsub',
            'complained': 'spam',
            'bounced': 'hard_bounce',
            'dropped': 'reject',
            'accepted': 'sent',
            'failed': 'error',
            'rejected': 'error',
        }

    def _mailgun_event_type_verify(self, event):
        event = event or {}
        mailgun_event_type = event.get('event')
        if mailgun_event_type not in self._mailgun_event_type_mapping:
            _logger.error("Mailgun: event type '%s' not supported",
                          mailgun_event_type)
            return False
        # OK, event type is valid
        return True

    def _mailgun_signature(self, api_key, timestamp, token):
        return hmac.new(
            key=bytes(api_key, 'utf-8'),
            msg=bytes('{}{}'.format(str(timestamp), str(token)), 'utf-8'),
            digestmod=hashlib.sha256).hexdigest()

    def _mailgun_values(self):
        icp = self.env['ir.config_parameter'].sudo()
        api_key = icp.get_param('mailgun.apikey')
        if not api_key:
            raise ValidationError(_('There is no Mailgun API key!'))
        api_url = icp.get_param(
            'mailgun.api_url', 'https://api.mailgun.net/v3')
        catchall_domain = icp.get_param('mail.catchall.domain')
        domain = icp.get_param('mailgun.domain', catchall_domain)
        if not domain:
            raise ValidationError(_('A Mailgun domain value is needed!'))
        validation_key = icp.get_param('mailgun.validation_key')
        return api_key, api_url, domain, validation_key

    def _mailgun_signature_verify(self, event):
        event = event or {}
        icp = self.env['ir.config_parameter'].sudo()
        api_key = icp.get_param('mailgun.apikey')
        if not api_key:
            _logger.warning("No Mailgun api key configured. "
                            "Please add 'mailgun.apikey' to System parameters "
                            "to enable Mailgun authentication webhoook "
                            "requests. More info at: "
                            "https://documentation.mailgun.com/"
                            "user_manual.html#webhooks")
        else:
            timestamp = event.get('timestamp')
            token = event.get('token')
            signature = event.get('signature')
            event_digest = self._mailgun_signature(api_key, timestamp, token)
            if signature != event_digest:
                _logger.error("Mailgun: Invalid signature '%s' != '%s'",
                              signature, event_digest)
                return False
        # OK, signature is valid
        return True

    def _db_verify(self, event):
        event = event or {}
        odoo_db = event.get('odoo_db')
        current_db = self.env.cr.dbname
        if odoo_db != current_db:
            _logger.error("Mailgun: Database '%s' is not the current database",
                          odoo_db)
            return False
        # OK, DB is current
        return True

    def _mailgun_metadata(self, mailgun_event_type, event, metadata):
        # Get Mailgun timestamp when found
        ts = event.get('timestamp', False)
        try:
            ts = float(ts)
        except Exception:
            ts = False
        if ts:
            dt = datetime.utcfromtimestamp(ts)
            metadata.update({
                'timestamp': ts,
                'time': fields.Datetime.to_string(dt),
                'date': fields.Date.to_string(dt),
                'mailgun_id': event.get('id', False)
            })
        # Common field mapping
        mapping = {
            'recipient': 'recipient',
            'ip': 'ip',
            'user_agent': 'user-agent',
            'os_family': 'client-os',
            'ua_family': 'client-name',
            'ua_type': 'client-type',
            'url': 'url',
        }
        for k, v in mapping.items():
            if event.get(v, False):
                metadata[k] = event[v]
        # Special field mapping
        metadata.update({
            'mobile': event.get('device-type') in {'mobile', 'tablet'},
            'user_country_id': self._country_search(
                event.get('country', False)),
        })
        # Mapping for special events
        if mailgun_event_type == 'bounced':
            metadata.update({
                'error_type': event.get('code', False),
                'error_description': event.get('error', False),
                'error_details': event.get('notification', False),
            })
        elif mailgun_event_type == 'dropped':
            metadata.update({
                'error_type': event.get('reason', False),
                'error_description': event.get('code', False),
                'error_details': event.get('description', False),
            })
        elif mailgun_event_type == 'complained':
            metadata.update({
                'error_type': 'spam',
                'error_description':
                    "Recipient '%s' mark this email as spam" %
                    event.get('recipient', False),
            })
        return metadata

    def _mailgun_tracking_get(self, event):
        tracking = False
        tracking_email_id = event.get('tracking_email_id', False)
        if tracking_email_id and tracking_email_id.isdigit():
            tracking = self.search([('id', '=', tracking_email_id)], limit=1)
        return tracking

    def _event_is_from_mailgun(self, event):
        event = event or {}
        return all([k in event for k in self._mailgun_mandatory_fields])

    @api.model
    def event_process(self, request, post, metadata, event_type=None):
        res = super(MailTrackingEmail, self).event_process(
            request, post, metadata, event_type=event_type)
        if res == 'NONE' and self._event_is_from_mailgun(post):
            if not self._mailgun_signature_verify(post):
                res = 'ERROR: Signature'
            elif not self._mailgun_event_type_verify(post):
                res = 'ERROR: Event type not supported'
            elif not self._db_verify(post):
                res = 'ERROR: Invalid DB'
            else:
                res = 'OK'
        if res == 'OK':
            mailgun_event_type = post.get('event')
            mapped_event_type = self._mailgun_event_type_mapping.get(
                mailgun_event_type) or event_type
            if not mapped_event_type:  # pragma: no cover
                res = 'ERROR: Bad event'
            tracking = self._mailgun_tracking_get(post)
            if not tracking:
                res = 'ERROR: Tracking not found'
        if res == 'OK':
            # Complete metadata with mailgun event info
            metadata = self._mailgun_metadata(
                mailgun_event_type, post, metadata)
            # Create event
            tracking.event_create(mapped_event_type, metadata)
        if res != 'NONE':
            if event_type:
                _logger.info(
                    "Mailgun: event '%s' process '%s'", event_type, res)
            else:
                _logger.info("Mailgun: event process '%s'", res)
        return res

    @api.multi
    def action_manual_check_mailgun(self):
        """
        Manual check against Mailgun API
        API Documentation:
        https://documentation.mailgun.com/en/latest/api-events.html
        """
        api_key, api_url, domain, validation_key = self._mailgun_values()
        for tracking in self:
            if not tracking.mail_message_id:
                raise UserError(_('There is no tracked message!'))
            message_id = tracking.mail_message_id.message_id.replace(
                "<", "").replace(">", "")
            res = requests.get(
                '%s/%s/events' % (api_url, domain),
                auth=("api", api_key),
                params={
                    "begin": tracking.timestamp,
                    "ascending": "yes",
                    "message-id": message_id,
                }
            )
            if not res or res.status_code != 200:
                raise ValidationError(_(
                    "Couldn't retrieve Mailgun information"))
            content = res.json()
            if "items" not in content:
                raise ValidationError(_("Event information not longer stored"))
            for item in content["items"]:
                # mailgun event hasn't been synced and recipient is the same as
                # in the evaluated tracking. We use email_split since tracking
                # recipient could come in format: "example" <to@dest.com>
                if not self.env['mail.tracking.event'].search(
                        [('mailgun_id', '=', item["id"])]) and (
                            item.get("recipient", "") ==
                            email_split(tracking.recipient)[0]):
                    mapped_event_type = self._mailgun_event_type_mapping.get(
                        item["event"], item["event"])
                    metadata = self._mailgun_metadata(
                        mapped_event_type, item, {})
                    tracking.event_create(mapped_event_type, metadata)
