# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2020  Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import logging
import re

from django.contrib.postgres.fields import ArrayField
from django.core.validators import RegexValidator
from django.db import models
from django.urls import re_path, reverse
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _

from passerelle.base.models import BaseResource
from passerelle.sms.forms import SMSConnectorForm
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError

SEND_SCHEMA = {
    '$schema': 'http://json-schema.org/draft-04/schema#',
    "type": "object",
    'required': ['message', 'from', 'to'],
    'properties': {
        'message': {
            'description': 'String message',
            'type': 'string',
        },
        'from': {
            'description': 'Sender number',
            'type': 'string',
        },
        'to': {
            'description': 'Destination numbers',
            "type": "array",
            "items": {'type': 'string', 'pattern': r'^\+?[-.\s/\d]+$'},
        },
    },
}


def authorized_default():
    return [SMSResource.ALL]


class SMSResource(BaseResource):
    manager_form_base_class = SMSConnectorForm
    category = _('SMS Providers')
    documentation_url = (
        'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
    )

    _can_send_messages_description = _('Sending messages is limited to the following API users:')

    default_country_code = models.CharField(
        verbose_name=_('Default country code'),
        max_length=3,
        default='33',
        validators=[RegexValidator('^[0-9]*$', _('The country must only contain numbers'))],
    )
    default_trunk_prefix = models.CharField(
        verbose_name=_('Default trunk prefix'),
        max_length=2,
        default='0',
        validators=[RegexValidator('^[0-9]*$', _('The trunk prefix must only contain numbers'))],
    )  # Yeah France first !
    max_message_length = models.IntegerField(
        _('Maximum message length'), help_text=_('Messages over this limit will be truncated.'), default=2000
    )

    manager_view_template_name = 'passerelle/manage/messages_service_view.html'

    FR_METRO = 'fr-metro'
    FR_DOMTOM = 'fr-domtom'
    BE_ = 'be'
    ALL = 'all'
    AUTHORIZED = [
        (FR_METRO, _('France mainland (+33 [67])')),
        (FR_DOMTOM, _('France DOM/TOM (+262, etc.)')),
        (BE_, _('Belgian (+32 4[5-9]) ')),
        (ALL, _('All')),
    ]
    authorized = ArrayField(
        models.CharField(max_length=32, null=True, choices=AUTHORIZED),
        verbose_name=_('Authorized Countries'),
        default=authorized_default,
    )

    allow_premium_rate = models.BooleanField(
        _('Allow premium rate numbers'),
        default=False,
        help_text=_('This option is only applyed to France mainland'),
    )

    @classmethod
    def get_management_urls(cls):
        return import_string('passerelle.sms.urls.management_urlpatterns')

    @classmethod
    def get_statistics_urls(cls):
        from .views import SmsStatisticsView

        statistics_urlpatterns = [
            re_path(
                r'^(?P<slug>[\w,-]+)/sms-count/$',
                SmsStatisticsView.as_view(),
                name='api-statistics-sms-%s' % cls.get_connector_slug(),
            ),
        ]
        return statistics_urlpatterns

    def _get_authorized_display(self):
        result = []
        for key, value in self.AUTHORIZED:
            if key in self.authorized:
                result.append(str(value))
        return ', '.join(result)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.get_authorized_display = self._get_authorized_display

    def clean_numbers(self, destinations):
        numbers = []
        for dest in destinations:
            # most gateways needs the number prefixed by the country code, this is
            # really unfortunate.
            dest = dest.strip()
            number = ''.join(re.findall('[0-9]', dest))
            if dest.startswith('+'):
                number = '00' + number
            elif number.startswith('00'):
                # assumes 00 is international access code, remove it
                pass
            elif number.startswith(self.default_trunk_prefix):
                number = '00' + self.default_country_code + number[len(self.default_trunk_prefix) :]
            else:
                raise APIError(
                    'phone number %r is unsupported (no international prefix, '
                    'no local trunk prefix)' % number
                )
            numbers.append(number)
        return numbers

    def authorize_numbers(self, destinations):
        number_regexes = {
            'premium_rate': [r'^0033[8]\d{8}$'],
            SMSResource.FR_METRO: [r'^0033[67]\d{8}$'],
            SMSResource.FR_DOMTOM: [
                r'^00262262\d{6}$',  # Réunion, Mayotte, Terres australe/antarctiques
                r'^508508\d{6}$',  # Saint-Pierre-et-Miquelon
                r'^590590\d{6}$',  # Guadeloupe, Saint-Barthélemy, Saint-Martin
                r'^594594\d{6}$',  # Guyane
                r'^596596\d{6}$',  # Martinique
                r'^00687[67]\d{8}$',  # Nouvelle-Calédonie
            ],
            SMSResource.BE_: [r'^00324[5-9]\d{7}$'],
        }

        premium_numbers = set()
        if not self.allow_premium_rate:
            regex = re.compile('|'.join(number_regexes['premium_rate']))
            premium_numbers = {dest for dest in destinations if regex.match(dest)}

        foreign_numbers = set()
        if SMSResource.ALL not in self.authorized:
            regexes = []
            for country in self.authorized:
                regexes += number_regexes[country]
            regex = re.compile('|'.join(regexes))
            foreign_numbers = {dest for dest in destinations if not regex.match(dest)}

        authorized_numbers = sorted(set(destinations) - foreign_numbers - premium_numbers, key=int)

        premium_numbers_string = ", ".join(sorted(premium_numbers, key=int))
        foreign_numbers_string = ", ".join(sorted(foreign_numbers - premium_numbers, key=int))
        if premium_numbers_string:
            logging.warning('unauthorized premium rate phone number: %s', premium_numbers_string)
        if foreign_numbers_string:
            logging.warning('unauthorized foreign phone number: %s', foreign_numbers_string)
        if len(authorized_numbers) == 0:
            raise APIError('no phone number was authorized: %s' % ', '.join(destinations))
        warnings = {
            'deny premium rate phone numbers': premium_numbers_string,
            'deny foreign phone numbers': foreign_numbers_string,
        }
        return authorized_numbers, warnings

    @endpoint(
        perm='can_send_messages',
        methods=['post'],
        description=_('Send a SMS message'),
        parameters={'nostop': {'description': _('Do not send STOP instruction'), 'example_value': '1'}},
        post={'request_body': {'schema': {'application/json': SEND_SCHEMA}}},
    )
    def send(self, request, post_data, nostop=None):
        post_data['message'] = post_data['message'][: self.max_message_length]
        post_data['to'] = self.clean_numbers(post_data['to'])
        post_data['to'], warnings = self.authorize_numbers(post_data['to'])
        logging.info('sending SMS to %r from %r', post_data['to'], post_data['from'])
        stop = nostop is None  # ?nostop in not in query string
        self.add_job(
            'send_job',
            text=post_data['message'],
            sender=post_data['from'],
            destinations=post_data['to'],
            stop=stop,
        )
        return {'err': 0, 'warn': warnings}

    def send_job(self, *args, **kwargs):
        credits_spent = self.send_msg(**kwargs)
        SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug, credits=credits_spent)
        return {'status_info': {'credits-spent': credits_spent}}

    def get_statistics_entries(self, request):
        return [
            {
                'name': _('SMS Count (%s)') % self.slug,
                'url': request.build_absolute_uri(
                    reverse('api-statistics-sms-%s' % self.get_connector_slug(), kwargs={'slug': self.slug}),
                ),
                'id': 'sms_count_%s_%s' % (self.get_connector_slug(), self.slug),
                'filters': [
                    {
                        'id': 'time_interval',
                        'label': _('Interval'),
                        'options': [{'id': 'day', 'label': _('Day')}],
                        'required': True,
                        'default': 'day',
                    },
                ],
            }
        ]

    class Meta:
        abstract = True


class SMSLog(models.Model):
    timestamp = models.DateTimeField(auto_now_add=True)
    appname = models.CharField(max_length=128, verbose_name='appname', null=True)
    slug = models.CharField(max_length=128, verbose_name='slug', null=True)
    credits = models.PositiveSmallIntegerField(null=True)

    def __str__(self):
        return '%s %s %s' % (self.timestamp, self.appname, self.slug)
