#! python
# -*- coding: utf-8 -*-
# This is a utility module for integrating SMS providers into VizAlerts.

import os
import re
import phonenumbers
import twilio

# import local modules
import config
import log
import vizalert

# store the SMS client we get back from Twilio for use across all modules
smsclient = None

# regular expression used to split recipient number strings into separate phone numbers
SMS_RECIP_SPLIT_REGEX = u'[;,]*'

# appended to the bottom of all SMS messages, unless overidden
# expecting smsfooter.format(subscriber_email)
smsfooter = u'\r\rThis VizAlert SMS sent on behalf of {}'


class SMS:
    """Represents an SMS to be sent"""

    def __init__(self, sms_from, sms_to, msgbody=None):
        self.sms_from = sms_from
        self.sms_to = sms_to
        self.msgbody = msgbody  # REVISIT--why is it okay for this to be blank?


def get_sms_client():
    """Generic function get an SMS client object. This only works with Twilio at this time."""

    # check to see if there's a provider set
    if config.configs['smsaction.provider'] == None or len(config.configs['smsaction.provider']) == 0:
        errormessage = u'SMS Actions are enabled but smsaction.provider value is not set, exiting'
        log.logger.error(errormessage)
        raise ValueError(errormessage)        

    # load code for Twilio
    elif config.configs['smsaction.provider'].lower() == 'twilio': 
        # these need to be in the global name space to send SMS messages
        global twilio
        import twilio

        # Monkey patch to allow Twilio to find the cacert.pem file even when compiled into an exe
        #    See: https://stackoverflow.com/questions/17158529/fixing-ssl-certificate-error-in-exe-compiled-with-py2exe-or-pyinstaller
        #       and https://github.com/twilio/twilio-python/issues/167
        ca_cert_path = os.path.join('twilio', 'conf', 'cacert.pem')

        from twilio.http import get_cert_file
        get_cert_file = lambda: ca_cert_path

        twilio.http.get_cert_file = get_cert_file

        global twiliorest
        import twilio.rest as twiliorest
        
        global smsclient
        smsclient = twiliorest.Client(
            config.configs['smsaction.account_id'],
            config.configs['smsaction.auth_token'])
        
        return smsclient

    # unknown SMS provider error
    else:
        errormessage = u'SMS Actions are enabled but found unknown smsaction.provider {}, exiting'.format(
            config.configs['smsaction.provider'])
        log.logger.error(errormessage)
        raise ValueError(errormessage)      


def send_sms(sms_instance):
    """REVISIT: This and the other SMS methods should probably be members of the SMS class

    function to send an sms using Twilio's REST API, see https://www.twilio.com/docs/python/install for details.
    Presumes that numbers have gone through a first level of checks for validity
    Returns nothing on success, error string back on failure"""

    # shouldn't happen but setting content to '' if it's None
    if not sms_instance.msgbody:
        sms_instance.msgbody = ''

    log.logger.info(u'Sending SMS: {},{},{}'.format(sms_instance.sms_from, sms_instance.sms_to, sms_instance.msgbody))

    # now to send the message
    try:
        if sms_instance.sms_from.startswith('+'):
            # kinda kloogy, but if we think it's an E.164 number, assume it is...
            message = smsclient.messages.create(body=sms_instance.msgbody, to=sms_instance.sms_to,
                                                from_=sms_instance.sms_from)
        else:
            # if not, assume it must be a message service SID
            message = smsclient.messages.create(body=sms_instance.msgbody, to=sms_instance.sms_to,
                                                messaging_service_sid=sms_instance.sms_from)

        # this may never happen since the Twilio REST API throws exceptions, it's a failsafe check
        if message.status == 'failed':
            raise ValueError(u'Failed to deliver SMS message to {} with body {},'
                             u' no additional information is available'.format(
                                sms_instance.sms_to,
                                sms_instance.msgbody))

    # check for Twilio REST API exceptions
    except twilio.base.exceptions.TwilioRestException as e:
        errormessage = u'Could not send SMS message to {} with body {}.\nHTTP status {} returned for request: ' \
                       u'{} {}\nWith error {}: {} '.format(
                            sms_instance.sms_to,
                            sms_instance.msgbody,
                            e.status,
                            e.method,
                            e.uri,
                            e.code,
                            e.msg)
        log.logger.error(errormessage)
        return errormessage

    # check for ValueError from try 
    except ValueError as e:
        log.logger.error(e)
        return e

    except Exception as e:
        errormessage = u'Could not send SMS message to {} with body {}, error {}, type {}'.format(
            sms_instance.sms_to,
            sms_instance.msgbody, e, e.__class__.__name__)
        log.logger.error(errormessage)
        return e

    return None


def sms_append_body(body, vizcompleterefs, row, alert):
    """Generic function for filling SMS body text with the body & footers from the csv
        plus inserting content references"""
    body.append(row[alert.action_field_dict[vizalert.SMS_MESSAGE_FIELDKEY].field_name])

    # add the footer if needed
    if alert.action_field_dict[vizalert.SMS_FOOTER_FIELDKEY].field_name:
        body.append(row[alert.action_field_dict[vizalert.SMS_FOOTER_FIELDKEY].field_name].replace(
            vizalert.DEFAULT_FOOTER,
            smsfooter.format(alert.subscriber_email)))
    else:
        # no footer specified, add the default footer
        body.append(smsfooter.format(alert.subscriber_email))

    # find all distinct content references in the email body list
    # so we can replace each with an inline image or hyperlink text
    foundcontent = re.findall(u'VIZ_LINK\(.*?\)', ' '.join(body))

    foundcontentset = set(foundcontent)
    vizrefs = list(foundcontentset)

    if len(vizrefs) > 0:
        for vizref in vizrefs:
            # we're replacing #VIZ_LINK text
            if vizcompleterefs[vizref]['formatstring'] == 'LINK':

                # always use raw link, ignore presence or absence of RAWLINK argument
                replacestring = alert.get_view_url(vizcompleterefs[vizref]['view_url_suffix'])
                replaceresult = vizalert.replace_in_list(body, vizref, replacestring)

                if replaceresult['foundstring']:
                    body = replaceresult['outlist']
    return body


def validate_smsnumbers(vizdata, sms_to_fieldname, allowed_recipient_numbers, iso2countrycode):
    """Loops through the viz data for an Advanced Alert and returns a list of dicts
        containing any errors found in recipients"""

    errorlist = []
    rownum = 2  # account for field header in CSV

    try:
        for row in vizdata:
            result = smsnumbers_are_invalid(row[sms_to_fieldname],
                                            False,  # empty string not acceptable as a To number
                                            iso2countrycode,
                                            allowed_recipient_numbers)
            if result:
                errorlist.append(
                    {'Row': rownum, 'Field': sms_to_fieldname, 'Value': result['number'], 'Error': result['errormessage']})

            rownum += 1
    except Exception as e:
        errormessage = u'Encountered error validating SMS numbers. Error: {}'.format(e.message)
        log.logger.error(errormessage)
        errorlist.append(errormessage)
        return errorlist
    return errorlist


def smsnumbers_are_invalid(sms_numbers, emptystringok, iso2countrycode, regex_eval=None):
    """Validates all SMS numbers found in a given string, optionally that conform to the regex_eval"""
    
    log.logger.debug(u'Validating SMS field value: {}'.format(sms_numbers))

    sms_number_list = re.split(SMS_RECIP_SPLIT_REGEX, sms_numbers.strip())

    for sms_number in sms_number_list:
        log.logger.debug(u'Validating presumed sms number: {}'.format(sms_number))

        try:
            # skip if we're okay with empty, and it is
            if sms_number == '':
                if not emptystringok:
                    errormessage = u'SMS number is empty'
                else:
                    continue
            else:
                errormessage = smsnumber_is_invalid(sms_number, iso2countrycode, regex_eval)

            if errormessage:
                log.logger.debug(u'SMS number is invalid: {}, Error: {}'.format(sms_number, errormessage))
                if len(sms_number) > 64:
                    sms_number = sms_number[:64] + '...'  # truncate a too-long address for error formattting purposes
                return {'number': sms_number, 'errormessage': errormessage}
        except Exception as e:
            errormessage = u'Encountered error validating an SMS number. Error: {}'.format(e.message)
            log.logger.error(errormessage)
            return {'number': sms_number, 'errormessage': errormessage}
    return None


def smsnumber_is_invalid(smsnumber, iso2countrycode, regex_eval=None):
    """Checks for a syntactically invalid phone number, returns None for success or an error message"""

    try:
        e164_number = smsnumber_to_e164(smsnumber, iso2countrycode)

        # looks valid, but it must be permitted by regex pattern if so specified
        if regex_eval:
            log.logger.debug("testing smsnumber {} against regex {}".format(e164_number, regex_eval))
            if not re.match(regex_eval, e164_number):
                errormessage = u'SMS number must match regex pattern set by the administrator: {}'.format(regex_eval)
                log.logger.error(errormessage)
                return errormessage
    except Exception as e:
        return e.message

    # looks like it was fine!
    return None


def get_e164numbers(sms_numbers, iso2countrycode):
    """Converts a delimited string or list of SMS numbers to E.164 format
        Returns a UNIQUE list of E.164 numbers
        NOTE: This method ASSUMES that they have all been validated already """
    sms_number_list = []
    e164_numbers = []

    if isinstance(sms_numbers, str) or isinstance(sms_numbers, unicode):
        sms_number_list.extend(re.split(SMS_RECIP_SPLIT_REGEX, sms_numbers.strip()))
    elif isinstance(sms_numbers, list):
        sms_number_list.extend(sms_numbers)
    else:
        # that's not what we expected
        errormessage = u'Input is neither a string nor a list: {}'.format(sms_numbers)
        log.logger.error(errormessage)
        raise UserWarning(errormessage)

    # convert and add each number to our return list
    for sms_number in sms_number_list:
        log.logger.debug(u'Converting {} to E.164 format'.format(sms_number))
        try:
            e164_number = smsnumber_to_e164(sms_number, iso2countrycode)
            if e164_number not in e164_numbers:
                e164_numbers.append(e164_number)
        except Exception as e:
            raise UserWarning(e.message)

    return e164_numbers


def smsnumber_to_e164(smsnumber, iso2countrycode):
    """Tries to convert a string into an E.164 formatted phone number
       Raises exception if it can't, returns the E.164 number as a string, if it can """

    try:
        log.logger.debug(u'Converting {} to E.164 format, country code {}'.format(smsnumber, iso2countrycode))

        try:
            if smsnumber.startswith('+'):
                smsnumber_obj = phonenumbers.parse(smsnumber)
            else:
                # country code not specified in number, so pass it in
                smsnumber_obj = phonenumbers.parse(smsnumber, iso2countrycode)
        except phonenumbers.NumberParseException as e:
            errormessage = u'SMS Unable to parse number {}. Error: {}'.format(smsnumber, e.message)
            log.logger.error(errormessage)
            raise UserWarning(errormessage)

        try:
            if not phonenumbers.is_possible_number(smsnumber_obj):
                errormessage = u'SMS Number is not possibly valid: {}.'.format(smsnumber)
                log.logger.error(errormessage)
                raise UserWarning(errormessage)
        except phonenumbers.NumberParseException as e:
            errormessage = u'SMS Unable to parse number {}. Error: {}'.format(smsnumber, e.message)
            log.logger.error(errormessage)
            raise UserWarning(errormessage)

        if not phonenumbers.is_valid_number(smsnumber_obj):
            errormessage = u'SMS Number is not valid: {}.'.format(smsnumber)
            log.logger.error(errormessage)
            raise UserWarning(errormessage)


        e164_number = phonenumbers.format_number(smsnumber_obj, phonenumbers.PhoneNumberFormat.E164)
        if not e164_number:
            errormessage = u'SMS number {} could not be converted to E.164 for an unknown reason.'.format(smsnumber)
            log.logger.error(errormessage)
            raise UserWarning(errormessage)

        # all good, return it!
        return e164_number
    except Exception as e:
        log.logger.error(e.message)
        return None