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_new/buyercall/buyercall/blueprints/phonenumbers/twilio_inbound.py
"""
Governs the logic of the incoming lead calls and lead/agent interations.

At any given moment the call is in one of the following states:

* 'NEW': New inbound call
* 'CALLBACK': Callback call, agent called first
* 'LEAD_ONHOLD': Lead selected a routing, placed on hold until agent answers
* 'AGENT_ONHOLD': In a scheduled callback, the agent is called first and placed
  on hold.
* 'CALLBACK_PROMPT': Lead has been waiting for a while and is asked if he wants
  to be called back.
* 'ONGOING': Agent answered/call ongoing.
* 'MISSED': Call ended/lead missed.
* 'CAPTURED': Call ended/lead captured.
"""
import traceback
import logging as log
import time
from datetime import datetime, timedelta

from flask import (
    Blueprint,
    current_app as app,
    make_response,
    request,
    url_for,
)
from twilio.twiml.voice_response import VoiceResponse
from twilio.base.exceptions import TwilioRestException
import redis
from sqlalchemy.orm import load_only, joinedload
import pytz

from buyercall.blueprints.block_numbers.models import Block
from buyercall.blueprints.phonenumbers.models import Audio, HoldMusic
from buyercall.extensions import csrf, db
from buyercall.lib.flask_mailplus import _try_renderer_template
from buyercall.lib.util_ses_email import send_ses_email
from buyercall.lib.util_twilio import (
    CallStorage, subaccount_client, InboundCallState as State,
    update_response_time, BusyAgents
)
from buyercall.lib.util_webhooks import WebhookUtil
from .models import Phone
from ..leads.models import Lead
from ..agents.models import Agent
from ..user.models import User
from ..partnership.models import Partnership, PartnershipAccount
from .routing import (
    get_routing_agents, schedule_callback,
    get_agent_number, get_agent_text
)

webhooker = WebhookUtil()

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

HOURS = 3600
""" The length of an hour in seconds """

DAYS = 86400
""" The length of a day in seconds """

TWIMLET_CLASSICAL = 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical'  # noqa
""" The URL for the Twimlet with some nice hold music """

CALLBACK_PROMPT_DELAY = 60
""" Seconds to wait until prompting the user for callback """

DEFAULT_VOICE = 'alice'
""" The voice to use when speaking a message for the lead """

AGENT_TIMEOUT = 18
""" The agent calls will be timed out/dropped in the seconds specified if no answer from agent """


@twilio_inbound.route('/twiml/inbound/<int:inbound_id>', methods=['POST'])
@csrf.exempt
def lead_inbound_call(inbound_id):
    """ Entry point for the incoming lead call.
    """
    # Declare redis url
    redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

    # Check first if the phone number that's calling is not blocked
    is_number_blocked = Block.blocked(inbound_id, request.form.get('From', ''))
    if is_number_blocked:
        log.info(
            'This call was not processed because phone number: {} was blocked'.format(request.form.get('From', '')))
        r = VoiceResponse()
        r.reject()
        return str(r)

    inbound = Phone.query.options(
        joinedload(Phone.partnership_account).joinedload(
            PartnershipAccount.subscription,
        )
    ).filter(Phone.id == inbound_id).first()
    if inbound is None:
        app.logger.error('Cannot find inbound route {} in the database!'.format(
            inbound_id
        ))
        r = VoiceResponse()
        r.reject()
        return str(r)

    # Check if the account has been disabled; if so, reject the call
    subscription = inbound.partnership_account.subscription
    if not subscription:
        subscription = inbound.partnership_account.partnership.subscription
    if subscription.cancelled_subscription_on or subscription.usage_over_limit:
        r = VoiceResponse()
        r.reject()
        return str(r)

    caller_name = request.form.get('CallerName', '').lower()
    phone_number = request.form.get('From', '')
    app.logger.info("The caller's phone number is {}".format(phone_number))

    first_name, last_name = Lead.get_last_known_name(
            inbound.partnership_account_id, phone_number
        )

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

    if previous_lead is not None:
        contact_id = previous_lead.contact_id
        caller_id = previous_lead.caller_id
        progress_status = previous_lead.progress_status
    else:
        contact_id = None
        caller_id = ''
        progress_status = 'no status'

    if caller_id == '':
        caller_id = caller_name

    lead = Lead(
        partnership_account_id=inbound.partnership_account_id,
        firstname=first_name,
        lastname=last_name,
        caller_id=caller_id,
        phonenumber=phone_number,
        email='',
        starttime=datetime.utcnow(),
        call_sid=request.form.get('CallSid', ''),
        call_type='inbound',
        my_phone=inbound.phonenumber,
        inbound_id=inbound_id,
        status='ringing',
        contact_id=contact_id,
        call_source=inbound.friendly_name,
        originating_number=phone_number,
        progress_status=progress_status
    )
    db.session.add(lead)
    db.session.commit()

    # Add contact based on new lead using celery
    from .tasks import add_tw_contact
    add_tw_contact(phone_number, first_name, last_name, caller_id, inbound.partnership_account_id)

    call_sid = request.form.get('CallSid', '')
    key = 'LEAD_{}'.format(call_sid)
    redis_db.setex(key, 2*HOURS, lead.id)

    storage = CallStorage(redis_db, lead.id)
    storage.init()

    storage.call_sid = call_sid
    storage.routing_config = inbound.routing_config
    storage.clear_retry_cnt()
    storage.clear_callback_cnt()
    storage.caller_name = caller_name
    storage.subaccount_sid = inbound.partnership_account.subscription.twilio_subaccount_sid
    storage.state = State.NEW

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

    r = VoiceResponse()
    r.redirect(url=url_for(
        'twilio_inbound.lead_inbound_call_continue',
        lead_id=lead.id,
        _external=True,
        _scheme='https'
    ))
    return to_response(r)


@twilio_inbound.route(
    '/twiml/inbound/callback/<int:agent_id>/<int:lead_id>', methods=['POST']
)
@csrf.exempt
def lead_callback(agent_id, lead_id):
    """ Entry point for the lead's callback call. Place lead into the
        confererence call where the agent is already waiting.
    """
    # 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)
    app.logger.debug('Lead {}: Callback call started.'.format(lead_id))

    lead = Lead.query.filter(Lead.id == lead_id).first()
    if not lead:
        return

    lead.agent_id = agent_id
    lead.status = 'in-progress'
    db.session.commit()

    call_sid = request.form.get('CallSid', '')
    key = 'LEAD_{}'.format(call_sid)
    redis_db.setex(key, 2*HOURS, lead.id)

    storage.call_sid = call_sid
    storage.clear_retry_cnt()
    storage.state = State.ONGOING

    routing_config = storage.routing_config

    whisper_message_status = routing_config.get('callBackMessage')
    whisper_message_type = routing_config.get('callBackTextType')
    whisper_message = routing_config.get('callBackText')
    language = routing_config.get('language', 'en')

    r = VoiceResponse()

    # Find the the first lead row that includes inbound id
    phonenumber = Lead.query.filter(lead_id == Lead.id).first()

    # Query the audio url table and find the row that matches the inbound id and whisper message type
    audio_file = Audio.query.filter(Audio.inbound_id == phonenumber.inbound_id,
                                       Audio.whisper_message_type == 'CallBackText')\
        .order_by(Audio.id.desc()).first()

    # Check to see if there's an audio file row and then check if it's enabled to play
    # If it's enabled it should be played else use the text-to-speech whisper message
    if whisper_message_status:
        if whisper_message_type == 'audio':
            if audio_file and audio_file.enabled:
                app.logger.debug('An audio file is being played instead of text-to-speech for the call back whisper message')
                r.play(audio_file.audio_url)
            else:
                log.info('No audio file has been uploaded or there is an error for call back message')
        else:
            if whisper_message:
                r.say(whisper_message, language=language, voice=DEFAULT_VOICE)
            else:
                log.info('There is no text specified for the text to speach call back message')
    else:
        log.info('The call back message is turned off for phone number')

    with r.dial(
        action=url_for(
            'twilio_inbound.lead_dial_result',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        )
    ) as d:
        waitUrl = get_hold_music(routing_config)

        record = 'do-not-record'
        if routing_config.get('recordCalls', False):
            record = 'record-from-start'

        d.conference(
            name='lead_%d' % lead_id,
            muted=False,
            beep=False,
            startConferenceOnEnter=True,
            endConferenceOnExit=True,
            waitUrl=waitUrl,
            waitMethod='GET',
            record=record,
            eventCallbackUrl=url_for(
                'twilio_inbound.lead_conference_callback',
                lead_id=lead_id,
                _external=True,
                _scheme='https'
            )
        )

    return to_response(r)


@twilio_inbound.route(
    '/twiml/inbound/continue/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_inbound_call_continue(lead_id):
    """ Continuation of the lead's call. Why two methods for the same call?
        To make sure that we call the agent after the lead's call connected.
    """
    # 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)
    app.logger.debug('Lead {}: Continuing inbound call.'.format(lead_id))

    routing_config = storage.routing_config

    routing_type = routing_config.get('routingType', '')

    r = VoiceResponse()

    whisper_message_status = routing_config.get('greetingMessage', '')
    whisper_message_type = routing_config.get('whisperMessageType', '')
    whisper_message = routing_config.get('whisperMessage', '')
    language = routing_config.get('language', 'en')

    log.info('The greeting message is set as {}'.format(whisper_message_status))
    log.info('The greeting message type is set as {}'.format(whisper_message_type))

    if routing_type == 'default':
        routing = routing_config['defaultRouting']

        # Find the the first lead row that includes inbound id
        phonenumber = Lead.query.filter(lead_id == Lead.id).first()

        # Query the audio url table and find the row that matches the inbound id and whisper message type
        audio_file = Audio.query.filter(Audio.inbound_id == phonenumber.inbound_id,
                                        Audio.whisper_message_type == 'whisperMessage')\
            .order_by(Audio.id.desc()).first()

        # Get the routing phone number
        routing_phonenumber = Phone.query.filter(Phone.id == phonenumber.inbound_id).first()

        # Check to see if there's an audio file row and then check if it's enabled to play
        # If it's enabled it should be played else use the text-to-speech whisper message
        if whisper_message_status:
            if whisper_message_type == 'audio':
                if audio_file and audio_file.enabled:
                    app.logger.debug('An audio file is being played instead of text-to-speech for whisper message')
                    r.play(audio_file.audio_url)
                else:
                    log.info('There is no audio file to play for the greeting whisper message to caller')
            else:
                if whisper_message:
                    r.say(whisper_message, language=language, voice=DEFAULT_VOICE)
                else:
                    log.info('There is no text message to play as greeting whisper message to caller')
        else:
            log.info('The caller greeting whisper message is turned off for phone number: {}'.format(routing_phonenumber.phonenumber))

        agents = get_routing_agents(routing)
        if not agents:
            voicemail_redirect(storage, r)
            return to_response(r)

        r.redirect(url=url_for(
            'twilio_inbound.lead_join_conference',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ))

        storage.routing = routing
        storage.state = State.LEAD_ONHOLD
    elif routing_type == 'digits':
        r = digits_choice(lead_id, routing_config)
    else:
        app.logger.error(
            'Unknown routing type for lead {}'.format(lead_id)
        )
        r.say('We\'re sorry, our service is currently unavailable. An agent '
              'will call you back as soon as possible.', voice=DEFAULT_VOICE)

    return to_response(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_join_conference/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_join_conference(lead_id):
    # 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)
    app.logger.debug('Lead {}: Lead joined conference.'.format(lead_id))

    routing_config = storage.routing_config
    routing = storage.routing

    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.

    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()
    subaccount_sid = storage.subaccount_sid
    if isinstance(subaccount_sid, bytes):
        subaccount_sid = subaccount_sid.decode()
    client = subaccount_client(subaccount_sid, partner.id)
    client.queues.create('lead_{}'.format(lead_id))

    call_agents(lead_id, routing)

    with storage.lock():
        if storage.state == State.ONGOING:
            pass
        elif storage.state == State.AGENT_ONHOLD:
            storage.state = State.ONGOING
        else:
            storage.state = State.LEAD_ONHOLD
            # prompt_task = redirect_to_callback_prompt.apply_async(
            #     args=[lead_id], countdown=CALLBACK_PROMPT_DELAY
            # )
            # storage.prompt_task = prompt_task.id

    r = VoiceResponse()
    waitUrl = get_hold_music(routing_config)

    r.enqueue(
        name='lead_%d' % lead_id,
        waitUrl=waitUrl,
        waitUrlMethod='GET'
    )

    return str(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_callback_prompt/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_callback_prompt(lead_id):
    app.logger.debug('Lead {}: Whispering callback prompt audio.'.format(lead_id))

    r = VoiceResponse()
    with r.gather(
        action=url_for(
            'twilio_inbound.lead_callback_prompt_result',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ),
        numDigits=1,
        finishOnKey=''
    ) as g:
        g.say(
            'We\'re sorry about the delay. Press the pound key to be called'
            'back in a few minutes, or any other key to remain on hold.',
            voice=DEFAULT_VOICE
        )
    r.redirect(url_for(
        'twilio_inbound.lead_join_conference',
        lead_id=lead_id,
        _external=True,
        _scheme='https'
    ))

    return str(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_callback_prompt_result/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_callback_prompt_result(lead_id):
    # 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)

    digits = request.form['Digits']

    r = VoiceResponse()
    if digits and digits[0] != '#':
        app.logger.debug('Lead {}: Lead chose to remain on hold.'.format(lead_id))
        storage.state = State.LEAD_ONHOLD
        r.redirect(url=url_for(
            'twilio_inbound.lead_join_conference',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ))
    else:
        app.logger.debug('Lead {}: Lead chose to be called back.'.format(lead_id))
        storage.state = State.CALL_ME_BACK
        redis_db.hset('CALL_ME_BACK', lead_id, time.time())
        Lead.query.filter(Lead.id == lead_id).update(
            {Lead.status: 'retry-pending'}
        )
        db.session.commit()

    return str(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_digits_choice/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_digits_choice(lead_id):
    """ Gather the digits from the 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)
    app.logger.debug('Lead {}: Selecting the routing digit.'.format(lead_id))

    routing_config = storage.routing_config

    r = digits_choice(lead_id, routing_config)

    return to_response(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_digit_result/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_digit_result(lead_id):
    """ Route the lead according to the digit pressed.
    """
    # 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)

    routing_config = storage.routing_config

    digit = request.form['Digits']
    app.logger.debug('Lead {}: The lead has pressed {}.'.format(lead_id, digit))

    r = VoiceResponse()

    for routing in routing_config['digitRoutings']:
        if routing['dialDigit'] == digit:
            agents = get_routing_agents(routing)
            if not agents:
                voicemail_redirect(storage, r)
                return to_response(r)

            r.redirect(url=url_for(
                'twilio_inbound.lead_join_conference',
                lead_id=lead_id,
                _external=True,
                _scheme='https'
            ))

            storage.routing = routing
            storage.state = State.LEAD_ONHOLD
            return to_response(r)

    r.say(
        'You have pressed an invalid digit. Please try again.',
        voice=DEFAULT_VOICE
    )
    r.redirect(url=url_for(
        'twilio_inbound.lead_digits_choice',
        lead_id=lead_id,
        _external=True,
        _scheme='https'
    ))

    return to_response(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_dial_result/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_dial_result(lead_id):
    """ Result of dialing a lead who asked to be called back.
    """
    # 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)

    cancel_agent_calls(lead_id)

    lead = Lead.query.filter(Lead.id == lead_id).first()
    status = request.form['DialCallStatus']

    app.logger.debug('Lead {}: Callback call status {}.'.format(lead_id, status))

    if status in ['completed', 'answered']:
        storage.state = State.CAPTURED
        lead.status = 'completed'
    elif status in ['no-answer']:
        storage.state = State.MISSED
        lead.status = 'unanswered'
    else:
        storage.state = State.MISSED
        lead.status = 'missed'

    lead.endtime = datetime.utcnow()
    lead.duration = (lead.endtime - lead.starttime).total_seconds()
    db.session.commit()

    return str(VoiceResponse())

@twilio_inbound.route(
    '/twiml/inbound/lead_conference_callback/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_conference_callback(lead_id):
    """ Result of recording a lead call.
    """
    r_url = request.form.get('RecordingUrl')

    if r_url:
        lead = Lead.query.filter(Lead.id == lead_id).first()
        partner_account = PartnershipAccount.query. \
            filter(PartnershipAccount.id == lead.partnership_account_id).first()
        partner_name = partner_account.name
        from .tasks import tw_upload_recording
        delay_time = datetime.utcnow() + timedelta(minutes=3)
        tw_upload_recording.apply_async((lead_id, r_url, partner_name), eta=delay_time)
        log.info('The recording upload, if available, will occur in 3 minutes')

    return str(VoiceResponse())


@twilio_inbound.route(
    '/twiml/inbound/lead_voicemail/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_voicemail(lead_id):
    # 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)
    app.logger.debug('Lead {}: Playing voicemail prompt.'.format(lead_id))

    routing_config = storage.routing_config

    language = routing_config.get('language', 'en')
    voicemail_message_type = routing_config.get('voicemailMessageType', '')

    r = VoiceResponse()

    # Find the the first lead row that includes inbound id
    phonenumber = Lead.query.filter(lead_id == Lead.id).first()
    # Query the audio url table and find the row that matches the inbound id and whisper message type
    audio_file = Audio.query.filter(Audio.inbound_id == phonenumber.inbound_id,
                                    Audio.whisper_message_type == 'voicemailMessage').order_by(Audio.id.desc()).first()

    if routing_config.get('voicemail', False):
        # Check to see if there's an audio file row and then check if it's enabled to play
        # If it's enabled it should be played else use the text-to-speech whisper message
        if voicemail_message_type =='audio':
            if audio_file and audio_file.enabled:
                app.logger.debug('An audio file is being played instead of text-to-speech for voicemail whisper message')
                r.play(
                    audio_file.audio_url
                )
                r.play(app.config.get('BEEP_SOUND'))
                r.record(
                    action=url_for(
                        'twilio_inbound.lead_voicemail_callback',
                        lead_id=lead_id,
                        _external=True,
                        _scheme='https'
                    ),
                    maxLength=300,
                    timeout=20
                )
            else:
                log.info('There is no audio file to play for the voicemail. We will use the text-to-speech')
                r.say(
                    routing_config['voicemailMessage'], language=language,
                    voice=DEFAULT_VOICE
                )
                r.play(app.config.get('BEEP_SOUND'))
                r.record(
                    action=url_for(
                        'twilio_inbound.lead_voicemail_callback',
                        lead_id=lead_id,
                        _external=True,
                        _scheme='https'
                    ),
                    maxLength=300,
                    timeout=20
                )
        else:
            r.say(
                routing_config['voicemailMessage'], language=language,
                voice=DEFAULT_VOICE
            )
            r.play(app.config.get('BEEP_SOUND'))
            r.record(
                action=url_for(
                    'twilio_inbound.lead_voicemail_callback',
                    lead_id=lead_id,
                    _external=True,
                    _scheme='https'
                ),
                maxLength=300,
                timeout=20
            )

    return str(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_hangup/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_hangup(lead_id):
    r = VoiceResponse()
    # r.say(
    #     'Sorry, we are not available at the moment. We will remember your '
    #     'call, and will get back to you as soon as possible.',
    #     voice=DEFAULT_VOICE
    # )
    return str(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_voicemail_callback/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_voicemail_callback(lead_id):

    r_url = request.form.get('RecordingUrl')

    if r_url:
        lead = Lead.query.filter(Lead.id == lead_id).first()
        partner_account = PartnershipAccount.query.\
            filter(PartnershipAccount.id == lead.partnership_account_id).first()
        partner_name = partner_account.name
        from .tasks import tw_upload_recording
        delay_time = datetime.utcnow() + timedelta(minutes=3)
        tw_upload_recording.apply_async((lead_id, r_url, partner_name), eta=delay_time)
        log.info('The voicemail upload, if available, will occur in 3 minutes')

    r = VoiceResponse()
    return str(r)


@twilio_inbound.route(
    '/twiml/inbound/lead_callback_call_status/<int:agent_id>/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def lead_callback_call_status(agent_id, lead_id):
    """ Called after the lead hangs up after being called back. """
    # 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)
    app.logger.debug('Lead {}: Callback call finished.'.format(lead_id))

    cancel_agent_calls(lead_id)

    lead = Lead.query.join(Lead.partnership_account)\
        .join(PartnershipAccount.partnership)\
        .filter(Lead.id == lead_id).first()

    lead.endtime = datetime.utcnow()
    lead.duration = (lead.endtime - lead.starttime).total_seconds()

    call_duration = int(request.form.get('CallDuration', '0'))
    if call_duration:
        subscription = lead.partnership_account.subscription
        if not subscription:
            subscription = lead.partnership_account.partnership.subscription
        subscription.update_usage(lead.partnership_account_id, seconds=call_duration)

    if storage.state in [State.ONGOING, State.CAPTURED]:
        storage.state = State.CAPTURED
        lead.status = 'completed'
    elif storage.state != State.CALL_ME_BACK:
        lead.status = 'missed'
        schedule_callback(storage)

    db.session.commit()
    send_notifications(lead)
    # webhooker.trigger_generic_webhook('operational_end_call', lead.id)

    return ''


@twilio_inbound.route(
    '/twiml/inbound/call_result',
    methods=['POST']
)
@csrf.exempt
def call_result_callback():
    """ Called after the lead hangs up. """
    # Declare redis url
    redis_db = redis.StrictRedis(
        host=app.config['REDIS_CONFIG_URL'],
        port=app.config['REDIS_CONFIG_PORT'],
        decode_responses=True
    )

    call_sid = request.form.get('CallSid', '')
    key = 'LEAD_{}'.format(call_sid)
    lead_id = redis_db.get(key)

    storage = CallStorage(redis_db, lead_id)
    routing_config = storage.routing_config

    if not lead_id:
        app.logger.warning('Cannot determine lead ID')
        return ''

    with storage.lock():
        if storage.state in [State.ONGOING, State.CAPTURED]:
            storage.state = State.CAPTURED
        elif storage.state != State.CALL_ME_BACK:
            storage.state = State.MISSED

    app.logger.debug('Lead {}: Lead hung up. Status: {}, call SID: {}'.format(
        lead_id, storage.state, call_sid
    ))

    cancel_agent_calls(lead_id)

    lead = Lead.query.filter(Lead.id == lead_id).first()
    lead.endtime = datetime.utcnow()
    lead.duration = (lead.endtime - lead.starttime).total_seconds()

    if storage.state == State.CAPTURED:
        lead.status = 'completed'
        send_notifications(lead)
        # If recording is enabled then the webhook will be called later with saving of recording
        if not routing_config.get('recordCalls', False):
            webhooker.trigger_generic_webhook('operational_end_call', lead.id)
    elif storage.state == State.MISSED:
        lead.status = 'missed'
        schedule_callback(storage)
        # If voicemail is enabled then the webhook will be called later with saving of voicemail recording
        if not routing_config.get('voicemail', False):
            webhooker.trigger_generic_webhook('operational_end_call', lead.id)

        from ..phonenumbers.tasks import send_email
        send_email.delay(lead.id)
        if routing_config.get('configSMSSetup') and routing_config.get('MissedCallAutoReply'):
            from ..sms.views import send_text_message
            text = routing_config.get('MissedCallAutoReplyText') + ' Reply STOP to unsubscribe.'
            media_url = ''
            send_text_message(lead.inbound_id, lead.phonenumber, text, media_url)
        else:
            log.info('The SMS Configuration is turned off for this phone number')
    db.session.commit()

    return ''


@twilio_inbound.route(
    '/twiml/inbound/agent/<int:agent_id>/digit/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def agent_digit_prompt(agent_id, lead_id):
    # 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)

    app.logger.debug('Lead {}, agent {}: Agent is prompted to press a digit.'.format(
        lead_id, agent_id
    ))

    r = VoiceResponse()

    routing_config = storage.routing_config

    lead = Lead.query.filter(Lead.id == lead_id).one()

    # get the inbound phone number associated with the lead
    phonenumber = Phone.query.filter(Phone.id == lead.inbound_id).first()
    app.logger.debug('The lead inbound id is {} and the phone number id is {}'.format(lead.inbound_id, phonenumber.id))

    # Check to see if there's a firstname saved to the database, which could've
    # been entered manually. If there is, use it as whisper message name instead
    # of the caller id (caller name). If not use caller id in agent whisper message
    if phonenumber.caller_id:
        if lead.firstname:
            storage.caller_name = ''.join((lead.firstname, lead.lastname))
            agent_text = get_agent_text(
                routing_config.get('hiddenInformation', ''), storage.caller_name
            )
        else:
            agent_text = get_agent_text(
                routing_config.get('hiddenInformation', ''), storage.caller_name
            )
    else:
        storage.caller_name = ''
        agent_text = get_agent_text(
            routing_config.get('hiddenInformation', ''), storage.caller_name
        )

    if agent_text:
        r.say(agent_text, voice=DEFAULT_VOICE)

    # If digit prompt is disabled for the phone number, just continue
    if not storage.routing_config.get('digitPrompt', False):
        r.redirect(url_for(
            'twilio_inbound.agent_join_conference',
            agent_id=agent_id,
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ))
        return str(r)

    # Return all active inbound phone numbers setup for buyercall
    phone_numbers = [x.phonenumber for x in Phone.query.filter(Phone.active.is_(True)).all()]
    app.logger.debug('The list of active phone numbers are {}'.format(phone_numbers))

    # Get the information on the agent. We are intrested in their phone number
    agent = Agent.query.filter(Agent.id == agent_id).first()
    app.logger.debug('The agent number before formatting is {}'.format(agent.phonenumber))
    # check the format of the agent number. It needs to be without spaces, special characters and contain +1
    if agent.phonenumber.startswith('+1'):
        agent_phone = agent.phonenumber.replace(" ", "").replace("-", "")
    elif agent.phonenumber.startswith('1'):
        agent_phone = '+' + agent.phonenumber.replace(" ", "").replace("-", "")
    else:
        agent_phone = '+1'+agent.phonenumber.replace(" ", "").replace("-", "")

    app.logger.debug('The agent number after formatting is {} and agent id {}'.format(agent_phone,agent_id))

    # if agent's number is a twilio/buyercall number do not ask for digit press and continue
    if agent_phone in phone_numbers:
        app.logger.debug('The agent has been identified as a buyercall number and no digit press is required. Number: {}'.format(agent_phone))
        r.redirect(url_for(
            'twilio_inbound.agent_join_conference',
            agent_id=agent_id,
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ))
        return str(r)

    with r.gather(
        action=url_for(
            'twilio_inbound.agent_join_conference',
            agent_id=agent_id,
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ),
        numDigits=1,
        timeout=int(app.config.get('AMD_DIGIT_PROMPT_TIMEOUT', 5)),
        finishOnKey=''
    ) as g:
        g.say(
            'Press any key to accept this call, or this call with be canceled.',
            voice=DEFAULT_VOICE
        )

    r.say('This call will be canceled now.', voice=DEFAULT_VOICE)
    app.logger.debug('The call is not connected. The id is {}, the state is {} and the status is {}'.format(lead_id, storage.state,lead.status))
    return str(r)


@twilio_inbound.route(
    '/twiml/inbound/agent/<int:agent_id>/conference/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def agent_join_conference(agent_id, lead_id):
    # Declare redis url
    redis_db = redis.StrictRedis(host=app.config['REDIS_CONFIG_URL'], port=app.config['REDIS_CONFIG_PORT'])

    from .tasks import celery

    r = VoiceResponse()
    storage = CallStorage(redis_db, lead_id)
    routing_config = storage.routing_config

    lead = Lead.query.filter(Lead.id == lead_id).one()

    with storage.lock():
        if storage.agent_id is None:
            storage.agent_id = agent_id
        if storage.agent_id != agent_id:
            call_status = request.form.get('CallStatus')
            app.logger.debug(
                f'Another agent with id {agent_id} has accepted the call with call '
                f'status {call_status} and lead status {lead.status} and state {storage.state}'
            )
            r.say(
                "We're sorry, another agent has accepted this call.",
                voice=DEFAULT_VOICE
            )
            return to_response(r)

    app.logger.debug('Lead {}, agent {}: The agent has joined the conference.'.format(
        lead_id, agent_id,
    ))

    cancel_agent_calls(lead_id, lambda id_, _: id_ != agent_id)

    lead = Lead.query\
        .join(Lead.partnership_account)\
        .join(PartnershipAccount.partnership)\
        .filter(Lead.id == lead_id)\
        .one()

    # import partnership information to get partnership id
    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    with storage.lock():
        if storage.state == State.LEAD_ONHOLD:
            storage.state = State.ONGOING
            if storage.prompt_task:
                celery.control.revoke(storage.prompt_task)
                storage.prompt_task = None
        elif storage.state == State.CALLBACK_PROMPT:
            storage.state = State.AGENT_ONHOLD
            # Redirect lead's call to agent
            subscription = lead.partnership_account.subscription
            if not subscription:
                subscription = lead.partnership_account.partnership.subscription
            twilio_client = subaccount_client(subscription.twilio_subaccount_sid, partner.id)
            twilio_client.calls.route(storage.call_sid, url=url_for(
                'twilio_inbound.lead_join_conference',
                lead_id=lead_id,
                _external=True,
                _scheme='https'
            ))
        else:
            app.logger.error('Invalid state: {}'.format(storage.state))

    lead.agent_id = agent_id
    lead.status = 'in-progress'
    db.session.commit()

    webhooker.trigger_generic_webhook('operational_agent_call', lead.id)
    if agent_id and lead:
        from buyercall.blueprints.contacts.models import Contact
        contact = Contact.query.filter(Contact.id == lead.contact_id).first()
        agent = Agent.query.filter(Agent.id == agent_id).first()
        if contact and agent and contact.agent_id != agent.id:
            contact.agent_id = agent_id
            contact.agent_assigned = agent.full_name
            db.session.commit()
            from buyercall.blueprints.mobile.utils import send_agent_push_notification
            send_agent_push_notification(contact)

    record = 'do-not-record'
    if routing_config.get('recordCalls', False):
        record = 'record-from-answer-dual'
    app.logger.debug('the recording routing value is {} and the record value is {}'.format(routing_config.get('recordCalls'),record))

    r.play(app.config.get('BEEP_SOUND'), loop=1)

    with r.dial(record=record,
                action=url_for(
                'twilio_inbound.lead_conference_callback',
                lead_id=lead_id,
                _external=True,
                _scheme='https')
                ) as d:
        d.queue(
            name='lead_%d' % lead_id
        )

    return to_response(r)


@twilio_inbound.route(
    '/twiml/inbound/agent/<int:agent_id>/callback/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def agent_callback_lead(agent_id, lead_id):
    # 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)
    app.logger.debug('{}: {}'.format(lead_id, storage.state))

    lead = Lead.query.join(Lead.partnership_account).join(PartnershipAccount.partnership).filter(Lead.id == lead_id).first()
    lead.status = 'ringing'
    db.session.commit()

    redis_db.hdel('CALL_ME_BACK', lead_id)

    routing_config = storage.routing_config

    r = VoiceResponse()

    agent_text = get_agent_text(
        routing_config.get('hiddenInformation', ''), storage.caller_name,
        callback=True, manual_call=storage.manual_call
    )

    if agent_text:
        r.say(agent_text, voice=DEFAULT_VOICE)

    # If digit prompt is disabled for the phone number, just continue
    if not storage.routing_config.get('digitPrompt', False):
        r.redirect(url_for(
            'twilio_inbound.agent_callback_lead_result',
            agent_id=agent_id,
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ))
        return str(r)

    with r.gather(
            action=url_for(
                'twilio_inbound.agent_callback_lead_result',
                agent_id=agent_id,
                lead_id=lead_id,
                _external=True,
                _scheme='https'
            ),
            numDigits=1,
            timeout=int(app.config.get('AMD_DIGIT_PROMPT_TIMEOUT', 5)),
            finishOnKey=''
    ) as g:
        g.say(
            'Press any key to accept this call, or this call with be canceled.',
            voice=DEFAULT_VOICE
        )

    r.say('This call will be canceled now.', voice=DEFAULT_VOICE)
    call_status = request.form.get('CallStatus')
    app.logger.debug('The id is {}, the state is {} and the call status is {}'.format(
        lead_id, storage.state, call_status
    ))
    storage.state = State.MISSED
    app.logger.debug(
        'The id is {}, The state is {} and status is {} and storage agent id is {} and '
        'the call status is {}'.format(
            lead_id, storage.state, lead.status, storage.agent_id, call_status
        )
    )
    return str(r)


@twilio_inbound.route(
    '/twiml/inbound/agent/<int:agent_id>/callback_digit_result/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def agent_callback_lead_result(agent_id, lead_id):
    # 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:
        return

    # import partnership information to get partnership id
    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    r = VoiceResponse()
    storage = CallStorage(redis_db, lead_id)

    app.logger.debug('The storage agent id is {} and the agent id is {}'.format(storage.agent_id,agent_id))

    with storage.lock():
        if storage.agent_id is None:
            storage.agent_id = agent_id
        if storage.agent_id != agent_id:
            r.say(
                "We're sorry, another agent has accepted this call.",
                voice=DEFAULT_VOICE
            )
            app.logger.debug('The call was accepted by another agent. The storage agent id is {} and the agent id is {}'.format(storage.agent_id, agent_id))
            return to_response(r)

    cancel_agent_calls(lead_id, lambda id_, _: id_ != agent_id)

    digits = request.form.get('Digits', '')
    app.logger.debug('The agent pressed {}'.format(digits))

    with r.dial() as d:
        waitUrl = url_for(
            'twilio_inbound.twiml_hold_music', file_guid='loop.mp3',
            _external=True,
            _scheme='https'
        )
        d.conference(
            name='lead_%d' % lead_id,
            muted=False,
            beep=False,
            waitUrl=waitUrl,
            waitMethod='GET',
            startConferenceOnEnter=True,
            endConferenceOnExit=True
        )

    # Now call the lead
    subscription = lead.partnership_account.subscription
    if not subscription:
        subscription = lead.partnership_account.partnership.subscription

    twilio_client = subaccount_client(subscription.twilio_subaccount_sid, partner.id)
    try:
        call = twilio_client.calls.create(
            from_=lead.inbound.phonenumber,
            to=lead.phonenumber,
            url=url_for(
                'twilio_inbound.lead_callback',
                lead_id=lead_id,
                agent_id=agent_id,
                _external=True,
                _scheme='https'
            ),
            status_callback=url_for(
                'twilio_inbound.lead_callback_call_status',
                agent_id=agent_id,
                lead_id=lead_id,
                _external=True,
                _scheme='https'
            ))
        storage.call_sid = call.sid
    except Exception as e:
        app.logger.error(traceback.format_exc())
        pass

    return to_response(r)


@twilio_inbound.route(
    '/twiml/inbound/agent_callback_call_status/<int:agent_id>/<int:lead_id>',  # noqa
    methods=['POST']
)
@csrf.exempt
def agent_callback_call_status(agent_id, lead_id):
    # 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)
    app.logger.debug('{}: {}'.format(lead_id, storage.state))

    call_sid = request.form.get('CallSid')
    call_status = request.form.get('CallStatus')
    app.logger.debug('The call status is {}'.format(call_status))

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

    # import partnership information to get partnership id
    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    # Update agent response time
    time_key = 'TIME_{}_{}'.format(lead_id, agent_id)
    if call_status == 'in-progress':
        BusyAgents.add(agent_id)

        timestamp_now = time.time()
        timestamp_then = float(redis_db.get(time_key) or 0)

        with storage.lock():
            if storage.agent_id is None:
                storage.agent_id = agent_id
            active_agent_id = storage.agent_id

        #if active_agent_id == str(agent_id):
        #    cancel_agent_calls(lead_id, lambda id_, _: id_ != agent_id)

        if timestamp_then:
            Lead.query.filter(Lead.id == lead_id).update({
                Lead.response_time_seconds: timestamp_now - timestamp_then
            })
            db.session.commit()
        return ''

    # The call has ended
    BusyAgents.remove(agent_id)

    answered_by = request.form.get('AnsweredBy', '')
    if answered_by == 'machine':
        call_status = 'no-answer'

    calls_left = storage.remove_agent_call(agent_id, call_sid)
    storage.agent_id = None

    if calls_left == 0:
        if storage.state == State.CALLBACK:
            # The agent has selected to call back lead? but the agent didn't
            # answer.
            # Mark lead as missed
            Lead.query.filter(Lead.id == lead_id).update(
                {Lead.status: 'missed'}
            )
            db.session.commit()
            schedule_callback(storage)
        elif storage.state == State.MISSED:
            schedule_callback(storage)
        elif storage.state not in [
            State.CALL_ME_BACK, State.ONGOING, State.CAPTURED
        ]:
            # Cancel lead's call
            twilio_client = subaccount_client(storage.subaccount_sid, partner.id)

            try:
                # TODO: Do we even need this?
                twilio_client.calls(storage.call_sid).update(status='completed')
                app.logger.debug('The call has been hangup for lead {}...'.format(lead_id))
            except TwilioRestException as e:
                # Likely the user hung up on us before we could do the same
                app.logger.warning(str(e))

            if not schedule_callback(storage):
                # Mark lead as missed
                Lead.query.filter(Lead.id == lead_id).update(
                    {Lead.status: 'missed'}
                )
                db.session.commit()

    if storage.state == State.CALL_ME_BACK:
        Lead.query.filter(Lead.id == lead_id).update(
            {Lead.status: 'retry-pending'}
        )
        db.session.commit()
        app.logger.debug('Send out email for state {} and lead status {}'.format(storage.state, lead.status))
        send_notifications(lead)
    elif storage.state == State.MISSED and call_status == 'completed' or call_status == 'no-answer':
        Lead.query.filter(Lead.id == lead_id).update(
            {Lead.status: 'missed'}
        )
        db.session.commit()
        if calls_left == 0:
            send_notifications(lead)
            app.logger.debug(
                'Send out email for state {} and lead status {} and storage status {}'.format(storage.state,
                                                                                              lead.status,
                                                                                              call_status))
    elif storage.state == State.CAPTURED and call_status == 'completed':
        Lead.query.filter(Lead.id == lead_id).update(
            {Lead.status: 'completed'}
        )
        db.session.commit()

    # Update minutes consumed
    call_duration = int(request.form.get('CallDuration', '0'))
    if call_duration:
        lead = Lead.query.join(Lead.partnership_account).join(PartnershipAccount.partnership).filter(Lead.id == lead_id).first()
        subscription = lead.partnership_account.subscription
        if not subscription:
            subscription = lead.partnership_account.partnership.subscription
        subscription.update_usage(lead.partnership_account_id, seconds=call_duration)

    app.logger.debug('The call state is {} and lead status is {} and the call status is {}'.format(storage.state, lead.status, call_status))

    return ''


@twilio_inbound.route(
    '/twiml/inbound/agent_inbound_call_status/<int:agent_id>/<int:lead_id>',
    methods=['POST']
)
@csrf.exempt
def agent_inbound_call_status(agent_id, lead_id):
    # 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)

    lead = Lead.query.join(Lead.partnership_account).join(PartnershipAccount.partnership).filter(
        Lead.id == lead_id).first()

    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    call_sid = request.form.get('CallSid')
    call_duration = int(request.form.get('CallDuration', '0'))
    call_status = request.form['CallStatus']

    agent = Agent.query.options(load_only('firstname', 'lastname')).filter(
        Agent.id == agent_id
    ).first()
    agent_name = agent.full_name if agent else ''

    app.logger.debug(
        'Lead {}, agent {} ({}): call status: {}, '
        'call duration: {}, call state: {}'.format(
            lead_id, agent_id, agent_name, call_status, call_duration, storage.state
        ))

    if call_status in ['ringing', 'in-progress']:
        update_response_time(call_status, agent_id, lead_id, redis_db)
    if call_status == 'ringing':
        return ''
    if call_status == 'in-progress':
        # Mark the agent as busy
        BusyAgents.add(agent_id)
        return ''

    # We can be sure the call is over, one way or another

    # Mark the agent as free
    BusyAgents.remove(agent_id)
    answered_by = request.form.get('AnsweredBy', '')

    if answered_by == 'machine' or (
          call_status == 'completed' and not call_duration):
        call_status = 'no-answer'

    calls_left = storage.remove_agent_call(agent_id, call_sid)

    # Update minutes consumed
    if call_duration:
        subscription = lead.partnership_account.subscription
        if not subscription:
            subscription = lead.partnership_account.partnership.subscription
        subscription.update_usage(lead.partnership_account_id, seconds=call_duration)

    routing = storage.routing
    if call_status in ['busy', 'failed', 'no-answer'] and storage.state in [
        State.NEW, State.LEAD_ONHOLD, State.CALLBACK_PROMPT
    ]:
        if routing['callOrder'] == 'simultaneous' or storage.is_group:
            app.logger.debug('Agents still being called right now: {}'.format(calls_left))
            if calls_left == 0:
                if not storage.is_group:
                    retry_once_more(routing, lead_id)
                else:
                    app.logger.debug('Lead {}: All calls for current group failed; continuing with next sequential agent...'.format(
                        lead_id,
                    ))
                    call_agent_sequence(routing, lead_id)
        else:
            app.logger.debug('Lead {}, agent "{}": Call failed; continuing with next agent...'.format(
                lead_id, agent_name
            ))
            call_agent_sequence(routing, lead_id)
    elif call_status == 'completed':
        # If we haven't accepted the call, and the lead voicemail is setup,
        # redirect them to voicemail.
        if storage.state in [
            State.NEW, State.LEAD_ONHOLD, State.CALLBACK_PROMPT
        ]:
            if routing['callOrder'] == 'simultaneous':
                if calls_left == 0:
                    redirect_to_voicemail(lead_id)
            else:
                app.logger.debug("Call not accepted; continuing with next agent...")
                call_agent_sequence(routing, lead_id)
        elif calls_left == 0:
            # Otherwise, cancel lead's call
            twilio_client = subaccount_client(storage.subaccount_sid, partner.id)
            twilio_client.calls(storage.call_sid).update(status='completed')
            app.logger.debug('The calls_left is 0 and the call has been hungup for lead {}...'.format(lead_id))

    return ''


def cancel_agent_calls(lead_id, predicate=None):
    # 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
    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    app.logger.debug('Lead {}: Canceling {} agent calls...'.format(
        lead_id, 'some' if predicate else 'all pending'
    ))

    storage = CallStorage(redis_db, lead_id)

    agent_calls = storage.clear_agent_calls(predicate)
    if not agent_calls:
        return

    twilio_client = subaccount_client(storage.subaccount_sid, partner.id)

    for (agent_id, call_sid) in agent_calls:
        try:
            app.logger.debug('...agent {}, call {}'.format(agent_id, call_sid))
            twilio_client.calls(call_sid).update(status='completed')
        except Exception as e:
            app.logger.info('Cannot hangup call {}'.format(call_sid))


def digits_choice(lead_id, routing_config):
    """ :returns twiml.Response
    """
    no_digit_whisper_message = routing_config.get('noDigitWhisperMessage', '')
    no_digit_whisper_message_type = routing_config.get('noDigitWhisperMessageType')
    digit_whisper_message = routing_config.get('digitWhisperMessage', '')
    digit_whisper_message_type = routing_config.get('digitWhisperMessageType')
    whisper_message_type = routing_config.get('whisperMessageType')
    whisper_message = routing_config.get('whisperMessage')
    language = routing_config.get('language', 'en')

    r = VoiceResponse()

    # Find the the first lead row that includes inbound id
    phonenumber = Lead.query.filter(lead_id == Lead.id).first()

    # Query the audio url table and find the row that matches the inbound id and whisper message type
    audio_file = Audio.query.filter(Audio.inbound_id == phonenumber.inbound_id,
                                    Audio.whisper_message_type == 'whisperMessage').order_by(Audio.id.desc()).first()

    # Check to see if there's an audio file row and then check if it's enabled to play
    # If it's enabled it should be played else use the text-to-speech whisper message
    if whisper_message_type == 'audio':
        if audio_file and audio_file.enabled:
            app.logger.debug('An audio file is being played instead of text-to-speech for whisper message')
            r.play(audio_file.audio_url)
        else:
            if whisper_message:
                r.say(whisper_message, language=language, voice=DEFAULT_VOICE)
                log.info('No audio file for the whisper message available')
    else:
        if whisper_message:
            r.say(whisper_message, language=language, voice=DEFAULT_VOICE)
        else:
            log.info('There is no text to speak for the whisper message')

    with r.gather(
        action=url_for(
            'twilio_inbound.lead_digit_result',
            lead_id=lead_id,
            _external=True,
            _scheme='https'
        ),
        numDigits=1
    ) as g:
        # Query the audio url table and find the row that matches the inbound id and whisper message type
        digit_audio_file = Audio.query.filter(Audio.inbound_id == phonenumber.inbound_id,
                                        Audio.whisper_message_type == 'digitWhisperMessage')\
            .order_by(Audio.id.desc()).first()
        # Request digit press input based of direction of call
        if digit_whisper_message_type == 'audio':
            if digit_audio_file and digit_audio_file.enabled:
                app.logger.debug('An audio file is being played to give the caller menu option for auto attendance')
                g.play(digit_audio_file.audio_url,loop=1)
            else:
                app.logger.debug('text-to-speech is used to give the caller menu option for auto attendance')
                g.say(digit_whisper_message, language=language,
                            voice=DEFAULT_VOICE)
        else:
            if digit_whisper_message:
                g.say(digit_whisper_message, language=language, voice=DEFAULT_VOICE)
            else:
                log.info('No text message added for digit whisper message')

        # Query the audio url table and find the row that matches the inbound id and whisper message type
        no_digit_audio_file = Audio.query.filter(Audio.inbound_id == phonenumber.inbound_id,
                                        Audio.whisper_message_type == 'noDigitWhisperMessage')\
            .order_by(Audio.id.desc()).first()

        if no_digit_whisper_message_type == 'audio':
            if no_digit_audio_file and no_digit_audio_file.enabled:
                for i in range(3):
                    g.pause(length=5)
                    g.play(no_digit_audio_file.audio_url)
                g.pause(length=5)
                app.logger.debug('An audio file is being played instead of text-to-speech for the no digit press whisper message')
        else:
            if no_digit_whisper_message:
                for i in range(3):
                    g.pause(length=5)
                    g.say(
                        no_digit_whisper_message, language=language,
                        voice=DEFAULT_VOICE
                    )
                g.pause(length=5)
    return r


def voicemail_redirect(storage, r):
    routing_config = storage.routing_config

    try:
        # If voicemail enabled, redirect there...
        voicemail = routing_config.get('voicemail', False)
        if voicemail:
            app.logger.info('Redirecting lead to voicemail')

            r.redirect(url=url_for(
                'twilio_inbound.lead_voicemail',
                lead_id=storage.lead_id,
                _external=True,
                _scheme='https'
            ))
        else:
            # ...otherwise, hang up
            r.redirect(url=url_for(
                'twilio_inbound.lead_hangup',
                lead_id=storage.lead_id,
                _external=True,
                _scheme='https'
            ))
    except Exception as e:
        app.logger.error(traceback.format_exc())
        return


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

    from .tasks import celery

    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
    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    storage = CallStorage(redis_db, lead_id)
    routing_config = storage.routing_config

    with storage.lock():
        if storage.prompt_task:
            celery.control.revoke(storage.prompt_task)
            storage.prompt_task = None

    twilio_client = subaccount_client(storage.subaccount_sid, partner.id)

    try:
        # If voicemail enabled, redirect there...
        voicemail = routing_config.get('voicemail', False)
        if voicemail:
            app.logger.debug('Lead {}: redirecting to voicemail'.format(lead_id))
            twilio_client.calls(storage.call_sid).update(
                method="POST",
                url=url_for(
                    'twilio_inbound.lead_voicemail',
                    lead_id=lead_id,
                    _external=True,
                    _scheme='https'
                )
            )
        else:
            app.logger.debug('Lead {}: voicemail not enabled for routing; hanging up.'.format(lead_id))

            # ...otherwise, hang up
            twilio_client.calls(storage.call_sid).update(
                method="POST",
                url=url_for(
                    'twilio_inbound.lead_hangup',
                    lead_id=lead_id,
                    _external=True,
                    _scheme='https'
                )
            )
    except Exception as e:
        app.logger.error(traceback.format_exc())
        return


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

    from .tasks import call_simultaneous

    call_order = routing.get('callOrder', '')

    agents = get_routing_agents(routing)

    if call_order == 'shuffle':
        import random
        app.logger.debug('Shuffling agent list...')
        random.shuffle(agents)
        call_order = 'sequence'

    if call_order == 'sequence':
        storage = CallStorage(redis_db, lead_id)
        agent_ids = [agent.id for agent in agents]
        app.logger.info('Calling agents {}'.format(agent_ids))
        app.logger.debug('{}: agent list in Redis'.format(agent_ids))
        storage.set_agents_to_call(agent_ids)

        call_agent_sequence(routing, lead_id)
    elif call_order == 'simultaneous':
        agent_ids = [a.id for a in agents]
        call_simultaneous.delay(agent_ids, lead_id, routing)


def retry_once_more(routing, lead_id):
    """ Try calling the agents once more, up to the maximum number of retries.
        Return False if the maximum number of retries exceeded, otherwise True.
    """
    # 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)
    cnt = storage.inc_retry_cnt()
    if cnt <= int(routing['retryRouting']):
        app.logger.debug('Retrying once more for lead {}...'.format(lead_id))
        call_agents(lead_id, routing)
        return True

    redirect_to_voicemail(lead_id)
    return False


def call_agent_sequence(routing, lead_id):
    # 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)
    lead = Lead.query.join(Lead.inbound).filter(Lead.id == lead_id).first()

    # import partnership information to get partnership id
    partner_account = PartnershipAccount.query \
        .filter(PartnershipAccount.id == lead.partnership_account_id).first()
    # Get the partner id to get relevant twilio credentails with twilio client
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    twilio_client = subaccount_client(storage.subaccount_sid, partner.id)
    call = None

    while not call:
        agent_id = storage.next_agent_to_call()
        if not agent_id:
            retry_once_more(routing, lead_id)
            return

        agent = Agent.query.filter(Agent.id == agent_id).first()
        if not agent:
            continue

        agents = [agent]
        if agent.is_group:
            agents = agent.agents
            storage.is_group = True

        for agent in agents:
            try:
                agent_number, extension = get_agent_number(agent, routing)
                digits = 'wwww' + extension if extension else None

                with storage.lock():
                    if storage.state in [
                        State.NEW, State.LEAD_ONHOLD, State.CALLBACK_PROMPT
                    ]:
                        call = twilio_client.calls.create(
                            from_=lead.inbound.phonenumber,
                            to=agent_number,
                            timeout=AGENT_TIMEOUT,
                            url=url_for(
                                'twilio_inbound.agent_digit_prompt',
                                agent_id=agent.id,
                                lead_id=lead_id,
                                _external=True,
                                _scheme='https'
                            ),
                            status_callback=url_for(
                                'twilio_inbound.agent_inbound_call_status',
                                agent_id=agent.id,
                                lead_id=lead_id,
                                _external=True,
                                _scheme='https'
                            ),
                            send_digits=digits,
                            status_callback_event=['ringing', 'answered', 'completed'],
                        )
                        storage.push_agent_call(agent.id, call.sid)
            except Exception as e:
                app.logger.warning(traceback.format_exc())
                app.logger.warning('Error calling agent {}...'.format(
                    agent.id
                ))


@twilio_inbound.route('/twiml/inbound/hold-music/<file_guid>', methods=['GET', 'POST'])
def twiml_hold_music(file_guid):
    r = VoiceResponse()
    r.play(url=url_for('phonenumbers.hold_music', file_guid=file_guid, _external=True, _scheme='https'), loop='0'
           )
    # r.redirect()
    response = make_response(str(r))
    response.headers['Content-Type'] = 'text/xml; charset=utf-8'
    return response


@twilio_inbound.route('/twiml/inbound/custom-hold-music/<file_guid>', methods=['GET'])
def twiml_custom_hold_music(file_guid):
    r = VoiceResponse()
    hold_music = HoldMusic.query.filter(HoldMusic.uuid == file_guid).first()
    hold_music_url = hold_music.url
    if hold_music_url:
        r.play(hold_music_url, loop='0'
               )
    else:
        r.play('https://buyercall.com/inbound/hold-music/loop.mp3', loop=0
               )
        log.info('There is no audio file to play as hold music. Reverting to ring tone')
    r.redirect()
    response = make_response(str(r))
    response.headers['Content-Type'] = 'text/xml; charset=utf-8'
    return response


@twilio_inbound.route('/twiml/inbound/standard-hold-music', methods=['GET'])
def standard_hold_music():
    r = VoiceResponse()
    r.play('https://s3.amazonaws.com/buyercall-static-sounds/holdmusic.mp3'
           )
    r.say('Thank you for waiting. Your call is important to us. Please stay on the line.',
          voice=DEFAULT_VOICE
          )
    r.play('https://s3.amazonaws.com/buyercall-static-sounds/holdmusic.mp3', loop=2
           )
    r.say('Thank you for waiting. Your call is important to us. Please stay on the line.',
          voice=DEFAULT_VOICE
          )
    r.play('https://s3.amazonaws.com/buyercall-static-sounds/holdmusic.mp3', loop=5
           )
    r.say('Thank you for waiting. Your call is important to us. Please stay on the line.',
          voice=DEFAULT_VOICE
          )
    r.play('https://s3.amazonaws.com/buyercall-static-sounds/holdmusic.mp3', loop=5
           )
    r.redirect()
    response = make_response(str(r))
    response.headers['Content-Type'] = 'text/xml; charset=utf-8'
    return response


def get_hold_music(routing_config):
    if not routing_config.get('playHoldMusic'):
        waitUrl = url_for(
            'twilio_inbound.twiml_hold_music', file_guid='loop.mp3',
            _external=True,
            _scheme='https'
        )
    elif (routing_config.get('customHoldMusic') and
          routing_config.get('holdMusicId')):
        waitUrl = url_for('twilio_inbound.twiml_custom_hold_music', file_guid=routing_config['holdMusicId'],
                          _external=True, _scheme='https')
    else:
        waitUrl = url_for(
            'twilio_inbound.standard_hold_music',
            _external=True,
            _scheme='https'
        )

    return waitUrl


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

    from ..widgets.models import split_emails

    storage = CallStorage(redis_db, lead.id)
    if storage.manual_call:
        app.logger.debug('Notification email suppressed.')
        return

    inbound = Phone.query.filter(
        Phone.id == lead.inbound_id,
        Phone.partnership_account_id == lead.partnership_account_id
    ).first()
    if not inbound:
        return

    notifications = inbound.notifications
    routings = inbound.routing_config

    try:
        if notifications['notifyLeads'] == 'none':
            return None
    except KeyError:
        log.info('The notifications dict does not have a notifyLeads key for inbound id: {}'.format(inbound.id))

    if notifications == {}:
        log.info('The notifications is an empty dict for inbound id: {}'.format(inbound.id))
        return None

    # Send eventual notifications
    emails = []
    adf_emails = []

    user_email = None
    user = User.query.filter(
        User.partnership_account_id == lead.partnership_account_id,
        User.active,
        User.is_deactivated.is_(False),
        User.role == 'admin'
    ).first()
    if user:
        user_email = user.email

    agent_ids = [
        a['id'] for a in inbound.routing_config['defaultRouting']['agents']
    ]
    agent_emails = [
        a.email
        for a in Agent.query.options(load_only('email')).filter(
            Agent.partnership_account_id == lead.partnership_account_id,
            Agent.id.in_(agent_ids)
        ).all()
    ]

    partner_account = PartnershipAccount.query.filter(PartnershipAccount.id == lead.partnership_account_id).first()
    partner = Partnership.query.filter(Partnership.id == partner_account.partnership_id).first()

    if notifications['notifyLeads'] == 'missed' and lead.status == 'missed':
        emails.extend(split_emails(notifications['notifyMissedCustom']))
        if notifications['notifyMissedAgents']:
            emails.extend(agent_emails)
        if notifications['notifyMissedMe'] and user_email:
            emails.append(user_email)
    if notifications['notifyLeads'] == 'all':
        emails.extend(split_emails(notifications['notifyAllCustom']))
        if notifications['notifyAllAgents']:
            emails.extend(agent_emails)
        if notifications['notifyAllMe'] and user_email:
            emails.append(user_email)

    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['partner_logo'] = partner.logo
    ctx['company'] = partner.name

    if partner.partner_url:
        ctx['lead_contact_url'] = partner.partner_url + '/contacts/contact/' + str(lead.contact_id)
    else:
        ctx['lead_contact_url'] = url_for('contacts.contact_lead_page', id=lead.contact_id, _external=True, _scheme='https')

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

    if lead.caller_id:
        ctx['caller_id_label'] = 'Caller Id:'
        ctx['caller_id'] = lead.caller_id

    if lead.recording_url:
        ctx['vm_div_style'] = 'block'
    else:
        ctx['vm_div_style'] = 'none'

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

    if lead.inbound:
        ctx['reference'] = lead.inbound.routing_config.get(
            'hiddenInformation', ''
        )

    # webhooker.trigger_generic_webhook('operational_end_call', lead.id)

    try:
        if routings.get('notifyAdf'):
            if routings['notifyAdfCustom'] is not None and routings['notifyAdfCustom'] is not '':
                multi_adf_emails = split_emails(routings['notifyAdfCustom'])

                if multi_adf_emails is not None:
                    adf_emails.extend(multi_adf_emails)
                else:
                    adf_emails.append(routings['notifyAdfCustom'])

            ctx['lead_comments'] = 'Call status: ' + lead.status
            ctx['receiver'] = ''
            agent_name = lead.agent_name
            friendly_name = lead.source
            phonenumber = lead.inbound.phonenumber
            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'] = ''

            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 friendly_name is not None and friendly_name is not '':
                ctx['lead_comments'] = ctx['lead_comments'] + ', Call source: ' + friendly_name
                ctx['receiver'] = friendly_name

            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 is not None and len(adf_emails) > 0:
                # Render text template for adf email
                adf_lead_email_template = _try_renderer_template('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())
        log.error('something went wrong with the ADF email notification for lead id: {}'.format(lead.id))

    try:
        if emails:
            log.debug('The email list are {}'.format(emails))
            if lead.status == 'missed' and storage.state == State.MISSED:
                # Render html template for email
                missed_lead_email_template = _try_renderer_template('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
                               )
            elif lead.status == 'missed' and storage.state == State.CALLBACK:
                # Render html template for email
                callback_lead_email_template = _try_renderer_template('mail/missed_lead', ext='html', **ctx)
                send_ses_email(recipients=emails,
                               p_id=partner.id,
                               subject=partner.name + ' - Missed lead notification',
                               html=callback_lead_email_template
                               )
            elif lead.status == 'retry-pending' or lead.status == 'missed' and storage.state == State.CALL_ME_BACK:
                # Render html template for email
                retry_lead_email_template = _try_renderer_template('mail/call_back', ext='html', **ctx)
                send_ses_email(recipients=emails,
                               p_id=partner.id,
                               subject=partner.name + ' - Missed Call-back lead notification',
                               html=retry_lead_email_template
                               )
            elif lead.status == 'completed' and storage.state == State.CAPTURED:
                # Render html template for email
                completed_lead_email_template = _try_renderer_template('mail/captured_lead', ext='html', **ctx)
                send_ses_email(recipients=emails,
                               p_id=partner.id,
                               subject=partner.name + ' - Answered lead notification',
                               html=completed_lead_email_template
                               )
            else:
                # Render html template for email
                captured_lead_email_template = _try_renderer_template('mail/captured_lead', ext='html', **ctx)
                send_ses_email(recipients=emails,
                               p_id=partner.id,
                               subject='{} - New lead notification'.format(partner.name),
                               html=captured_lead_email_template
                               )
        else:
            log.info('Theres no normal emails added for notifications on phone number: {} and lead id {}'.
                     format(lead.inbound.phonenumber, lead.id))
    except Exception as e:
        log.error(traceback.format_exc())


# DEBUG
def to_response(r):
    """ Transform a Twiml Response to a Flask response object. """
    import xml.dom.minidom
    xml = xml.dom.minidom.parseString(str(r))
    return xml.toprettyxml()