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/bw_outbound.py
from __future__ import print_function
import time
import logging as log
import traceback
import json
from dateutil import parser
from datetime import datetime

from flask import (
    Blueprint,
    current_app,
    request,
    url_for,
)
# from flask_cors import cross_origin
# from twilio import twiml
import redis
from sqlalchemy import and_
from buyercall.lib.util_lists import to_ints
from buyercall.lib.util_webhooks import WebhookUtil
from buyercall.blueprints.widgets.routing import NoAgentsException

from buyercall.extensions import db, csrf
# from buyercall.lib.util_lists import to_ints
from buyercall.lib.util_twilio import (
    BusyAgents,
    CallStorage,
    InboundCallState as State,
    select_voice
)
from buyercall.lib.util_bandwidth import bw_client
from buyercall.lib.bandwidth_bxml import create_xml_response, Response

from buyercall.blueprints.widgets.routing import (
    BandwidthRouting,
    get_agent_text, send_notify_email, schedule_retries
)
from buyercall.blueprints.filters import format_phone_number
from buyercall.blueprints.leads.models import Lead
from buyercall.blueprints.agents.models import Agent


webhooker = WebhookUtil()

# FIXME: Default Bandwidth voice
DEFAULT_VOICE = 'susan'

DAYS = 86400  # The length of a day in seconds

MSG_AGENT_KEY_PRESS_PROMPT = "...Press any number key to accept the call or the hash key to cancel."

MSG_LEAD_NOT_ANSWERING = 'The lead is not answering our call.'

AGENT_TIMEOUT = 23

bw_outbound = Blueprint('bw_outbound', __name__, template_folder='templates')


@bw_outbound.route(
    '/api/bw/outbound/agent/<int:lead_id>/<int:agent_id>',
    methods=['POST']
)
@csrf.exempt
def agent_status_callback(lead_id, agent_id):
    """ An agent is connected. Whisper the lead information to the agent, and
    dial the lead.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])

    args = request.json
    call_id = args.get('callId')
    event_type = args.get('eventType')
    tag = args.get('tag')

    log.debug(args)

    lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
    # import partnership information to get partnership id
    from buyercall.blueprints.partnership.models import Partnership, PartnershipAccount
    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()

    storage = CallStorage(redis_db, lead_id)
    client = bw_client(partner_account.partnership_id, 'voice')
    call = client.calls[call_id]
    call_info = client.calls.info(call_id)

    language = lead.widget.options.get('language', 'en')
    widget_digit_prompt = lead.widget.options.get('digitPrompt', True)
    gender, locale, voice = select_voice(language)

    if args.get('callState') == 'completed':
        print('Agent call completed')

    do_gather = False
    if tag == 'start' and event_type == 'answer':
        # update_response_time(call_status, agent_id, lead_id, redis_db)
        log.debug('Agent {} picked up'.format(agent_id))
        BusyAgents.add(agent_id)
        agent_text = get_agent_text(lead.widget, lead)
        storage.state = State.AGENT_ONHOLD
        storage.routing_config = lead.widget.options
        storage.agent_call_id = call_id
        lead.agent_id = agent_id
        lead.transfer_call_id = call_id
        db.session.commit()

        # If no whisper message, just continue
        if not agent_text:
            do_gather = True
        else:
            if lead.status != 'missed':
                call.audio(
                    sentence=agent_text, tag='hidden_info',
                    gender=gender, locale=locale, voice=voice
                )

    do_call_lead = False
    if do_gather or (
        tag == 'hidden_info' and args.get('state') == 'PLAYBACK_STOP'
    ):
        # If digit prompt is disabled for the phone number, just continue
        if not (lead.widget.inbound.routing_config.get('digitPrompt', True)
                or widget_digit_prompt):
            do_call_lead = True
        else:
            call.gather({
                "max_digits": 1,
                "prompt": {
                    "sentence": MSG_AGENT_KEY_PRESS_PROMPT,
                    "gender": gender,
                    "locale": locale
                },
                "tag": 'gather',
            })

    if do_call_lead or (
        tag == 'gather' and args.get('reason') == 'max-digits'
    ):
        from .bw_tasks import bw_call_lead

        # call.audio(sentence='Calling lead...')
        called_agents_key = 'LIST{}'.format(lead_id)

        with storage.lock():
            if not storage.agent_id:
                storage.agent_id = agent_id

        if int(storage.agent_id) == agent_id:
            # Cancel all other current calls
            calls = redis_db.lrange(called_agents_key, 0, -1)
            for call in calls:
                call_decoded = call.decode()
                a, sid = call_decoded.split('_')
                if int(a) != agent_id:
                    log.info('Canceling call for agent {}...'.format(a))
                    client.calls.hangup(sid)
        else:
            # Someone else picked up; exit
            log.debug('Agent {}: agent {} picked up; exit'.format(
                agent_id, agent_id
            ))
            return ''
        try:
            # Call lead
            bw_call_lead.delay(lead_id, agent_id)
        except Exception as e:
            log.info('Agent id: {} cancel call during call up'
                     'date for call id: {} and exception is: {}'.format(agent_id, call_id, e))

    if tag == 'hangup' and args.get('state') == 'PLAYBACK_STOP':
        call.hangup()

    if tag == 'gather' and args.get('reason') == 'inter-digit-timeout':
        call.audio(
            sentence="This call will be canceled now.", tag="canceling",
            gender=gender, locale=locale, voice=voice
        )

    if args.get('state') == 'PLAYBACK_STOP' and (
        tag == 'canceling' or tag == 'lead_call_error'
    ):
        call.hangup()

    if args.get('callState') == 'completed':
        BusyAgents.remove(agent_id)

        called_agents_key = 'LIST{}'.format(lead_id)

        with storage.lock():
            calls = redis_db.lrange(called_agents_key, 0, -1)
            for call in calls:
                call_decoded = call.decode()
                a, sid = call_decoded.split('_')
                if int(a) == agent_id:
                    log.info(
                        'Removing call for agent {} from list...'.format(a))
                    redis_db.lrem(called_agents_key, 1, call)
                    break
            ongoing_calls = redis_db.llen(called_agents_key)
            log.info('Still {} calls ongoing...'.format(ongoing_calls))

        retries_left = False
        if storage.state == State.NEW and not ongoing_calls:
            lead.status = 'missed'
            db.session.commit()
            retries_left = schedule_retries(lead)

        if ongoing_calls == 0 and not retries_left:
            try:
                call_duration = int(call_info["chargeableDuration"])
            except:
                call_duration = 0
            cause = args.get('cause')
            lead.transfer_call_id = call_id
            db.session.commit()
            agent_update_lead(lead, agent_id, call_duration, cause)
            try:
                pass
                client.calls.hangup(storage.call_sid)
            except Exception as e:
                log.info('Unable to hangup call with call id: {} because id does not exist. '
                         'Error: {}'.format(storage.agent_call_id, e))

    return ''


@bw_outbound.route(
    '/api/bw/outbound/agent_seq/<int:lead_id>/<int:agent_id>',
    methods=['POST']
)
@csrf.exempt
def agent_status_callback_sequential(lead_id, agent_id):
    """ Handle status updates when initiating calls agents in sequence.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])

    # The agent who's currently being called
    agent_id = agent_id

    args = request.json
    call_id = args.get('callId')
    event_type = args.get('eventType')
    tag = args.get('tag')

    lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()

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

    storage = CallStorage(redis_db, lead_id)
    client = bw_client(partner_account.partnership_id)
    call = client.calls[call_id]
    call_info = client.calls.info(call_id)

    language = lead.widget.options.get('language', 'en')
    widget_digit_prompt = lead.widget.options.get('digitPrompt', True)
    gender, locale, voice = select_voice(language)

    do_gather = False
    if tag == 'start' and event_type == 'answer':
        # update_response_time(call_status, agent_id, lead_id, redis_db)
        log.debug('Agent {} picked up'.format(agent_id))
        BusyAgents.add(agent_id)
        agent_text = get_agent_text(lead.widget, lead)
        storage.state = State.AGENT_ONHOLD
        storage.routing_config = lead.widget.options
        storage.agent_call_id = call_id
        lead.agent_id = agent_id
        lead.transfer_call_id = call_id
        db.session.commit()

        # If no whisper message, just continue
        if not agent_text:
            do_gather = True
        else:
            if lead.status != 'missed':
                call.audio(
                    sentence=agent_text, tag='hidden_info',
                    gender=gender, locale=locale, voice=voice
                )
    do_call_lead = False
    if do_gather or (
            tag == 'hidden_info' and args.get('state') == 'PLAYBACK_STOP'
    ):
        # If digit prompt is disabled for the phone number, just continue
        if not (lead.widget.inbound.routing_config.get('digitPrompt', True)
                or widget_digit_prompt):
            do_call_lead = True
        else:
            try:
                call.gather({
                    "max_digits": 1,
                    "prompt": {
                        "sentence": MSG_AGENT_KEY_PRESS_PROMPT,
                        "gender": gender,
                        "locale": locale
                    },
                    "tag": 'gather',
                })
            except Exception as e:
                log.error('An error occurred when trying '
                          'gather digit on sequence call: {}. Error: {}'.format(call_id, e))

    if do_call_lead or (
        tag == 'gather' and args.get('reason') == 'max-digits'
    ):
        from .bw_tasks import bw_call_lead
        try:
            # Call lead
            bw_call_lead.delay(lead_id, agent_id)
        except Exception as e:
            log.info('Agent id: {} cancel call during call up'
                     'date for call id: {} and exception is: {}'.format(agent_id, call_id, e))

    if tag == 'hangup' and args.get('state') == 'PLAYBACK_STOP':
        call.hangup()

    if tag == 'gather' and args.get('reason') == 'inter-digit-timeout':
        call.audio(
            sentence="This call will be canceled now.", tag="canceling",
            gender=gender, locale=locale, voice=voice
        )

    if args.get('state') == 'PLAYBACK_STOP' and (
        tag == 'canceling' or tag == 'lead_call_error'
    ):
        call.hangup()

    if args.get('callState') == 'completed':
        BusyAgents.remove(agent_id)
        print('Agent call completed')
        if len(request.args['other_agents']) > 0 and storage.state not in ['ONGOING', 'CAPTURED']:
            agent_ids = to_ints(request.args['other_agents'])
            BandwidthRouting(client).route_sequential(agent_ids, lead.widget, lead)
        else:
            try:
                call_duration = int(call_info["chargeableDuration"])
            except:
                call_duration = 0
            cause = args.get('cause')
            lead.transfer_call_id = call_id
            db.session.commit()
            agent_update_lead(lead, agent_id, call_duration, cause)
    if event_type == 'hangup':
        try:
            client.calls.hangup(storage.call_sid)
        except Exception as e:
            log.info('Unable to hangup call with call id: {} because id does not exist. '
                     'Error: {}'.format(storage.agent_call_id, e))
    return ''


@bw_outbound.route(
    '/api/bw/outbound/lead/<int:lead_id>/<int:agent_id>', methods=['POST']
)
@csrf.exempt
def lead_status_callback(lead_id, agent_id):
    """ Greet the lead.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])

    args = request.json or request.args
    print(args)

    lead = Lead.query.filter(Lead.id == lead_id).first()
    # import partnership information to get partnership id
    from buyercall.blueprints.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()

    client = bw_client(partner.id, 'voice')
    storage = CallStorage(redis_db, lead_id)
    language = storage.routing_config.get('language', 'en')
    gender, locale, voice = select_voice(language)

    event_type = args.get('eventType')
    call_id = args.get('callId')
    call = client.calls[call_id]

    if args.get('callState') == 'completed':
        print("Lead completed")
        if storage.state == State.AGENT_ONHOLD:
            storage.state = State.MISSED
            lead.status = 'missed'
            db.session.commit()

            # Stop dial tone
            # agent_call_id_decoded = storage.agent_call_id.decode()
            # client.calls[agent_call_id_decoded].audio(
            #     file_url=''
            # )
            # client.calls[agent_call_id_decoded].audio(
            #     sentence=MSG_LEAD_NOT_ANSWERING,
            #     tag='hangup',
            #     gender=gender, locale=locale, voice=voice
            # )
        elif storage.state == State.ONGOING:
            storage.state = State.CAPTURED
            lead.status = 'completed'
            lead.call_sid = call_id
            db.session.commit()

            from buyercall.blueprints.phonenumbers.bw_tasks import bw_upload_recording
            bw_upload_recording.delay(call_id)

        # Hangup agent call
        try:
            client.calls.hangup(storage.agent_call_id.decode())
        except Exception as e:
            log.info('Unable to hangup call with call id: {}. '
                     'Error: {}'.format(storage.call_sid, e))

        if redis_db.get('SUCC{}'.format(lead_id)) != '1':
            # TODO: Call next agent
            pass

        return ''

    if event_type == 'answer':
        lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()
        # lang = lead.widget.options.get('language', 'en')
        lead_text = lead.widget.options.get('whisperMessage', '')
        call.audio(
            sentence=lead_text,
            gender=gender, locale=locale, voice=voice, tag='lead_whisper'
        )
        lead.status = 'in-progress'
        db.session.commit()

    if args.get('state') == 'PLAYBACK_STOP':
        try:
            client.calls[storage.agent_call_id.decode()].audio(
                file_url=current_app.config['BEEP_SOUND']
            )
        except Exception as e:
            log.error('Unable to play beep sound to '
                      'agent for call: {} Error: {}'.format(storage.agent_call_id.decode(), e))
        lead = Lead.query.join(Lead.widget).filter(Lead.id == lead_id).first()

        if lead.widget.options.get('recordCalls'):
            try:
                call.set(recording_enabled=True)
            except Exception as e:
                log.error('Unable to set recording on call: {} Error: {}'.format(call_id, e))
        # recordings = call.recordings()
        # if recordings:
        #     lead.recording_url = recordings[0].media
        #     db.session.commit()

        agent_call_id_decoded = storage.agent_call_id.decode()
        try:
            client.bridge(call_ids=[call_id, agent_call_id_decoded])
        except Exception as e:
            client.calls.hangup(call_id)
            log.info('The call bridge between calls: {} and {} could not be'
                     'completed. Error: {}'.format(call_id, agent_call_id_decoded, e))
        storage.state = State.ONGOING

    return ''


def agent_update_lead(lead, agent_id, call_duration, cause):
    """ Update the lead information at the end of a call.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])

    storage = CallStorage(redis_db, lead.id)
    success = (storage.state == State.CAPTURED)

    log.info('success is {}'.format(success))
    if success:
        redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '-1')
    else:
        redis_db.setex('CONNECT{}'.format(lead.id), 2 * DAYS, '1')

    if success:
        lead.status = 'completed'
    elif lead.status == 'ringing':
        lead.status = 'missed'
    if storage.agent_outbound_call:
        lead.agent_id = agent_id
    else:
        lead.agent_id = agent_id if success else None
    lead.call_count += 1
    lead.missed_call_cause = cause
    lead.endtime = datetime.utcnow()
    lead.duration = call_duration

    if call_duration:
        subscription = lead.partnership_account.subscription
        subscription.update_usage(lead.partnership_account_id, seconds=call_duration)
    db.session.commit()

    try:
        if not storage.agent_outbound_call:
            send_notify_email(lead, lead.widget)
    except Exception as e:
        log.error(traceback.format_exc())


def agent_manual_outbound_call(lead, call_settings):
    """
    agent_manual_outbound_call: Entry point for agent manual outbound calls. The api/call endpoint gets hit
    when an outbound call is made through the application by an agent.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])

    # Initiate the redis storage used to store call details for handling call
    storage = CallStorage(redis_db, lead.id)
    storage.agent_outbound_call = True
    # confirm the account's subscription. If not on account level then on partner level.
    subscription = lead.partnership_account.subscription
    if not subscription:
        subscription = lead.partnership_account.partnership.subscription

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

    # Send the first webhook saying that a incoming call started
    webhooker.trigger_generic_webhook('operational_start_call', lead.id)

    agent_id = call_settings['agents'][0]['id']
    agent_call_method = call_settings['agents'][0]['contactUsing']

    if not agent_id:
        lead.status = 'missed'
        db.session.commit()
        raise NoAgentsException(lead)

    route_to_agent(agent_id, agent_call_method, lead)

    return lead


def route_to_agent(agent_id, agent_call_method, lead):
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])

    agent = Agent.query.filter(and_(
        Agent.partnership_account_id == lead.partnership_account_id,
        Agent.id == agent_id
    )).first()

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

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

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

    log.debug('Trying call from {} to agent {}...'.format(
        lead.inbound_id, agent_id
    ))
    call_agent_outbound(
        lead, agent, agent_call_method)
    db.session.commit()


def call_agent_outbound(lead, agent, agent_call_method):
    """
    For outbound calls when agent gets called first
    """
    from_number = lead.inbound.phonenumber
    call_agent_no(
        agent,
        agent_call_method,
        answer_callback_url=url_for(
            'bw_outbound.manual_outbound_agent_answer_webhook',
            lead_id=lead.id,
            agent_id=agent.id,
            from_tn=from_number,
            _external=True,
            _scheme='https'
        ),
        answer_fallback_callback_url=url_for(
            'bw_outbound.manual_outbound_agent_answer_fallback_webhook',
            lead_id=lead.id,
            agent_id=agent.id,
            from_tn=from_number,
            _external=True,
            _scheme='https'
        ),
        disconnect_callback_url=url_for(
            'bw_outbound.manual_outbound_agent_disconnect_webhook',
            lead_id=lead.id,
            agent_id=agent.id,
            from_tn=from_number,
            _external=True,
            _scheme='https'
        ),
        timeout=AGENT_TIMEOUT,
        from_=from_number,
        lead_id=lead.id,
    )
    return ''


def call_agent_no(agent, agent_call_method, answer_callback_url=None, disconnect_callback_url=None,
                  answer_fallback_callback_url=None, from_=None, lead_id=None, **kwargs):
    """ Call either the agent's phone number, mobile
    """
    from buyercall.blueprints.partnership.models import PartnershipAccount
    partnership_account = PartnershipAccount.query\
        .filter(PartnershipAccount.id == agent.partnership_account_id).first()

    # 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'
    )

    client = bw_client(partnership_account.partnership_id, 'voice')
    to = None
    try:
        if agent_call_method == 'phone':
            to = agent.phonenumber
        elif agent_call_method == 'mobile':
            to = agent.mobile

        # 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)

        agent_call = 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-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))
    except Exception as e:
        log.info('Unable to make call to agent: {} with error: {}'.format(agent.id, e))
    return ''


@bw_outbound.route('/api/bw/manual-outbound-agent-answer/webhook/<int:lead_id>/<int:agent_id>/<string:from_tn>', methods=['POST'])
@csrf.exempt
def manual_outbound_agent_answer_webhook(lead_id, agent_id, from_tn, *args):
    """ The answer callback triggered when an agent answers the outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])
    # Activate and set redis storage and add lead
    storage = CallStorage(redis_db, lead_id)

    lead = Lead.query.filter(Lead.id == lead_id).first()
    from buyercall.blueprints.partnership.models import PartnershipAccount
    partnership_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()

    # Fetch the request data. This includes message data for incoming sms/mms
    args = request.json or request.args
    agent_call_id = args.get('callId')
    req_start_time = parser.parse(args.get('startTime', ''))
    if req_start_time:
        start_time = req_start_time
    else:
        start_time = parser.parse(args.get('enqueuedTime', ''))
    connect_time = parser.parse(args.get('answerTime', ''))
    storage.bridge_call_sid_a = agent_call_id
    storage.manual_call = True
    log.info('The manual outbound agent answer callback args: {}'.format(args))
    # The save the call id to the db
    lead.call_sid = agent_call_id
    lead.call_count = 1
    # Calculate response time by the agent to answer their phone
    response_time_seconds = (connect_time - start_time).total_seconds()
    lead.response_time_seconds = response_time_seconds
    db.session.commit()

    # Fire off webhook for start of call
    from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
    delay_webhook_trigger.apply_async(args=['operational_agent_call', lead.id], countdown=15)

    # Declare the bandwidth response xml tag. This will be used for the xml responses
    bxml = Response()
    bxml.say('Please hold on while we try to connect you to the lead.')
    bxml.ring('55')

    client = bw_client(partnership_account.partnership_id, 'voice')
    # The callbacks for the lead manual outbound calls
    answer_callback_url = url_for(
        'bw_outbound.manual_outbound_lead_answer_webhook',
        lead_id=lead.id,
        agent_call_id=agent_call_id,
        _external=True,
        _scheme='https'
    )
    answer_fallback_callback_url = url_for(
        'bw_outbound.manual_outbound_lead_answer_fallback_webhook',
        lead_id=lead.id,
        agent_call_id=agent_call_id,
        _external=True,
        _scheme='https'
    )
    disconnect_callback_url = url_for(
        'bw_outbound.manual_outbound_lead_disconnect_webhook',
        lead_id=lead.id,
        agent_call_id=agent_call_id,
        _external=True,
        _scheme='https'
    )
    # Set the answering machine detecting callback
    amd_callback_url = url_for(
        'bw_outbound.manual_amd_webhook',
        party='lead',
        lead_id=lead_id,
        _external=True,
        _scheme='https'
    )
    amd_fallback_callback_url = url_for(
        'bw_outbound.manual_amd_fallback_webhook',
        party='lead',
        lead_id=lead_id,
        _external=True,
        _scheme='https'
    )

    # call the lead once the agent is connected.
    try:
        lead_call = client.call.create(
            c_from=from_tn,
            c_to=format_phone_number(lead.phonenumber),
            answer_callback_url=answer_callback_url,
            disconnect_callback_url=disconnect_callback_url,
            answer_fallback_callback_url=answer_fallback_callback_url,
            tag='start-lead-outbound',
            call_timeout='60',
            machine_detection={"callbackUrl": amd_callback_url,
                               "fallbackUrl": amd_fallback_callback_url,
                               "speechThreshold": 5,
                               "speechEndThreshold": 2,
                               "silenceTimeout": 10,
                               "detectionTimeout": 15,
                               }
        )
        json_call_string = json.loads(lead_call)
        lead_call_id = json_call_string.get('callId', '')
        storage.bridge_call_sid_b = lead_call_id
    except Exception as e:
        log.info('Unable to make call to lead: {} with error: {}'.format(lead_id, e))

    return create_xml_response(bxml)


@bw_outbound.route('/api/bw/manual-outbound-agent-answer-fallback/webhook/<int:lead_id>/<int:agent_id>/<string:from_tn>', methods=['POST'])
@csrf.exempt
def manual_outbound_agent_answer_fallback_webhook(lead_id, agent_id, from_tn):
    """ The answer callback triggered when an agent answers the outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
    """
    # Get the params of the Request
    args = request.json or request.args
    log.info('The manual outbound agent answer fallback callback response: {}'.format(args))
    return manual_outbound_agent_answer_webhook(lead_id, agent_id, from_tn, args)


@bw_outbound.route('/api/bw/manual-outbound-agent-disconnect/webhook/<int:lead_id>/<int:agent_id>', methods=['POST'])
@csrf.exempt
def manual_outbound_agent_disconnect_webhook(lead_id, agent_id):
    """ The disconnect callback triggered when an agent disconnect the outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/disconnect.html
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])
    storage = CallStorage(redis_db, lead_id)
    lead = Lead.query.filter(Lead.id == lead_id).first()
    # Fetch the request data. This includes message data for incoming sms/mms
    args = request.json or request.args
    call_id = args.get('callId', '')
    cause = args.get('cause', '')
    error_message = args.get('errorMessage', '')
    log.info('The manual outbound agent disconnect answer callback args: {}'.format(args))
    if cause in ['timeout', 'cancel', 'rejected', 'busy']:
        lead.call_sid = call_id
        lead.status = 'missed'
        lead.call_count = 1
        lead.missed_call_cause = cause
        lead.cause_description = error_message
        db.session.commit()
        # Fire off webhook for start of call
        from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
        delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
    elif cause == 'hangup' and storage.state not in ['ONGOING', 'MACHINE']:
        if storage.bridge_call_sid_b:
            from buyercall.blueprints.partnership.models import PartnershipAccount
            partnership_account = PartnershipAccount.query \
                .filter(PartnershipAccount.id == lead.partnership_account_id).first()
            client = bw_client(partnership_account.partnership_id, 'voice')
            client.call.update(storage.bridge_call_sid_b, 'completed')
        lead.cause_description = 'Canceled or hangup by agent before lead could answer.'
        db.session.commit()
    return ''


@bw_outbound.route('/api/bw/widget-outbound-agent-answer/webhook/<int:lead_id>/<int:agent_id>/<string:from_tn>', methods=['POST'])
@csrf.exempt
def widget_outbound_agent_answer_webhook(lead_id, agent_id, from_tn, *args):
    """ The answer callback triggered when an agent answers the widget outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])
    # Activate and set redis storage and add lead
    storage = CallStorage(redis_db, lead_id)

    lead = Lead.query.filter(Lead.id == lead_id).first()
    from buyercall.blueprints.partnership.models import PartnershipAccount
    partnership_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()

    # Fetch the request data. This includes message data for incoming sms/mms
    args = request.json or request.args
    agent_call_id = args.get('callId', '')
    req_start_time = parser.parse(args.get('startTime', ''))
    if req_start_time:
        start_time = req_start_time
    else:
        start_time = parser.parse(args.get('enqueuedTime', ''))
    connect_time = parser.parse(args.get('answerTime', ''))
    storage.bridge_call_sid_a = agent_call_id
    log.info('The widget outbound agent answer callback args: {}'.format(args))
    # The save the call id to the db
    lead.call_sid = agent_call_id
    lead.call_count = 1
    storage.state = State.AGENT_ONHOLD
    storage.routing_config = lead.widget.options
    storage.agent_call_id = agent_call_id
    storage.manual_call = False
    lead.agent_id = agent_id
    BusyAgents.add(agent_id)
    # Calculate response time by the agent to answer their phone
    response_time_seconds = (connect_time - start_time).total_seconds()
    lead.response_time_seconds = response_time_seconds
    db.session.commit()

    log.debug('Agent {} picked up'.format(agent_id))
    agent_text = get_agent_text(lead.widget, lead)

    # Declare the bandwidth response xml tag. This will be used for the xml responses
    bxml = Response()

    widget_digit_prompt = lead.widget.options.get('digitPrompt', False)
    # If no whisper message, just continue
    if not agent_text:
        bxml.say('Please hold on while we try to connect you to the lead.')
    else:
        if lead.status != 'missed':
            bxml.say(agent_text)
    if widget_digit_prompt:
        # If digit prompt is disabled for the phone number, just continue
        log.info('the widget digit prompt is: {}'.format(widget_digit_prompt))
        # This is the gathering digit call web-hook. Every time there's a new event with the gather verb
        # this endpoint will be hit with some call-back information
        digits_webhook_url = url_for(
            'bw_outbound.agent_acceptance_phone_call_digits_bw_webhook', lead_id=lead_id, agent_id=agent_id,
            _external=True, _scheme='https')
        digits_fallback_webhook_url = url_for(
            'bw_outbound.agent_acceptance_phone_call_fallback_digits_bw_webhook', lead_id=lead_id,
            agent_id=agent_id, _external=True, _scheme='https')
        digit_gather = bxml.gather(digits_webhook_url, digits_fallback_webhook_url, tag='accept-gather-digits',
                                   first_digit_timeout='5', max_digits='1', repeat_count='2')
        digit_gather.say(MSG_AGENT_KEY_PRESS_PROMPT)
    else:
        bxml.ring('55')
        bxml.tag('agent-answered')

        called_agents_key = 'LIST{}'.format(lead_id)

        calls = redis_db.lrange(called_agents_key, 0, -1)
        log.info('list of calls: {}'.format(calls))

        with storage.lock():
            if not storage.agent_id:
                storage.agent_id = agent_id
                # Fire off webhook for start of call
                from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
                delay_webhook_trigger.apply_async(args=['operational_agent_call', lead.id], countdown=15)

        if int(storage.agent_id) == agent_id:
            # Cancel all other current calls
            calls = redis_db.lrange(called_agents_key, 0, -1)
            for call in calls:
                call_decoded = call.decode()
                a, sid = call_decoded.split('_')
                if int(a) != agent_id:
                    log.info('Cancel call for agent {}....'.format(a))
                    client = bw_client(partnership_account.partnership_id, 'voice')
                    client.call.update(sid, 'completed')
        else:
            # Someone else picked up; exit
            log.debug('Agent {}: agent {} picked up; exit'.format(
                agent_id, agent_id
            ))
            cancel_redirect_url = url_for(
                'bw_outbound.agent_cancel_call_bw_webhook',
                _external=True,
                _scheme='https'
            )
            cancel_redirect_fallback_url = url_for(
                'bw_outbound.agent_cancel_call_fallback_bw_webhook',
                _external=True,
                _scheme='https'
            )
            client = bw_client(partnership_account.partnership_id, 'voice')
            client.call.update(agent_call_id, 'active', cancel_redirect_url, cancel_redirect_fallback_url,
                               'another-agent-picked-ip')
        from buyercall.blueprints.partnership.models import PartnershipAccount
        partnership_account = PartnershipAccount.query \
            .filter(PartnershipAccount.id == lead.partnership_account_id).first()
        client = bw_client(partnership_account.partnership_id, 'voice')
        # The callbacks for the lead manual outbound calls
        answer_callback_url = url_for(
            'bw_outbound.manual_outbound_lead_answer_fallback_webhook',
            lead_id=lead.id,
            agent_call_id=agent_call_id,
            _external=True,
            _scheme='https'
        )
        answer_fallback_callback_url = url_for(
            'bw_outbound.manual_outbound_lead_answer_fallback_webhook',
            lead_id=lead.id,
            agent_call_id=agent_call_id,
            _external=True,
            _scheme='https'
        )
        disconnect_callback_url = url_for(
            'bw_outbound.manual_outbound_lead_disconnect_webhook',
            lead_id=lead.id,
            agent_call_id=agent_call_id,
            _external=True,
            _scheme='https'
        )
        # Set the answering machine detecting callback
        amd_callback_url = url_for(
            'bw_outbound.manual_amd_webhook',
            party='lead',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        )
        amd_fallback_callback_url = url_for(
            'bw_outbound.manual_amd_fallback_webhook',
            party='lead',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        )

        # call the lead once the agent is connected.
        try:
            lead_call = client.call.create(
                c_from=from_tn,
                c_to=format_phone_number(lead.phonenumber),
                answer_callback_url=answer_callback_url,
                disconnect_callback_url=disconnect_callback_url,
                answer_fallback_callback_url=answer_fallback_callback_url,
                tag='start-lead-outbound',
                call_timeout='60',
                machine_detection={"callbackUrl": amd_callback_url,
                                   "fallbackUrl": amd_fallback_callback_url,
                                   "speechThreshold": 6,
                                   "speechEndThreshold": 2,
                                   "silenceTimeout": 10,
                                   "detectionTimeout": 15,
                                   }
            )
            json_call_string = json.loads(lead_call)
            lead_call_id = json_call_string.get('callId', '')
            storage.bridge_call_sid_b = lead_call_id
        except Exception as e:
            log.info('Unable to make call to lead: {} with error: {}'.format(lead_id, e))
    return create_xml_response(bxml)


@bw_outbound.route('/api/bw/widget-outbound-agent-answer-fallback/webhook/<int:lead_id>/<int:agent_id>/<string:from_tn>', methods=['POST'])
@csrf.exempt
def widget_outbound_agent_answer_fallback_webhook(lead_id, agent_id, from_tn):
    """ The answer callback triggered when an agent answers the widget outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
    """
    # Get the params of the Request
    args = request.json or request.args
    log.info('The widget outbound agent answer fallback callback response: {}'.format(args))
    return widget_outbound_agent_answer_webhook(lead_id, agent_id, from_tn, args)


@bw_outbound.route('/api/bw/widget-parallel-outbound-agent-disconnect/webhook/<int:lead_id>/<int:agent_id>', methods=['POST'])
@csrf.exempt
def widget_parallel_outbound_agent_disconnect_webhook(lead_id, agent_id):
    """ The disconnect callback triggered when an agent disconnect the widget parallel outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/disconnect.html
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])
    storage = CallStorage(redis_db, lead_id)
    lead = Lead.query.filter(Lead.id == lead_id).first()
    # Fetch the request data. This includes message data for incoming sms/mms
    args = request.json or request.args
    call_id = args.get('callId', '')
    cause = args.get('cause', '')
    error_message = args.get('errorMessage', '')
    log.info('The widget outbound agent disconnect answer callback args: {}'.format(args))
    BusyAgents.remove(agent_id)
    called_agents_key = 'LIST{}'.format(lead_id)
    with storage.lock():
        calls = redis_db.lrange(called_agents_key, 0, -1)
        log.info('The calls are: {}'.format(calls))
        for call in calls:
            call_decoded = call.decode()
            a, sid = call_decoded.split('_')
            if int(a) == agent_id:
                log.info(
                    'Removing call for agent {} from list...'.format(a))
                redis_db.lrem(called_agents_key, 1, call)
                break
        ongoing_calls = redis_db.llen(called_agents_key)
        log.info('Still {} calls ongoing...'.format(ongoing_calls))

    if cause in ['timeout', 'cancel', 'rejected', 'busy'] and storage.state != 'AGENT_ONHOLD':
        lead.call_sid = call_id
        lead.status = 'missed'
        lead.call_count = 1
        lead.missed_call_cause = cause
        lead.cause_description = error_message
        db.session.commit()
        log.info('Still {} calls ongoing show me...'.format(ongoing_calls))

        if ongoing_calls == 0:
            schedule_retries(lead)
            lead.call_count += 1

        if not storage.agent_disconnect_webhook_sent:
            # Fire off webhook for start of call
            from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
            delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
            storage.agent_disconnect_webhook_sent = True
            if lead.status != 'retry-pending' and not schedule_retries(lead):
                send_notify_email(lead, lead.widget)
    elif cause == 'hangup' and storage.state not in ['ONGOING', 'MACHINE', 'AGENT_ONHOLD']:
        log.info('THE STORAGE STATE IS: {}'.format(storage.state))
        if storage.bridge_call_sid_b:
            from buyercall.blueprints.partnership.models import PartnershipAccount
            partnership_account = PartnershipAccount.query \
                .filter(PartnershipAccount.id == lead.partnership_account_id).first()
            client = bw_client(partnership_account.partnership_id, 'voice')
            client.call.update(storage.bridge_call_sid_b, 'completed')
        lead.cause_description = 'Canceled or hangup by agent before lead could answer.'
        if storage.state == 'DIGIT_MISSED':
            lead.missed_call_cause = cause
            lead.status = 'missed'
            lead.cause_description = 'Agent pressed # or did not answer the call.'
            if ongoing_calls == 0:
                # Fire off webhook for start of call
                from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
                delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
                storage.agent_disconnect_webhook_sent = True
                schedule_retries(lead)
                lead.call_count += 1
                log.info('The lead status is: {}'.format(lead.status))
                if lead.status != 'retry-pending' and not schedule_retries(lead):
                    send_notify_email(lead, lead.widget)
        db.session.commit()
    return ''


@bw_outbound.route('/api/bw/widget-sequence-outbound-agent-disconnect/webhook/<int:lead_id>/<int:agent_id>', methods=['POST'])
@csrf.exempt
def widget_sequence_outbound_agent_disconnect_webhook(lead_id, agent_id):
    """ The disconnect callback triggered when an agent disconnect the widget sequence outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/disconnect.html
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])
    storage = CallStorage(redis_db, lead_id)
    lead = Lead.query.filter(Lead.id == lead_id).first()
    # Fetch the request data. This includes message data for incoming sms/mms
    args = request.json
    additional_args = request.args
    call_id = args.get('callId', '')
    cause = args.get('cause', '')
    error_message = args.get('errorMessage', '')
    log.info('The widget outbound agent disconnect answer callback args: {}'.format(args))
    BusyAgents.remove(agent_id)

    from buyercall.blueprints.partnership.models import PartnershipAccount
    partnership_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    client = bw_client(partnership_account.partnership_id, 'voice')

    if cause in ['timeout', 'cancel', 'rejected', 'busy'] and storage.state != 'AGENT_ONHOLD':
        lead.call_sid = call_id
        lead.status = 'missed'
        lead.call_count = 1
        lead.missed_call_cause = cause
        lead.cause_description = error_message
        db.session.commit()
        log.info('The storage state: {}'.format(storage.state))
        log.info('agents list are: {}'.format(len(additional_args['other_agents'])))
        if len(additional_args['other_agents']) > 0 and storage.state not in ['ONGOING', 'CAPTURED']:
            log.info('This got hit.')
            agent_ids = to_ints(request.args['other_agents'])
            return BandwidthRouting(client).route_sequential(agent_ids, lead.widget, lead)
        else:
            if schedule_retries(lead):
                lead.call_count += 1

                # Fire off webhook for start of call
                from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
                delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
                storage.agent_disconnect_webhook_sent = True
                if lead.status != 'retry-pending' and not schedule_retries(lead):
                    send_notify_email(lead, lead.widget)
    elif cause == 'hangup' and storage.state not in ['ONGOING', 'MACHINE', 'AGENT_ONHOLD']:
        log.info('THE STORAGE STATE IS: {}'.format(storage.state))
        if storage.bridge_call_sid_b:
            client.call.update(storage.bridge_call_sid_b, 'completed')
        if len(request.args['other_agents']) > 0 and storage.state not in ['ONGOING', 'CAPTURED']:
            agent_ids = to_ints(request.args['other_agents'])
            BandwidthRouting(client).route_sequential(agent_ids, lead.widget, lead)
        else:
            lead.cause_description = 'Canceled or hangup by agent before lead could answer.'
            if storage.state == 'DIGIT_MISSED':
                lead.missed_call_cause = cause
                lead.status = 'missed'
                lead.cause_description = 'Agent pressed # or did not answer the call.'
                # Fire off webhook for start of call
                from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
                delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
                storage.agent_disconnect_webhook_sent = True
                schedule_retries(lead)
                lead.call_count += 1
                log.info('The lead status is: {}'.format(lead.status))
                if lead.status != 'retry-pending' and not schedule_retries(lead):
                    send_notify_email(lead, lead.widget)
        db.session.commit()
    return ''


@bw_outbound.route('/api/bw/manual-outbound-lead-answer/webhook/<int:lead_id>/<string:agent_call_id>', methods=['POST'])
@csrf.exempt
def manual_outbound_lead_answer_webhook(lead_id, agent_call_id, *args):
    """ The answer callback triggered when a lead answers the outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])
    lead = Lead.query.filter(Lead.id == lead_id).first()
    # Activate and set redis storage and add lead
    storage = CallStorage(redis_db, lead_id)

    # Fetch the request data. This includes message data for incoming sms/mms
    args = request.json or request.args
    lead_call_id = args.get('callId')
    req_start_time = parser.parse(args.get('startTime', ''))
    if req_start_time:
        start_time = req_start_time
    else:
        start_time = parser.parse(args.get('enqueuedTime', ''))
    lead.starttime = start_time
    lead.status = 'in-progress'
    storage.state = State.ONGOING
    lead.transfer_call_id = lead_call_id
    storage.bridge_call_sid_b = lead_call_id
    db.session.commit()
    log.info('The manual outbound lead answer callback args: {}'.format(args))
    # Declare the bandwidth response xml tag. This will be used for the xml responses
    bxml = Response()
    # If it's a widget call we might want to play a whisper message
    if storage.manual_call:
        log.info('This is an manual outbound call')
    else:
        log.info('This is an widget call')
        lead_text = lead.widget.options.get('whisperMessage', '')
        record_call = lead.widget.options.get('recordCalls', '')
        transcribe_call = storage.routing_config.get('transcribeAnsweredCall', '')
        if lead_text:
            bxml.say(lead_text)
        if record_call:
            if record_call:
                record_call_back_url = url_for(
                    'bw_operational.operational_call_record_bw_webhook', lead_id=lead_id,
                    _external=True, _scheme='https')
                if transcribe_call:
                    transcribe = "true"
                    transcribe_call_back_url = url_for(
                        'bw_operational.operational_call_transcribe_bw_webhook', lead_id=lead_id,
                        _external=True, _scheme='https')
                else:
                    transcribe = "false"
                    transcribe_call_back_url = ""

                bxml.start_record(recording_available_url=record_call_back_url, transcribe=transcribe,
                                  transcribe_available_url=transcribe_call_back_url, tag='start-recording')

    # Bridge the agent and lead calls to talk to each other
    bxml.bridge(agent_call_id)

    return create_xml_response(bxml)


@bw_outbound.route('/api/bw/manual-outbound-lead-answer-fallback/webhook/<int:lead_id>/<string:agent_call_id>', methods=['POST'])
@csrf.exempt
def manual_outbound_lead_answer_fallback_webhook(lead_id, agent_call_id):
    """ The answer callback triggered when a lead answers the outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/answer.html
    """
    # Get the params of the Request
    args = request.json or request.args
    log.info('The manual outbound lead answer fallback callback response: {}'.format(args))

    return manual_outbound_lead_answer_webhook(lead_id, agent_call_id, args)


@bw_outbound.route('/api/bw/manual-outbound-lead-disconnect/webhook/<int:lead_id>/<string:agent_call_id>', methods=['POST'])
@csrf.exempt
def manual_outbound_lead_disconnect_webhook(lead_id, agent_call_id):
    """ The disconnect callback triggered when a lead disconnect the outbound the call
    https://dev.bandwidth.com/voice/bxml/callbacks/disconnect.html
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])
    # Activate and set redis storage and add lead
    storage = CallStorage(redis_db, lead_id)
    lead = Lead.query.filter(Lead.id == lead_id).first()
    # Fetch the request data. This includes message data for incoming sms/mms
    args = request.json or request.args
    call_id = args.get('callId', '')
    cause = args.get('cause', '')
    error_message = args.get('errorMessage', '')
    req_start_time = parser.parse(args.get('startTime', ''))
    if req_start_time:
        start_time = req_start_time
    else:
        start_time = parser.parse(args.get('enqueuedTime', ''))
    end_time = parser.parse(args.get('endTime', ''))
    log.info('The manual outbound lead disconnect answer callback args: {}'.format(args))
    if cause in ['timeout', 'cancel', 'rejected', 'busy']:
        lead.transfer_call_id = call_id
        lead.status = 'missed'
        lead.call_count = 1
        lead.missed_call_cause = cause
        if error_message:
            lead.cause_description = error_message
        else:
            lead.cause_description = 'The call was not successful.'
    elif cause in ['hangup'] and storage.state not in ['MACHINE']:
        lead.transfer_call_id = call_id
        lead.status = 'completed'
        lead.missed_call_cause = cause
        duration = (end_time - start_time).total_seconds()
        lead.duration = duration
        lead.cause_description = 'Call successfully connected and ended.'
        lead.starttime = start_time
        lead.endtime = end_time
    elif cause in ['callback-error', 'invalid-bxml', 'application-error', 'account-limit',
                   'node-capacity-exceeded', 'error', 'unknown'] and storage.state not in ['MACHINE']:
        lead.transfer_call_id = call_id
        lead.status = 'error'
        lead.call_count = 1
        lead.missed_call_cause = cause
        lead.cause_description = error_message
        lead.endtime = end_time
    else:
        lead.transfer_call_id = call_id
        lead.call_count = 1
    db.session.commit()

    if lead.widget:
        record_call = lead.widget.options.get('recordCalls', '')
        if not record_call:
            # Fire off webhook for start of call
            from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
            delay_webhook_trigger.apply_async(args=['operational_end_call', lead.id], countdown=15)
        send_notify_email(lead, lead.widget)
    return ''


@bw_outbound.route('/api/bw/manual-amd/webhook/<string:party>/<int:lead_id>', methods=['POST'])
@csrf.exempt
def manual_amd_webhook(party, lead_id, *args):
    """ The machine detection callback
    https://dev.bandwidth.com/voice/bxml/callbacks/machineDetectionComplete.html
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'],
                                 port=current_app.config['REDIS_CONFIG_PORT'])
    # Activate and set redis storage and add lead
    storage = CallStorage(redis_db, lead_id)

    lead = Lead.query.filter(Lead.id == lead_id).first()
    # Fetch the request data.
    args = request.json or request.args
    call_id = args.get('callId', '')
    result = args['machineDetectionResult'].get('value', '')
    log.info('The machine detection args are: {}'.format(args))
    if result in ['answering-machine']:
        if party == 'agent':
            from buyercall.blueprints.partnership.models import PartnershipAccount
            partnership_account = PartnershipAccount.query \
                .filter(PartnershipAccount.id == lead.partnership_account_id).first()
            client = bw_client(partnership_account.partnership_id, 'voice')
            if storage.bridge_call_sid_b:
                client.call.update(storage.bridge_call_sid_b, 'completed')
            agent_call_info = client.call.info(call_id)
            agent_call_state = agent_call_info.get('state', '')
            if agent_call_state and agent_call_state != 'disconnected':
                client.call.update(call_id, 'completed')
            storage.state = State.MACHINE
            called_agents_key = 'LIST{}'.format(lead_id)
            ongoing_calls = redis_db.llen(called_agents_key)
            if ongoing_calls == 0:
                lead.status = 'missed'
                lead.missed_call_cause = 'answering-machine'
                lead.cause_description = 'Agent answering machine was reached.'
                db.session.commit()
        elif party == 'lead':
            storage.state = State.MACHINE
            lead.status = 'missed'
            lead.missed_call_cause = 'answering-machine'
            lead.cause_description = 'Lead answering machine was reached.'
            db.session.commit()
        else:
            pass

    return ''


@bw_outbound.route('/api/bw/manual-amd-fallback/webhook/<string:party>/<int:lead_id>', methods=['POST'])
@csrf.exempt
def manual_amd_fallback_webhook(party, lead_id):
    """ The machine detection fallback callback
    https://dev.bandwidth.com/voice/bxml/callbacks/machineDetectionComplete.html
    """
    # Fetch the request data.
    args = request.json or request.args
    log.info('The machine detection fallback args are: {}'.format(args))
    return manual_amd_webhook(party, lead_id, args)


@bw_outbound.route('/api/bw/agent-acceptance-phone-call-digits/webhook/<int:lead_id>/<int:agent_id>',
                      methods=['GET', 'POST'])
@csrf.exempt
def agent_acceptance_phone_call_digits_bw_webhook(lead_id, agent_id, *args):
    # Declare redis config url
    redis_db = redis.StrictRedis(host=current_app.config['REDIS_CONFIG_URL'], port=current_app.config['REDIS_CONFIG_PORT'])
    # Declare the redis db model
    storage = CallStorage(redis_db, lead_id)

    # Get the params of the Request
    args = request.json or request.args
    log.info('The operation call gather digits call-back/web-hook response: {}'.format(args))
    agent_call_id = args.get('callId', '')
    pressed_digit = args.get('digits', '')
    terminating_digit = args.get('terminatingDigit', '')
    lead = Lead.query.filter(Lead.id == lead_id).first()
    # Declare the bandwidth response xml tag. This will be used for the xml responses
    from_tn = lead.inbound.phonenumber
    bxml = Response()
    if pressed_digit not in ["#", ""]:
        bxml.ring('55')
        bxml.tag('agent-answered')
        called_agents_key = 'LIST{}'.format(lead_id)

        from buyercall.blueprints.partnership.models import PartnershipAccount
        partnership_account = PartnershipAccount.query \
            .filter(PartnershipAccount.id == lead.partnership_account_id).first()
        client = bw_client(partnership_account.partnership_id, 'voice')

        with storage.lock():
            if not storage.agent_id:
                storage.agent_id = agent_id
                # Fire off webhook for start of call
                from ..phonenumbers.bw_operational_tasks import delay_webhook_trigger
                delay_webhook_trigger.apply_async(args=['operational_agent_call', lead.id], countdown=15)

        if int(storage.agent_id) == agent_id:
            # Cancel all other current calls
            calls = redis_db.lrange(called_agents_key, 0, -1)
            for call in calls:
                call_decoded = call.decode()
                a, sid = call_decoded.split('_')
                if int(a) != agent_id:
                    log.info('Cancel call for agent {}....'.format(a))
                    client.call.update(sid, 'completed')
        else:
            # Someone else picked up; exit
            log.debug('Agent {}: agent {} picked up; exit'.format(
                agent_id, agent_id
            ))
            cancel_redirect_url = url_for(
                'bw_outbound.agent_cancel_call_bw_webhook',
                _external=True,
                _scheme='https'
            )
            cancel_redirect_fallback_url = url_for(
                'bw_outbound.agent_cancel_call_fallback_bw_webhook',
                _external=True,
                _scheme='https'
            )
            client.call.update(agent_call_id, 'active', cancel_redirect_url, cancel_redirect_fallback_url,
                               'another-agent-picked-ip')

        # The callbacks for the lead manual outbound calls
        answer_callback_url = url_for(
            'bw_outbound.manual_outbound_lead_answer_webhook',
            lead_id=lead.id,
            agent_call_id=agent_call_id,
            _external=True,
            _scheme='https'
        )
        answer_fallback_callback_url = url_for(
            'bw_outbound.manual_outbound_lead_answer_fallback_webhook',
            lead_id=lead.id,
            agent_call_id=agent_call_id,
            _external=True,
            _scheme='https'
        )
        disconnect_callback_url = url_for(
            'bw_outbound.manual_outbound_lead_disconnect_webhook',
            lead_id=lead.id,
            agent_call_id=agent_call_id,
            _external=True,
            _scheme='https'
        )
        # Set the answering machine detecting callback
        amd_callback_url = url_for(
            'bw_outbound.manual_amd_webhook',
            party='lead',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        )
        amd_fallback_callback_url = url_for(
            'bw_outbound.manual_amd_fallback_webhook',
            party='lead',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        )

        # call the lead once the agent is connected.
        try:
            lead_call = client.call.create(
                c_from=from_tn,
                c_to=format_phone_number(lead.phonenumber),
                answer_callback_url=answer_callback_url,
                disconnect_callback_url=disconnect_callback_url,
                answer_fallback_callback_url=answer_fallback_callback_url,
                tag='start-lead-outbound',
                call_timeout='60',
                machine_detection={"callbackUrl": amd_callback_url,
                                   "fallbackUrl": amd_fallback_callback_url,
                                   "speechThreshold": 6,
                                   "speechEndThreshold": 2,
                                   "silenceTimeout": 10,
                                   "detectionTimeout": 15,
                                   }
            )
            json_call_string = json.loads(lead_call)
            lead_call_id = json_call_string.get('callId', '')
            storage.bridge_call_sid_b = lead_call_id
        except Exception as e:
            log.info('Unable to make call to lead: {} with error: {}'.format(lead_id, e))
    else:
        storage.state = State.DIGIT_MISSED
        bxml.hangup()
    return create_xml_response(bxml)


@bw_outbound.route('/api/bw/agent-acceptance-phone-call-digits-fallback/webhook/<int:lead_id>/<int:agent_id>',
                      methods=['GET', 'POST'])
@csrf.exempt
def agent_acceptance_phone_call_fallback_digits_bw_webhook(lead_id, agent_id):
    """ This is the fallback callback url for the gather digits callback event
        https://dev.bandwidth.com/voice/bxml/callbacks/gather.html
    """
    # Get the params of the Request
    args = request.json or request.args
    log.info('The call gather digits fallback callback response: {}'.format(args))
    return agent_acceptance_phone_call_digits_bw_webhook(lead_id, agent_id, args)


@bw_outbound.route('/api/bw/agent-cancel-call/webhook', methods=['GET', 'POST'])
@csrf.exempt
def agent_cancel_call_bw_webhook(*args):
    """ This is the redirect callback url to cancel call when another agent picked up
        https://dev.bandwidth.com/voice/bxml/callbacks/redirect.html
    """
    # Get the params of the Request
    args = request.json or request.args
    log.info('The call cancel redirect callback response: {}'.format(args))
    # Declare the bandwidth response xml tag. This will be used for the xml responses
    bxml = Response()
    bxml.say('Another agent answered this call. You will be disconnected.')
    bxml.hangup()
    return create_xml_response(bxml)


@bw_outbound.route('/api/bw/agent-cancel-fallback-call/webhook', methods=['GET', 'POST'])
@csrf.exempt
def agent_cancel_call_fallback_bw_webhook():
    """ This is the redirect fallback callback url to cancel call when another agent picked up
        https://dev.bandwidth.com/voice/bxml/callbacks/redirect.html
    """
    # Get the params of the Request
    args = request.json or request.args
    log.info('The call cancel redirect fallback callback response: {}'.format(args))
    return agent_cancel_call_bw_webhook(args)