HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux spn-python 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 x86_64
User: arjun (1000)
PHP: 8.1.2-1ubuntu2.20
Disabled: NONE
Upload Files
File: //home/arjun/projects/buyercall_forms/buyercall/buyercall/blueprints/widgets/routing.py
import logging
import random
import traceback
from datetime import datetime
import json
from flask import (
    url_for, current_app as app,
    request
)
from sqlalchemy import and_, or_
from twilio.base.exceptions import TwilioRestException
import redis
import pytz
import xml.etree.ElementTree as et
from buyercall.extensions import db
from buyercall.lib.flask_mailplus import _try_renderer_template
from buyercall.lib.util_lists import first, to_string
from buyercall.lib.util_twilio import (
    BusyAgents,
    CallStorage,
    InboundCallState as State,
    subaccount_client,
)
from buyercall.lib.util_webhooks import WebhookUtil
from ..agents.models import Agent
from ..user.models import User
from ..leads.models import Lead
from buyercall.blueprints.filters import format_phone_number
AGENT_TIMEOUT = 23

DAYS = 86400  # The length of a day in seconds

AGENT_WHISPER_HELLO = """Hello ... .."""

AGENT_WHISPER_LEAD_NAME = """The lead's name is: {} . {} ."""

AGENT_WHISPER_QUESTION = """The lead is asking: {}. """

log = logging.getLogger(__name__)
webhooker = WebhookUtil()

class FileFormatException(Exception):
    pass


class NoAgentsException(Exception):
    pass


def phonecall_inprogress(widget, phoneNumber='', **kwargs):
    if widget and phoneNumber:
        phone_number = normalize(phoneNumber)

        lead_on_call = Lead.query\
            .filter(
                phone_number == Lead.phonenumber,
                widget.partnership_account_id == Lead.partnership_account_id)\
            .filter(or_(
                Lead.STATUSES['in-progress'] == Lead.status,
                Lead.STATUSES['ringing'] == Lead.status))\
            .first()

        if lead_on_call:
            log.info('Cancelling widget {0} call request. Lead {1} with phonenumber {2} currently on call.'
                     .format(widget.id,
                             lead_on_call.id,
                             lead_on_call.phonenumber))
            return True

    return False


def add_widget_lead(
    widget, firstName='', lastName='', emailAddress='', phoneNumber='',
    sourceTag='', question='', status='ringing', **kwargs
):
    if widget.inbound is None:
        raise Exception('Widget {} has no inbound phone associated'.format(
            widget.id
        ))

    my_phone = widget.inbound.phonenumber
    inbound_id = widget.inbound.id
    sourceTag = widget.name
    phone_number = normalize(phoneNumber)

    if not firstName and not lastName:
        firstName, lastName = Lead.get_last_known_name(
            widget.partnership_account_id, phone_number
        )

    previous_lead = Lead.query.filter(
        phone_number == Lead.phonenumber, widget.partnership_account_id == Lead.partnership_account_id
    ).first()

    if previous_lead is not None:
        progress_status = previous_lead.progress_status
        agent_assigned = previous_lead.agent_assigned_id
    else:
        progress_status = 'no status'
        agent_assigned = None

    lead = Lead(
        partnership_account_id=widget.partnership_account_id,
        firstname=firstName,
        lastname=lastName,
        email=emailAddress,
        phonenumber=phone_number,
        progress_status=progress_status,
        question=question,
        starttime=datetime.utcnow(),
        call_type='outbound',
        status=status,
        my_phone=my_phone,
        widget_guid=widget.guid,
        additional_info=kwargs,
        call_source=sourceTag,
        agent_assigned_id=agent_assigned,
        originating_number=my_phone,
        inbound_id=inbound_id,
    )

    db.session.add(lead)
    db.session.commit()

    # Add contact based on new lead using celery
    from .tasks import add_contact
    add_contact(phone_number, firstName, lastName, emailAddress, widget.partnership_account_id)

    return lead


def normalize(phone_number):
    """ Convert a US phone number into an E.123 format.
    """
    # str.translate and unicode.translate work differently, so we make sure we
    # have an str object.
    stripped = phone_number = str(phone_number).strip()
    if not phone_number.startswith('sip:'):
        stripped = phone_number.translate(str.maketrans({' ': '', '.': '', '(': '', '-': '', ')': ''}))
    if stripped.startswith('+'):
        return stripped
    if len(stripped) == 10:  # US number w/o country code
        return '+1' + stripped
    if len(stripped) == 11 and stripped.startswith('1'):  # Missing '+'
        return '+' + stripped

    return stripped


def get_available_agent_ids(widget):
    """Return a list of available agent IDs for a widgets."""
    agent_ids = [
        asg.agent_id
        for asg in sorted(widget.assignments, key=lambda x: x.order)
        if asg.agent.available_now
    ]
    free_agents = BusyAgents.filter_from(agent_ids)
    return free_agents


class Routing(object):

    def __init__(self, client):
        self.client = client

    def save_lead_agent(self, lead_id, agent_id):
        """ Save the agent on call to the database for this lead.
        Return True if this is the agent chosen for the call, or False
        if some other agent picked up first.
        """
        # Declare redis url
        redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

        storage = CallStorage(redis_db, lead_id)
        called_agents_key = 'LIST{}'.format(lead_id)

        with storage.lock():
            if not storage.agent_id:
                storage.agent_id = agent_id
            else:
                # Someone else picked up; exit
                return False

        # Cancel all other current calls
        calls = redis_db.lrange(called_agents_key, 0, -1)
        for call in calls:
            a, sid = call.split('_')
            if int(a) != agent_id:
                log.info('Canceling call for agent {}...'.format(a))
                self.client.calls.hangup(sid)
        storage.agent_call_sid = request.args['callId']

        lead = Lead.query.filter(Lead.id == lead_id).first()
        lead.agent_id = agent_id
        db.session.commit()

        return True

    def call_lead(self, widget, lead):
        # Declare redis url
        redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

        storage = CallStorage(redis_db, lead.id)
        subscription = widget.partnership_account.subscription
        if not subscription:
            subscription = widget.partnership_account.partnership.subscription

        storage.subaccount_sid = subscription.twilio_subaccount_sid
        storage.clear_callback_cnt()
        storage.state = State.NEW

        webhooker.trigger_generic_webhook('operational_start_call', lead.id)

        # Route the call
        agent_ids = get_available_agent_ids(widget)
        if not agent_ids:
            log.warning('No agents available for widget {}.'.format(widget.id))
            lead.status = 'missed'
            db.session.commit()
            send_notify_email(lead, widget)
            raise NoAgentsException(lead)

        if widget.options['routeSimultaneously']:
            self.schedule_call_parallel(lead)
            return lead

        if widget.options['routeRandomly']:
            random.shuffle(agent_ids)

        self.route_sequential(agent_ids, widget, lead)

        return lead

    def schedule_call_parallel(self, lead):
        from .tasks import call_parallel
        call_parallel.delay(lead.id)

    def call_agent_number(
        self, agent, widget, lead_id, url=None, status_callback=None,
        status_events=None, timeout=None, from_=None, if_machine=None, **kwargs
    ):
        """ Call either the agent's phone number, mobile, or extension
        """
        # Declare redis url
        redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

        lead = Lead.query.filter(Lead.id == lead_id).first()
        if not lead:
            log.warning('Lead not found in the database')
            return

        # import partnership information to get partnership id
        from ..partnership.models import Partnership, PartnershipAccount
        partner_account = PartnershipAccount.query \
            .filter(PartnershipAccount.id == lead.partnership_account_id).first()
        partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

        storage = CallStorage(redis_db, lead_id)
        twilio_client = subaccount_client(storage.subaccount_sid, partner.id)
        send_digits = None

        wa = first(widget.assignments, lambda x: x.agent_id == agent.id)
        if not wa:
            log.error(
                'Cannot find agent {} in widget {}\'s list of agents'.format(
                    agent.full_name, widget.id
                ))
            return

        if wa.contact_using == 'phone':
           to = agent.phonenumber
        elif wa.contact_using == 'mobile':
           to = agent.mobile
        elif wa.contact_using == 'extension':
            to = agent.phonenumber
            send_digits = 'wwww' + agent.extension if agent.extension else None
        else:
            raise Exception(
                'Unknown calling method {} for agent {}, widget {}'.format(
                    repr(wa.contact_using), agent.full_name, widget.id
                ))

        return twilio_client.calls.create(
            to=to,
            from_=from_,
            url=url,
            status_callback=status_callback,
            status_callback_event=status_events,
            send_digits=send_digits,
            timeout=timeout,
        )

    def route_sequential(self, agent_ids, widget, lead):
        # Declare redis url
        redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

        # Remove any agents that joined a call in the meantime
        agent_ids = BusyAgents.filter_from(agent_ids)

        agent = None
        while agent_ids:
            agent = Agent.query.filter(and_(
                Agent.partnership_account_id == lead.partnership_account_id,
                Agent.id == agent_ids[0]
            )).first()
            if agent:
                break
            agent_ids = agent_ids[1:]

        log.info('The lead call status is: {}'.format(lead.status))

        if not agent_ids or agent is None or lead.status == 'completed':
            log.error('No more agents available for widget {}.'.format(
                widget.id
            ))
            redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')
            if lead.status != 'completed':
                lead.status = 'missed'
                db.session.commit()
                send_notify_email(lead, widget)
                schedule_retries(lead)
            return

        # Reset the agent who answered
        redis_db.setex('CALL{}'.format(lead.id), 4 * DAYS, '0')

        try:
            log.debug('Trying call from {} to agent {}...'.format(
                widget.name, agent.id
            ))
            self.call_agent_sequential(
                lead, agent, widget, agent_ids)
            db.session.commit()
        except TwilioRestException:
            log.error('Error caling agent {}; retrying...'.format(agent_ids[0]))
            self.route_sequential(agent_ids[1:], widget, lead)

    def call_agent_sequential(self, lead, agent, widget, agent_ids):
        return self.call_agent_number(
            agent,
            widget,
            url=url_for(
                'twilio_api.agent_digit_prompt',
                lead_id=lead.id,
                agent_id=agent.id,
                _external=True,
                _scheme='https'
            ),
            status_callback=url_for(
                'twilio_api.agent_sequential_call_status',
                widget_guid=widget.guid,
                lead_id=lead.id,
                agent_id=agent.id,
                other_agents=to_string(agent_ids[1:]),
                _external=True,
                _scheme='https'
            ),
            status_callback_event=['ringing', 'answered', 'completed'],
            timeout=AGENT_TIMEOUT,
            from_=widget.inbound.phonenumber,
            lead_id=lead.id,
        )

    def call_agent_parallel(self, lead, agent, widget):
        return self.call_agent_number(
            agent,
            widget,
            url=url_for(
                'twilio_api.agent_digit_prompt',
                lead_id=lead.id,
                agent_id=agent.id,
                _external=True,
                _scheme='https'
            ),
            status_callback=url_for(
                'twilio_api.agent_parallel_call_status',
                agent_id=agent.id,
                lead_id=lead.id,
                _external=True,
                _scheme='https'
            ),
            status_callback_event=['ringing', 'answered', 'completed'],
            timeout=AGENT_TIMEOUT,
            # from_=widget.inbound.phonenumber,
            # FIXME:
            from_=widget.inbound.phonenumber,
            lead_id=lead.id,
        )


class BandwidthRouting(Routing):

    def __init__(self, client):
        self.client = client

    """
    call_lead: Entry point for widget calls. The api/call endpoint gets hit when a widget call request is made
    through the widget gui that triggers this call_lead which have authenticated with Bandwidth.
    """
    def call_lead(self, widget, lead):
        # Declare redis url
        redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

        # Initiate the redis storage used to store call details for handling call
        storage = CallStorage(redis_db, lead.id)

        # confirm the account's subscription. If not on account level then on partner level.
        subscription = widget.partnership_account.subscription
        if not subscription:
            subscription = widget.partnership_account.partnership.subscription

        storage.subaccount_sid = subscription.twilio_subaccount_sid
        storage.clear_callback_cnt()
        storage.state = State.NEW

        # Fire off webhook for start of call
        webhooker.trigger_generic_webhook('operational_start_call', lead.id)

        # Get agents to whom the call needs to forward to
        agent_ids = get_available_agent_ids(widget)
        if not agent_ids:
            log.warning('No agents available for widget {}.'.format(widget.id))
            lead.status = 'missed'
            db.session.commit()
            send_notify_email(lead, widget)
            raise NoAgentsException(lead)
        # If the widget routing is set to simultaneous then use this function
        if widget.options['routeSimultaneously']:
            self.schedule_call_parallel(lead)
            return lead
        # Shuffle agents if random call routing is set for the widget
        if widget.options['routeRandomly']:
            random.shuffle(agent_ids)

        self.route_sequential(agent_ids, widget, lead)

        return lead

    def schedule_call_parallel(self, lead):
        from .bw_tasks import call_parallel
        call_parallel.delay(lead.id)

    def call_agent_number(
        self, agent, widget, lead_id, url=None, status_callback=None,
        status_events=None, timeout=None, from_=None, if_machine=None, **kwargs
    ):
        """ Call either the agent's phone number, mobile, or extension
        """
        answer_callback_url = kwargs.get('answer_callback_url', '')
        answer_fallback_callback_url = kwargs.get('answer_fallback_callback_url', '')
        disconnect_callback_url = kwargs.get('disconnect_callback_url', '')
        wa = first(widget.assignments, lambda x: x.agent_id == agent.id)
        if not wa:
            log.error(
                'Cannot find agent {} ({}) in widget {}\'s list of agents'.format(
                    agent.full_name, agent.id, widget.id
                ))
            raise ValueError()

        if wa.contact_using == 'phone':
            to = agent.phonenumber
        elif wa.contact_using == 'mobile':
            to = agent.mobile
        elif wa.contact_using == 'extension':
            to = agent.phonenumber
            # FIXME: Implement dialing an extension
            # digits = 'wwww' + agent.extension if agent.extension else None
        else:
            raise Exception(
                'Unknown calling method {} for agent {}, widget {}'.format(
                    repr(wa.contact_using), agent.full_name, widget.id
                ))

        # Check to see if the agent number is a sip mobile number then use sip uri
        from buyercall.blueprints.phonenumbers.models import Phone
        buyercall_sip_lookup = Phone.mobile_sip_uri(to)
        if buyercall_sip_lookup:
            to = buyercall_sip_lookup
        else:
            to = format_phone_number(to)

        # Set the answering machine detecting callback
        amd_callback_url = url_for(
            'bw_outbound.manual_amd_webhook',
            party='agent',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        )
        amd_fallback_callback_url = url_for(
            'bw_outbound.manual_amd_fallback_webhook',
            party='agent',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        )

        agent_call = self.client.call.create(
            c_from=from_,
            c_to=to,
            answer_callback_url=answer_callback_url,
            disconnect_callback_url=disconnect_callback_url,
            answer_fallback_callback_url=answer_fallback_callback_url,
            tag='start-agent-widget-outbound',
            call_timeout=AGENT_TIMEOUT,
            machine_detection={"callbackUrl": amd_callback_url,
                               "fallbackUrl": amd_fallback_callback_url,
                               "speechThreshold": 4,
                               "speechEndThreshold": 1,
                               "silenceTimeout": 120,
                               "detectionTimeout": 120,
                               }
        )
        log.error('The BW call API error is: {}'.format(agent_call))
        json_call_string = json.loads(agent_call)
        agent_call_id = json_call_string.get('callId', '')
        return agent_call_id

    def route_sequential(self, agent_ids, widget, lead):
        # Declare redis url
        redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'],
                                     port=app.config['REDIS_CONFIG_PORT'])

        # Remove any agents that joined a call in the meantime
        agent_ids = BusyAgents.filter_from(agent_ids)

        agent = None
        while agent_ids:
            agent = Agent.query.filter(and_(
                Agent.partnership_account_id == lead.partnership_account_id,
                Agent.id == agent_ids[0]
            )).first()
            if agent:
                break
            agent_ids = agent_ids[1:]

        log.info('The lead call status is: {}'.format(lead.status))

        if not agent_ids or agent is None or lead.status == 'completed':
            log.error('No more agents available for widget {}.'.format(
                widget.id
            ))
            redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')
            if lead.status != 'completed':
                lead.status = 'missed'
                db.session.commit()
                send_notify_email(lead, widget)
                schedule_retries(lead)
            return

        # Reset the agent who answered
        redis_db.setex('CALL{}'.format(lead.id), 4 * DAYS, '0')

        try:
            log.debug('Trying call from {} to agent {}...'.format(
                widget.name, agent.id
            ))
            self.call_agent_sequential(
                lead, agent, widget, agent_ids)
            db.session.commit()
        except Exception as e:
            log.error('Error calling agent {}; Error: {}; retrying...'.format(agent_ids[0], e))
            self.route_sequential(agent_ids[1:], widget, lead)
        return ''

    def call_agent_sequential(self, lead, agent, widget, agent_ids):
        return self.call_agent_number(
            agent,
            widget,
            answer_callback_url=url_for(
                'bw_outbound.widget_outbound_agent_answer_webhook',
                lead_id=lead.id,
                agent_id=agent.id,
                from_tn=widget.inbound.phonenumber,
                _external=True,
                _scheme='https'
            ),
            answer_fallback_callback_url=url_for(
                'bw_outbound.widget_outbound_agent_answer_fallback_webhook',
                lead_id=lead.id,
                agent_id=agent.id,
                from_tn=widget.inbound.phonenumber,
                _external=True,
                _scheme='https'
            ),
            disconnect_callback_url=url_for(
                'bw_outbound.widget_sequence_outbound_agent_disconnect_webhook',
                lead_id=lead.id,
                agent_id=agent.id,
                from_tn=widget.inbound.phonenumber,
                other_agents=to_string(agent_ids[1:]),
                _external=True,
                _scheme='https'
            ),
            timeout=AGENT_TIMEOUT,
            from_=widget.inbound.phonenumber,
            lead_id=lead.id,
        )

    def call_agent_parallel(self, lead, agent, widget):
        return self.call_agent_number(
            agent,
            widget,
            answer_callback_url=url_for(
                'bw_outbound.widget_outbound_agent_answer_webhook',
                lead_id=lead.id,
                agent_id=agent.id,
                from_tn=widget.inbound.phonenumber,
                _external=True,
                _scheme='https'
            ),
            answer_fallback_callback_url=url_for(
                'bw_outbound.widget_outbound_agent_answer_fallback_webhook',
                lead_id=lead.id,
                agent_id=agent.id,
                from_tn=widget.inbound.phonenumber,
                _external=True,
                _scheme='https'
            ),
            disconnect_callback_url=url_for(
                'bw_outbound.widget_parallel_outbound_agent_disconnect_webhook',
                lead_id=lead.id,
                agent_id=agent.id,
                from_tn=widget.inbound.phonenumber,
                _external=True,
                _scheme='https'
            ),
            timeout=AGENT_TIMEOUT,
            from_=widget.inbound.phonenumber,
            lead_id=lead.id,
        )


def get_agent_text(widget, lead):
    """ Builds the text that the call center agent will head before picking up a
    call. Agent Text = Custom Lead Message + Fullname + Question + Hidden Information + .
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

    agent_text = AGENT_WHISPER_HELLO

    custom_lead_message = redis_db.get('CUSTOM_LEAD_MESSAGE:' + str(lead.id))

    if lead.full_name:
        agent_text += AGENT_WHISPER_LEAD_NAME.format(
            lead.firstname, lead.lastname
        )

    if lead.question:
        agent_text += AGENT_WHISPER_QUESTION.format(lead.question)

    if widget.options.get('addHiddenInformation', False):
        agent_text += widget.options.get('hiddenInformation', '').strip()

    if agent_text and not agent_text.endswith('.'):
        agent_text += '. '

    if custom_lead_message is not None and custom_lead_message != "" and lead.call_source is not None and lead.call_source.lower() == 'adf':
        agent_text = agent_text + ' . . . ' + custom_lead_message

    return agent_text


def to_status(call_status):
    """ Translate a Twilio call status into a Buyercall lead status.
    """
    if call_status in ['busy', 'failed', 'canceled']:
        return 'missed'
    if call_status in ['no-answer']:
        return 'unanswered'
    if call_status in ['initiated', 'queued']:
        return 'ringing'
    if call_status in ['in-progress', 'completed']:
        return call_status
    return 'other'


def after_call_events(lead, widget):
    from buyercall.lib.util_ses_email import send_ses_email
    # send notification emails
    send_notify_email(lead, widget)

    # send CRM email
    if widget.is_adf is True and str(lead.call_source).lower() == 'adf':
        comment_text = "Call status: " + lead.status
        agent_name = lead.agent_name
        adf_emails = widget.adf_notification_emails
        lead_source = lead.source
        widget_phonenumber = widget.inbound.phonenumber
        from buyercall.blueprints.partnership.models import PartnershipAccount
        partner_account = PartnershipAccount.query\
            .filter(PartnershipAccount.id == widget.partnership_account_id).first()

        if agent_name is not None and agent_name is not '':
            comment_text += ", Agent name: " + agent_name

        if lead_source is not None and lead_source is not '':
            comment_text += ", Call source: " + lead_source

        if widget_phonenumber is not None and widget_phonenumber is not '':
            comment_text += ", Phonenumber: " + widget_phonenumber

        if widget.options.get('addHiddenInformation', False) and lead.status is 'completed':
            hidden_info = widget.options.get('hiddenInformation', '').strip()
            if hidden_info is not None and hidden_info is not '' and hidden_info is not ' ':
                comment_text += ", Additional Information: " + hidden_info

        email_body = update_adf_comment(lead.id, comment_text)

        if len(email_body) > 0:
            log.error("Sending CRM redirect email.")

            if adf_emails is not None and len(adf_emails) > 0:
                send_ses_email(recipients=adf_emails,
                               p_id=partner_account.partnership_id,
                               subject='ADF lead notification',
                               text=email_body
                               )


def send_notify_email(lead, widget):
    from buyercall.lib.util_ses_email import send_ses_email
    # Declare redis url
    redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

    try:
        if widget.options['notifyNone']:
            return
        if widget.options['notifyMissedLeads'] and lead.status != 'missed':
            return

        user = User.query.filter(
            User.partnership_account_id == lead.partnership_account_id,
            User.active,
            User.is_deactivated.is_(False),
            User.role == 'admin'
        ).first()

        emails = widget.emails
        app.logger.debug('The email list are {}'.format(emails))

        from ..partnership.models import Partnership, PartnershipAccount
        partner_account = PartnershipAccount.query.filter(PartnershipAccount.id == lead.partnership_account_id).first()
        partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

        ctx = vars(lead)

        eastern = pytz.timezone('US/Eastern')

        ctx['created_on'] = lead.created_on.astimezone(eastern).strftime('%c')
        ctx['updated_on'] = lead.updated_on.astimezone(eastern).strftime('%c')
        ctx['adf_updated_on'] = lead.interaction_time
        ctx['company'] = partner.name
        ctx['partner_logo'] = partner.logo

        if lead.firstname:
            ctx['caller_name_label'] = 'Caller Name:'
            ctx['caller_name'] = ''.join((lead.firstname, ' ', lead.lastname))

        if lead.widget:
            ctx['reference'] = lead.widget.options.get('hiddenInformation', '')

        if lead.source:
            ctx['call_source'] = lead.source

        if lead.status == 'missed':
            # Render html template for email
            missed_lead_email_template = _try_renderer_template('widgets/mail/missed_lead', ext='html', **ctx)
            send_ses_email(recipients=emails,
                           p_id=partner.id,
                           subject=partner.name + ' - Missed lead notification',
                           html=missed_lead_email_template
                           )
        else:
            # Render html template for email
            captured_lead_email_template = _try_renderer_template('widgets/mail/captured_lead', ext='html', **ctx)
            send_ses_email(recipients=emails,
                           p_id=partner.id,
                           subject=partner.name + ' - Answered lead notification',
                           html=captured_lead_email_template
                           )

        if widget.is_adf is True and str(lead.call_source).lower() != 'adf':
            agent_name = lead.agent_name
            adf_emails = widget.adf_notification_emails
            phonenumber = widget.inbound.phonenumber
            ctx['lead_comments'] = 'Call status: ' + lead.status
            ctx['vehicle_year'] = ''
            ctx['vehicle_make'] = ''
            ctx['vehicle_model'] = ''
            dealership = partner_account.name
            if dealership:
                ctx['vendor_name'] = dealership
            else:
                ctx['vendor_name'] = ''
            call_lead_id = lead.id
            if call_lead_id:
                ctx['call_lead_id'] = call_lead_id
            else:
                ctx['call_lead_id'] = ''

            custom_lead_vehicle_year = redis_db.get('CUSTOM_LEAD_YEAR:' + str(lead.id))
            if custom_lead_vehicle_year is not None:
                ctx['vehicle_year'] = custom_lead_vehicle_year

            custom_lead_vehicle_make = redis_db.get('CUSTOM_LEAD_MAKE:' + str(lead.id))
            if custom_lead_vehicle_make is not None:
                ctx['vehicle_make'] = custom_lead_vehicle_make

            custom_lead_vehicle_model = redis_db.get('CUSTOM_LEAD_MODEL:' + str(lead.id))
            if custom_lead_vehicle_model is not None:
                ctx['vehicle_model'] = custom_lead_vehicle_model

            if agent_name is not None and agent_name is not '' and len(agent_name) > 2:
                ctx['lead_comments'] = ctx['lead_comments'] + ', Agent name: ' + agent_name

            if lead.source is not None and lead.source is not '':
                ctx['receiver'] = lead.source
                ctx['lead_comments'] = ctx['lead_comments'] + ', Call source: ' + lead.source

            if phonenumber is not None and phonenumber is not '':
                ctx['lead_comments'] = ctx['lead_comments'] + ', Phonenumber: ' + phonenumber

            ctx['campaign_name'] = ''
            ctx['campaign_exists'] = False
            from buyercall.blueprints.contacts.models import Contact, Campaigns
            contact = Contact.query.filter(
                    Contact.id == lead.contact_id,
                    Contact.partnership_account_id == lead.partnership_account_id)\
                .first()
            if contact and contact.campaign_id:
                campaign = Campaigns.query.filter(
                    Campaigns.id == contact.campaign_id)\
                    .first()
                if campaign:
                    ctx['campaign_exists'] = True
                    ctx['campaign_name'] = campaign.display_name

            if adf_emails and len(adf_emails) > 0:
                # Render text template for adf email
                adf_lead_email_template = _try_renderer_template('widgets/mail/adf_lead', ext='txt', **ctx)
                send_ses_email(recipients=adf_emails,
                               p_id=partner.id,
                               subject=partner.name + ' - ADF lead notification',
                               text=adf_lead_email_template
                               )

    except Exception as e:
        log.error(traceback.format_exc())


def schedule_retries(lead):
    # Declare redis url
    redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

    if lead.status in ['ringing', 'completed']:
        # Nothing to do
        log.debug('Cannot schedule retries yet/lead status is ' + lead.status)
        return

    storage = CallStorage(redis_db, lead.id)
    cnt = storage.callback_cnt

    routing_delays = lead.widget.options['retryRouting']

    if len(routing_delays) <= cnt or not routing_delays[cnt]:
        return False

    delay = int(routing_delays[cnt])
    log.info('Scheduling retry call for lead {} in {} minutes'.format(
        lead.id, delay
    ))

    lead.status = 'retry-pending'
    db.session.commit()

    # Get the service provider from inbound id to know which tasks to call
    from ..phonenumbers.models import Phone
    number = Phone.query.filter(Phone.id == lead.inbound_id).first()

    try:
        if number.provider == 'bandwidth':
            from .bw_tasks import call_again
            call_again.apply_async(args=[lead.id], countdown=(delay * 60))
        else:
            # This is for twilio call-backs
            from .tasks import call_again
            call_again.apply_async(args=[lead.id], countdown=(delay * 60))
        return True
    except Exception as e:
        log.info('Unable to perform call-back or retry for lead: {} Error: {}'.format(lead.id, e))
        return False


def update_adf_comment(lead_id, comment):
    # Declare redis url
    redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

    custom_lead_xml = redis_db.get('CUSTOM_LEAD_XML:' + str(lead_id))

    if custom_lead_xml != None and len(custom_lead_xml) > 0:
        try:
            xmlentity = et.fromstring(custom_lead_xml)
            prospect = xmlentity.find('prospect')

            if prospect != None or prospect != "":
                if prospect.find('customer') is not None:
                    customer = prospect.find('customer')
                    if customer.find('comments') is not None:
                        customer_comments = customer.find('comments').text

                        if len(customer_comments) > 0:
                            customer.find('comments').text = customer_comments + " - " + comment
                        else:
                            customer.find('comments').text = customer_comments + comment
                    else:
                        new_comment = et.SubElement(customer, 'comments')
                        new_comment.text = comment
                else:
                    log.error("No customer found in the XML lead")

                return et.tostring(xmlentity, encoding='utf8', method='xml')
            else:
                log.error("No prospect found in the XML lead")
        except FileFormatException:
            log.error("Error parsing XML file. Exception: "+sys.exc_info())

    return ""