import hashlib
import json
import random
import time
from datetime import timedelta
from urllib.parse import urljoin

import requests
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.mail import send_mail
from django.db import models
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _

from passerelle.base.models import SkipJob
from passerelle.sms.models import SMSResource
from passerelle.utils.jsonresponse import APIError


class OVHSMSGateway(SMSResource):
    documentation_url = (
        'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
    )
    hide_description_fields = ['account', 'credit_left']
    API_URL = 'https://eu.api.ovh.com/1.0/sms/%(serviceName)s/'
    URL = 'https://www.ovh.com/cgi-bin/sms/http2sms.cgi'
    MESSAGES_CLASSES = (
        (
            0,
            _(
                'Message are directly shown to users on phone screen '
                'at reception. The message is never stored, neither in the '
                'phone memory nor in the SIM card. It is deleted as '
                'soon as the user validate the display.'
            ),
        ),
        (1, _('Messages are stored in the phone memory, or in the SIM card if the memory is full. ')),
        (2, _('Messages are stored in the SIM card.')),
        (3, _('Messages are stored in external storage like a PDA or a PC.')),
    )
    NEW_MESSAGES_CLASSES = ['flash', 'phoneDisplay', 'sim', 'toolkit']

    account = models.CharField(
        verbose_name=_('Account'), max_length=64, help_text=_('Account identifier, such as sms-XXXXXX-1.')
    )

    application_key = models.CharField(
        verbose_name=_('Application key'),
        max_length=16,
        blank=True,
    )
    application_secret = models.CharField(
        verbose_name=_('Application secret'),
        max_length=32,
        blank=True,
        help_text=_('Obtained at the same time as "Application key".'),
    )
    consumer_key = models.CharField(
        verbose_name=_('Consumer key'),
        max_length=32,
        blank=True,
        help_text=_('Automatically obtained from OVH, should not be filled manually.'),
    )

    username = models.CharField(
        verbose_name=_('Username (deprecated)'),
        max_length=64,
        blank=True,
        help_text=_(
            'API user created on the SMS account. This field is obsolete once keys and secret '
            'fields above are filled.'
        ),
    )
    password = models.CharField(
        verbose_name=_('Password (deprecated)'),
        max_length=64,
        blank=True,
        help_text=_(
            'Password for legacy API. This field is obsolete once keys and secret fields above are filled.'
        ),
    )
    msg_class = models.IntegerField(choices=MESSAGES_CLASSES, default=1, verbose_name=_('Message class'))
    credit_threshold_alert = models.PositiveIntegerField(
        verbose_name=_('Credit alert threshold'), default=500
    )
    credit_left = models.PositiveIntegerField(verbose_name=_('Credit left'), default=0, editable=False)
    alert_emails = ArrayField(
        models.EmailField(blank=True),
        blank=True,
        null=True,
        verbose_name=_('Email addresses list to send credit alerts to, separated by comma'),
    )
    credit_alert_timestamp = models.DateTimeField(null=True, editable=False)

    TEST_DEFAULTS = {
        'create_kwargs': {
            'account': '1234',
            'username': 'john',
            'password': 'doe',
        },
        'test_vectors': [
            {
                'response': '',
                'result': {
                    'err': 1,
                    'err_desc': 'OVH error: bad JSON response',
                },
            },
            {
                'response': {
                    'status': 100,
                    'creditLeft': 47,
                    'SmsIds': [1234],
                },
                'result': {
                    'err': 0,
                    'data': {
                        'credit_left': 47.0,
                        'ovh_result': {'SmsIds': [1234], 'creditLeft': 47, 'status': 100},
                        'sms_ids': [1234],
                        'warning': 'credit level too low for ovhsmsgateway: 47.0 (threshold 100)',
                    },
                },
            },
        ],
    }

    class Meta:
        verbose_name = 'OVH'
        db_table = 'sms_ovh'

    @property
    def uses_new_api(self):
        return self.application_key and self.application_secret

    def request(self, method, endpoint, **kwargs):
        url = self.API_URL % {'serviceName': self.account, 'login': self.username}
        url = urljoin(url, endpoint)

        # sign request
        body = json.dumps(kwargs['json']) if 'json' in kwargs else ''
        now = str(int(time.time()))
        signature = hashlib.sha1()
        to_sign = "+".join((self.application_secret, self.consumer_key, method.upper(), url, body, now))
        signature.update(to_sign.encode())

        headers = {
            'X-Ovh-Application': self.application_key,
            'X-Ovh-Consumer': self.consumer_key,
            'X-Ovh-Timestamp': now,
            'X-Ovh-Signature': "$1$" + signature.hexdigest(),
        }

        try:
            response = self.requests.request(method, url, headers=headers, **kwargs)
        except requests.RequestException as e:
            raise APIError('OVH error: POST failed, %s' % e)
        else:
            try:
                result = response.json()
            except ValueError:
                raise APIError('OVH error: bad JSON response')
        try:
            response.raise_for_status()
        except requests.RequestException as e:
            raise APIError('OVH error: %s "%s"' % (e, result))
        return result

    def send_msg(self, text, sender, destinations, **kwargs):
        if not self.uses_new_api:
            return self.send_msg_legacy(text, sender, destinations, **kwargs)

        body = {
            'sender': sender,
            'receivers': destinations,
            'message': text,
            'class': self.NEW_MESSAGES_CLASSES[int(self.msg_class)],
        }
        if not kwargs['stop']:
            body.update({'noStopClause': True})

        result = self.request('post', 'jobs/', json=body)

        credits_removed = result['totalCreditsRemoved']
        # update credit left
        self.credit_left -= credits_removed
        self.credit_left = max(self.credit_left, 0)
        self.save(update_credit=False)
        return credits_removed

    def update_credit_left(self):
        result = self.request('get', endpoint='')
        self.credit_left = result['creditsLeft']
        self.save(update_credit=False)

    def send_credit_alert_if_needed(self):
        if self.credit_left >= self.credit_threshold_alert:
            return
        if self.credit_alert_timestamp and self.credit_alert_timestamp > timezone.now() - timedelta(days=1):
            return  # alerts are sent daily
        ctx = {
            'connector': self,
            'connector_url': urljoin(settings.SITE_BASE_URL, self.get_absolute_url()),
        }
        subject = render_to_string('ovh/credit_alert_subject.txt', ctx).strip()
        body = render_to_string('ovh/credit_alert_body.txt', ctx)
        html_body = render_to_string('ovh/credit_alert_body.html', ctx)
        send_mail(
            subject,
            body,
            settings.DEFAULT_FROM_EMAIL,
            self.alert_emails,
            html_message=html_body,
        )
        self.credit_alert_timestamp = timezone.now()
        self.save()
        self.logger.warning('credit is too low, alerts were sent to %s', self.alert_emails)

    def hourly(self):
        super().hourly()
        if self.uses_new_api:
            self.update_credit_left()
            self.send_credit_alert_if_needed()

    def save(self, *args, update_credit=True, **kwargs):
        super().save(*args, **kwargs)
        if update_credit and self.uses_new_api:
            self.add_job('update_credit_left')

    def send_msg_legacy(self, text, sender, destinations, **kwargs):
        """Send a SMS using the HTTP2 endpoint"""
        if not self.password:
            raise APIError('Improperly configured, empty keys or password fields.')

        text = force_str(text).encode('utf-8')
        to = ','.join(destinations)
        params = {
            'account': self.account.encode('utf-8'),
            'login': self.username.encode('utf-8'),
            'password': self.password.encode('utf-8'),
            'from': sender.encode('utf-8'),
            'to': to,
            'message': text,
            'contentType': 'text/json',
            'class': self.msg_class,
        }
        if not kwargs['stop']:
            params.update({'noStop': 1})
        try:
            response = self.requests.post(self.URL, data=params)
        except requests.RequestException as e:
            raise APIError('OVH error: POST failed, %s' % e)
        else:
            try:
                result = response.json()
            except ValueError:
                raise APIError('OVH error: bad JSON response')
            else:
                if not isinstance(result, dict):
                    raise APIError('OVH error: bad JSON response %r, it should be a dictionnary' % result)
                if 100 <= result['status'] < 200:
                    credit_left = float(result['creditLeft'])
                    # update credit left
                    OVHSMSGateway.objects.filter(id=self.id).update(credit_left=credit_left)
                elif result['status'] == 429 and 'Please retry' in result.get('message'):
                    raise SkipJob(after_timestamp=random.randint(4, 10))
                else:
                    raise APIError('OVH error: %r' % result)
