"""
Translate AMI events to call events.

The ChannelManager takes AMI or AMI-like events and translates them to
more higher level events.

During operation, the ChannelManager instance is fed AMI events through::

    on_event(self, event)

If it determines that something interesting has happened, it fires one
of these four events::

    on_b_dial(self, call_id, caller, callee)
    on_up(self, call_id, caller, callee)
    on_transfer(self, call_id, merged_id, transferor, party1, party1)
    on_hangup(self, call_id, caller, callee, reason)

You should override these ChannelManager methods in your
subclass and add the desired behaviour for those events.
"""
from cacofonisk.constants import (AST_CAUSE_ANSWERED_ELSEWHERE, AST_CAUSE_CALL_REJECTED, AST_CAUSE_NORMAL_CLEARING,
                                  AST_CAUSE_NO_ANSWER, AST_CAUSE_NO_USER_RESPONSE, AST_CAUSE_UNKNOWN,
                                  AST_CAUSE_USER_BUSY, AST_STATE_DIALING, AST_STATE_DOWN, AST_STATE_RING,
                                  AST_STATE_RINGING, AST_STATE_UP)
from .callerid import CallerId


class MissingChannel(KeyError):
    pass


class MissingUniqueid(KeyError):
    pass


class BridgedError(Exception):
    pass


class Channel(object):
    """
    A Channel holds Asterisk channel state.

    It can be renamed, linked, tied to other channels, bridged,
    masqueraded and so on. All of the above is typical low level
    Asterisk channel behaviour.

    Together with the ChannelManager, the Channel keeps track of the
    state of all open channels through the events generated by a running
    Asterisk instance.
    """

    def __init__(self, event, channel_manager):
        """
        Create a new channel instance. Takes a channel manager managing
        it as argument for callbacks.

        Args:
            event (Dict): A dictionary with Asterisk AMI data. Only the
                Newchannel event should be passed. The ChannelManager
                does the other translations.
            channel_manager (ChannelManager): The channel manager that takes
                the AMI events and passes state changes to individual channels
                or groups of channels at once.

        Example::

            channel = Channel(
                event={
                    'AccountCode': '',
                    'CallerIDName': 'Foo bar',
                    'CallerIDNum': '+31501234567',
                    'Channel='SIP/voipgrid-siproute-dev-0000000c',
                    'ChannelState': '0',
                    'ChannelStateDesc': 'Down',
                    'Context': 'voipgrid_in',
                    'Event': 'Newchannel',
                    'Exten': '+31501234567',
                    'Privilege: 'call,all',
                    'Uniqueid': 'vgua0-dev-1442239323.24',
                    'content: '',
                },
                channel_manager=channel_manager)
        """
        self._channel_manager = channel_manager

        # Uses of this instance may put data in the custom dict. We take
        # care to link this on masquerade.
        self.custom = {}

        self._name = event['Channel']
        self._id = event['Uniqueid']
        self._fwd_local_bridge = None
        self._back_local_bridge = None
        self.back_dial = None
        self.fwd_dials = []

        self._state = int(event['ChannelState'])  # 0, Down
        self._bridged = set()
        self._accountcode = event['AccountCode']
        self._exten = event['Exten']

        self.fwd_queue_agents = []
        self.back_queue_caller = None

        # If this is a SIP/<accountcode>- channel, then this is an
        # outbound channel where the CLI is wrong. We could set the
        # accountcode, but we overwrite it in get_callerid later on
        # anyway.
        if (len(self._accountcode) == 9 and
                self._accountcode.isdigit() and
                event['Channel'].startswith('SIP/{}-'.format(event['AccountCode']))):
            # This is a destination channel. Set exten as CLI.
            self._callerid = CallerId(name='', number=self._exten)
        else:
            # This is a source channel? Or a non-SIP channel? Set as
            # much info as we have at this point.
            self._callerid = CallerId(
                code=int(self._accountcode or 0),
                name=event['CallerIDName'], number=event['CallerIDNum'],
                is_public=True
            )

        self._trace('new {!r}'.format(self))

    def __repr__(self):
        return (
            '<Channel('
            'name={self._name!r} '
            'id={self._id!r} '
            'forward_local_bridge={next} '
            'backward_local_bridge={prev} '
            'state={self._state} '
            'accountcode={self._accountcode} '
            'cli={self._callerid} '
            'exten={self._exten!r})>').format(
            self=self,
            next=(self._fwd_local_bridge and self._fwd_local_bridge.name),
            prev=(self._back_local_bridge and self._back_local_bridge.name))

    def _trace(self, msg):
        """
        _trace can be used to follow interesting events.
        """
        pass

    @property
    def is_relevant(self):
        """
        is_relevant returns true if the channel is not a Zombie channel.

        Returns:
            bool: True if self is a SIP channel and not a zombie channel.
        """
        return self.is_sip and not self.is_zombie

    @property
    def is_zombie(self):
        """
        Whether the current channel is ZOMBIE channel.

        Returns:
            bool: True if the channel is a zombie, false otherwise.
        """
        return self.name.endswith('<ZOMBIE>')

    @property
    def is_sip(self):
        """
        Whether the current channel is a SIP channel.

        A connection to Asterisk consist of two parts, an internal connection
        and an external connection. The internal connection is prefixed
        with 'Local/', whereas a external connection is prefixed with 'SIP/'.

        Returns:
            bool: True if this channel is a SIP channel, false otherwise.
        """
        return self.name.startswith('SIP/')

    @property
    def uniqueid(self):
        return self._id

    @property
    def name(self):
        return self._name

    @property
    def callerid(self):
        # Unconditionally(!) replace accountcode if the channel has it.
        if self.is_sip:
            if self._name[13:14] == '-' and self._name[4:13].isdigit():
                # SIP/<accountcode>-
                return self._callerid.replace(code=int(self._name[4:13]))
            else:
                return self._callerid.replace(code=0)
        return self._callerid

    @property
    def accountcode(self):
        return self._accountcode

    @property
    def is_bridged(self):
        return bool(self._bridged)

    @property
    def bridged_channel(self):
        tmp = list(self._bridged)
        if len(tmp) != 1:
            raise BridgedError(
                'Expected one bridged channel. '
                'Did Asterisk bridge multiple? '
                'Or did you forget to call is_bridged? '
                'Bridge set: {!r}'.format(self._bridged))
        return tmp[0]

    @property
    def is_up(self):
        return self._state == AST_STATE_UP

    @property
    def state(self):
        return self._state

    def set_name(self, name):
        """
        set_name changes _name of ``self`` to ``name``.

        Args:
            name (str): The name that this channel should have.
        """
        old_name = self._name
        self._name = name
        self._trace('set_name {} -> {}'.format(old_name, name))

    def set_state(self, event):
        """
        set_state changes _state of ``self`` to the ChannelState in ``event``.
        If the channel state changes, it calls meth:`_raw_a_dial`
        and/or meth:`_raw_b_dial` if needed.

        Args:
            event (dict): A dictionary containing an AMI event.

        Example event:

            <Message CallerIDName='Foo bar' CallerIDNum='+31501234567'
            Channel='SIP/voipgrid-siproute-dev-0000000c' ChannelState='4'
            ChannelStateDesc='Ring' ConnectedLineName=''
            ConnectedLineNum='' Event='Newstate' Privilege='call,all'
            Uniqueid='vgua0-dev-1442239323.24' content=''>

        Asterisk ChannelStates:

            AST_STATE_DOWN = 0,
            AST_STATE_RESERVED = 1,
            AST_STATE_OFFHOOK = 2,
            AST_STATE_DIALING = 3,
            AST_STATE_RING = 4,
            AST_STATE_RINGING 5,
            AST_STATE_UP = 6,
            AST_STATE_BUSY = 7,
            AST_STATE_DIALING_OFFHOOK = 8,
            AST_STATE_PRERING = 9
        """
        old_state = self._state
        self._state = int(event['ChannelState'])  # 4=Ring, 6=Up
        assert old_state != self._state
        self._trace('set_state {} -> {}'.format(old_state, self._state))

        if old_state == AST_STATE_DOWN and self._state in (AST_STATE_DIALING, AST_STATE_RING, AST_STATE_UP):
            self._channel_manager._raw_a_dial(self)
        elif old_state == AST_STATE_DOWN and self._state == AST_STATE_RINGING:
            self._channel_manager._raw_b_dial(self)
        elif old_state == AST_STATE_RING and self._state == AST_STATE_UP:
            self._channel_manager._raw_a_up(self)
        elif old_state == AST_STATE_RINGING and self._state == AST_STATE_UP:
            self._channel_manager._raw_b_up(self)
        else:
            self._trace('Unimplemented state update: {} -> {}'.format(old_state, self._state))

    def set_callerid(self, event):
        """
        set_callerid sets a class:`CallerId` object as attr:`_callerid`
        according to the relevant variables in `event`.

        Args:
            event (dict): A dictionary containing an AMI event.

        Example event:

            <Message CID-CallingPres='1 (Presentation Allowed, Passed
            Screen)' CallerIDName='Foo bar' CallerIDNum='+31501234567'
            Channel='SIP/voipgrid-siproute-dev-0000000c'
            Event='NewCallerid' Privilege='call,all'
            Uniqueid='vgua0-dev-1442239323.24' content=''>
        """
        old_cli = str(self._callerid)
        self._callerid = CallerId(
            code=self._callerid.code,
            name=event['CallerIDName'], number=event['CallerIDNum'],
            is_public=('Allowed' in event['CID-CallingPres']))
        self._trace('set_callerid {} -> {}'.format(old_cli, self._callerid))

    def set_accountcode(self, event):
        """
        set_accountcode sets attr:`_accountcode` to the 'Accountcode' defined
        in `event`.

        Args:
            event (dict): A dictionary containing an AMI event.

        Example event:

            <Message AccountCode='12668'
            Channel='SIP/voipgrid-siproute-dev-0000000c'
            Event='NewAccountCode' Privilege='call,all'
            Uniqueid='vgua0-dev-1442239323.24' content=''>
        """
        old_code = self._accountcode
        self._accountcode = event['AccountCode']
        self._trace('set_accountcode {} -> {}'.format(
            old_code, self._accountcode))

    def do_hangup(self, event):
        """
        do_hangup clears clears all related channel and raises an error if any
        channels were bridged.

        Args:
            event (dict): A dictionary containing an AMI event.
        """
        # Remove the bridges.
        if self._fwd_local_bridge:
            self._fwd_local_bridge._back_local_bridge = None

        if self._back_local_bridge:
            self._back_local_bridge._fwd_local_bridge = None

        # Remove the dials.
        if self.back_dial:
            self.back_dial.fwd_dials.remove(self)
            self.back_dial = None

        # Remove the queue agent connections.
        if self.back_queue_caller:
            self.back_queue_caller.fwd_queue_agents.remove(self)
            self.back_queue_caller = None

        # Assert that there are no bridged channels.
        assert not self._bridged, self._bridged

    def do_localbridge(self, other):
        """
        do_localbridge sets `self` as attr:`_back_local_bridge` on `other`
        and other as attr:`_fwd_local_bridge` on `self`.

        Args:
            other (Channel): An instance of class:`Channel`.

        Example event:

            <Message
            Channel1='Local/ID2@osvpi_route_phoneaccount-00000006;1'
            Channel2='Local/ID2@osvpi_route_phoneaccount-00000006;2'
            Context='osvpi_route_phoneaccount' Event='LocalBridge'
            Exten='ID2' LocalOptimization='Yes' Privilege='call,all'
            Uniqueid1='vgua0-dev-1442239323.25'
            Uniqueid2='vgua0-dev-1442239323.26' content=''>
        """
        assert self._fwd_local_bridge is None, self._fwd_local_bridge
        assert self._back_local_bridge is None, self._back_local_bridge
        assert other._fwd_local_bridge is None, other._fwd_local_bridge
        assert other._back_local_bridge is None, other._back_local_bridge

        self._fwd_local_bridge = other
        other._back_local_bridge = self

        self._trace('do_localbridge -> {!r}'.format(other))

    def do_masquerade(self, other):
        """
        do_masquerade removes all links from `self` and moves the links from
        `other` to `self`. The `custom` dict is also moved from `other` to
        `self`.

        Args:
            other (Channel): An instance of class:`Channel`.
        """
        # If self is linked, we must undo all of that first.
        if self._fwd_local_bridge:
            self._trace('discarding old next link {}'.format(self._fwd_local_bridge.name))
            self._fwd_local_bridge._back_local_bridge = None
            self._fwd_local_bridge = None

        if self._back_local_bridge:
            self._trace('discarding old prev link {}'.format(self._back_local_bridge.name))
            self._back_local_bridge._fwd_local_bridge = None
            self._back_local_bridge = None

        # If other is linked, move that to us.
        if other._fwd_local_bridge:
            other._fwd_local_bridge._back_local_bridge = self
            self._fwd_local_bridge = other._fwd_local_bridge
            other._fwd_local_bridge = None
            self._trace('updated next link {}'.format(self._fwd_local_bridge.name))

        if other._back_local_bridge:
            other._back_local_bridge._fwd_local_bridge = self
            self._back_local_bridge = other._back_local_bridge
            other._back_local_bridge = None
            self._trace('updated prev link {}'.format(self._back_local_bridge.name))

        # What should we do with bridges? In the Asterisk source, it looks like
        # we keep the bridges intact, i.e.: the original (self) channel gets
        # properties copied from the clone (other), while we leave the bridging
        # intact. That would mean that any bridges on the clone would be
        # destroyed later on.

        # There is one interesting feature going on here, later on, in
        # certain cases, we a get a soon to be destroyed channel that we
        # need to write info to. We link the info dict to the new class
        # so we can write to the old one.
        self.custom = other.custom

        self._trace('do_masquerade -> {!r} {!r}'.format(self, other))

    def do_link(self, other):
        """
        do_link adds `other` to the set of bridged channels in `self` and vice
        versa.

        Args:
            other (Channel): An instance of class:`Channel`.
        """
        self._bridged.add(other)
        other._bridged.add(self)

    def do_unlink(self, other):
        """
        do_link removes `other` from the set of bridged channels in `self` and
        vice versa.

        Args:
            other (Channel): An instance of class:`Channel`.
        """
        self._bridged.remove(other)
        other._bridged.remove(self)

    def get_dialing_channel(self):
        """
        Figure out on whose channel's behalf we're calling.

        When a channel is not bridged yet, you can use this on the
        B-channel to figure out which A-channel initiated the call.
        """
        if self.back_dial:
            # Check if we are being dialed.
            a_chan = self.back_dial
        elif self.back_queue_caller:
            # Check if we are being called as an agent.
            a_chan = self.back_queue_caller
        else:
            # This is the root channel.
            a_chan = None

        # If our a_chan has a local bridge, use the back part of that bridge
        # to check for further dials.
        if a_chan and a_chan._back_local_bridge:
            a_chan = a_chan._back_local_bridge

        # If we have an incoming channel, recurse through the channels to find
        # the true origin channel. If we don't have one, it means we're the
        # origin channel.
        return a_chan.get_dialing_channel() if a_chan else self

    def get_dialed_channels(self):
        """
        Figure out which channels are calling on our behalf.

        When a channel is not bridged yet, you can use this on the
        A-channel to find out which channels are dialed on behalf of
        this channel.

        It works like this:

        * A-channel (this) has a list of _fwd_dials items (open
          dials).
        * We loop over those (a list of uniqueids) and find the
          corresponding channels.
        * Those channels may be SIP channels, or they can be local
          channels, in which case we have to look further (by calling
          this function on those channels).
        """
        b_channels = set()

        if self._fwd_local_bridge:
            b_chans = self._fwd_local_bridge.fwd_dials
        else:
            b_chans = self.fwd_dials

        for b_chan in b_chans:

            # Likely, b_chan._fwd_local_bridge is None, in which case we're
            # looking at a real tech channel (non-Local).
            # Or, the b_chan has one _fwd_local_bridge, after which we have
            # to call this function again.
            if b_chan._fwd_local_bridge:
                b_chan = b_chan._fwd_local_bridge

                assert not b_chan._fwd_local_bridge, \
                    ('Since when does asterisk do double links? b_chan={!r}'.format(b_chan))

                b_channels.update(b_chan.get_dialed_channels())
            else:
                assert not b_chan.fwd_dials
                b_channels.add(b_chan)

        return b_channels


class ChannelRegistry(object):
    """
    ChannelRegistry stores the channels tracked by ChannelManager.

    ChannelRegistry exposes methods to add and remove channels and to
    retrieve them by attributes like their name and uniqueid.
    """

    def __init__(self):
        self._channels_by_name = {}
        self._channels_by_uniqueid = {}

    def add(self, channel):
        """
        Add the channel to the registry.

        Args:
            channel (Channel): The channel to register.
        """
        self._channels_by_name[channel.name] = channel
        self._channels_by_uniqueid[channel.uniqueid] = channel

    def get_by_uniqueid(self, uniqueid):
        """
        Get the channel with the given UniqueID.

        Args:
            uniqueid (string): The UniqueID to look up.

        Returns:
            Channel: The channel with the given ID.
        """
        if uniqueid in self._channels_by_uniqueid:
            return self._channels_by_uniqueid[uniqueid]
        else:
            raise MissingUniqueid(uniqueid)

    def get_by_name(self, name):
        """
        Get the channel with the given channel name.

        Args:
            name (string): The name of the channel.

        Returns:
            Channel: The channel with the given name.
        """
        if name in self._channels_by_name:
            return self._channels_by_name[name]
        else:
            raise MissingChannel(name)

    def remove(self, channel):
        """
        Remove a channel from the registry.

        Args:
            channel (Channel): The channel to remove.
        """
        if channel.name in self._channels_by_name:
            del (self._channels_by_name[channel.name])

        if channel.uniqueid in self._channels_by_uniqueid:
            del (self._channels_by_uniqueid[channel.uniqueid])

        if not self._channels_by_name:
            assert not self._channels_by_uniqueid

    def __len__(self):
        """
        Get the number of channels currently in the registry.

        Returns:
            int: The number of channels.
        """
        return len(self._channels_by_name)


class ChannelManager(object):
    """
    The ChannelManager translates AMI events to high level call events.

    Usage::

        class MyChannelManager(ChannelManager):
            def on_b_dial(self, call_id, caller, callee):
                # Your code here.
                # call_id is a unique identifying string of a conversation.
                # Caller and callee are of type CallerId.
                pass

            def on_up(self, call_id, caller, callee):
                # Your code here.
                # call_id is a unique identifying string of a conversation.
                # Caller and callee are of type CallerId.
                pass

            def on_hangup(self, call_id, caller, callee, reason):
                # Your code here.
                # call_id is a unique identifying string of a conversation.
                # Caller and callee are of type CallerId.
                # reason is a keyword to identify why a conversation ended.
                pass

            def on_transfer(self, call_id, merged_id, transferor, party1, party1):
                # Your code here. call_id and merged_id are unique strings to
                # identify two conversations being merged into one.
                # transferor, caller and callee are of type CallerId.
                pass

            def on_user_event(self, event):
                # Your code here. Process custom events. event is a dict-like
                # object.
                pass

        class MyReporter(object):
            def trace_ami(self, ami):
                print(ami)

            def trace_msg(self, msg):
                print(msg)

        manager = MyChannelManager(MyReporter())

        # events is a list of AMI-event-like dictionaries.
        for event in events:
            if ('*' in manager.INTERESTING_EVENTS or
                    event['Event'] in manager.INTERESTING_EVENTS):
                # After some of the events, one of the event hook methods
                # is called.
                manager.on_event(event)
    """
    # We require all of these events to function properly. (Except
    # perhaps the FullyBooted one.)
    INTERESTING_EVENTS = (
        # This tells us that we're connected. We should probably
        # flush our channels at this point, because they aren't up
        # to date.
        'FullyBooted',
        # These events all relate to low level channel setup and
        # maintenance.
        'Newchannel', 'Newstate', 'NewCallerid',
        'NewAccountCode', 'LocalBridge', 'Rename',
        'Bridge', 'Masquerade',
        # Higher level channel info.
        'Dial', 'Hangup', 'Transfer',
        # Events related to tracking calls through queues.
        'AgentCalled',
        # UserEvents
        'UserEvent'
    )

    def __init__(self, reporter):
        """
        Create a ChannelManager instance.

        Args:
            reporter (Reporter): A reporter with trace_msg and trace_ami
                methods.
        """
        self._reporter = reporter
        self._registry = ChannelRegistry()

    def on_event(self, event):
        """
        on_event calls `_on_event` with `event`. If `_on_event` raisen an
        exception this is logged.

        Args:
            event (dict): A dictionary containing an AMI event.
        """
        try:
            self._on_event(event)
        except MissingChannel as e:
            # If this is after a recent FullyBooted and/or start of
            # self, it is reasonable to expect that certain events will
            # fail.
            self._reporter.trace_msg(
                'Channel with name {} not in mem when processing event: '
                '{!r}'.format(e.args[0], event))
        except MissingUniqueid as e:
            # This too is reasonably expected.
            self._reporter.trace_msg(
                'Channel with Uniqueid {} not in mem when processing event: '
                '{!r}'.format(e.args[0], event))
        except BridgedError as e:
            self._reporter.trace_msg(e)

        self._reporter.on_event(event)

    def _on_event(self, event):
        """
        on_event takes an event, extract and store the appropriate state
        updates and if possible fire an event ourself.

        Args:
            event (Dict): A dictionary with Asterisk AMI data.
        """
        # Write message to reporter, for debug/test purposes.
        self._reporter.trace_ami(event)

        event_name = event['Event']

        if event_name == 'FullyBooted':
            # Time to clear our channels because they are stale?
            self._reporter.trace_msg('Connected to Asterisk')
        elif event_name == 'Newchannel':
            channel = Channel(event, channel_manager=self)
            self._registry.add(channel)
        elif event_name == 'Newstate':
            channel = self._registry.get_by_name(event['Channel'])
            channel.set_state(event)
        elif event_name == 'NewCallerid':
            channel = self._registry.get_by_name(event['Channel'])
            channel.set_callerid(event)
        elif event_name == 'NewAccountCode':
            channel = self._registry.get_by_name(event['Channel'])
            channel.set_accountcode(event)
        elif event_name == 'LocalBridge':
            channel = self._registry.get_by_name(event['Channel1'])
            other = self._registry.get_by_name(event['Channel2'])
            channel.do_localbridge(other)
        elif event_name == 'Rename':
            channel = self._registry.get_by_name(event['Channel'])
            self._registry.remove(channel)
            channel.set_name(event['Newname'])
            self._registry.add(channel)
        elif event_name in 'Bridge':
            channel1 = self._registry.get_by_name(event['Channel1'])
            channel2 = self._registry.get_by_name(event['Channel2'])

            if event['Bridgestate'] == 'Link':
                channel1.do_link(channel2)
            elif event['Bridgestate'] == 'Unlink':
                channel1.do_unlink(channel2)
            else:
                raise ValueError('Unrecognized Bridgestate: %s' % event)
        elif event_name == 'Masquerade':
            # A Masquerade destroys the Original and puts the guts of
            # Clone into it. Afterwards, the Clone channel will be
            # removed.
            clone = self._registry.get_by_name(event['Clone'])
            original = self._registry.get_by_name(event['Original'])

            if event['CloneState'] != event['OriginalState']:
                # For blonde transfers, the original state is Ring.
                assert event['OriginalState'] in ('Ring', 'Ringing')
                assert event['CloneState'] == 'Up', event

                # This is a call pickup?
                if event['OriginalState'] == 'Ringing':
                    self._raw_pickup_transfer(winner=clone, loser=original)
                elif event['OriginalState'] == 'Ring':
                    # The channel state is changed from Ring to Up, change channel state and call _raw_a_up.
                    original._state = AST_STATE_UP
                    self._raw_a_up(original)

            original.do_masquerade(clone)
        elif event_name == 'Hangup':
            channel = self._registry.get_by_name(event['Channel'])
            self._raw_hangup(channel, event)

        elif event_name == 'Dial':
            if event['SubEvent'] == 'Begin':
                source = self._registry.get_by_uniqueid(event['UniqueID'])
                target = self._registry.get_by_uniqueid(event['DestUniqueID'])

                # Verify target is not being dialed already.
                assert not target.back_dial

                # _fwd_dials is a list of channels being dialed by A.
                source.fwd_dials.append(target)

                # _back_dial is the channel dialing B.
                target.back_dial = source
            elif event['SubEvent'] == 'End':
                # This is cleaned up after Hangup.
                pass
            else:
                raise ValueError('Unrecognized Dial SubEvent: %s' % event)

        elif event_name == 'Transfer':
            # Both TargetChannel and TargetUniqueid can be used to match
            # the target channel; they can be used interchangeably.
            channel = self._registry.get_by_name(event['Channel'])
            target = self._registry.get_by_name(event['TargetChannel'])
            assert target == self._registry.get_by_uniqueid(event['TargetUniqueid'])

            if event['TransferType'] == 'Attended':
                self._raw_attended_transfer(channel, target)
            elif event['TransferType'] == 'Blind':
                self._raw_blind_transfer(channel, target)
            else:
                raise NotImplementedError(event)

        elif event_name == 'AgentCalled':
            # The Queue app does not create regular dials for calls passing
            # through it. So essentially, you've got an incoming channel,
            # a local bridge and a destination channel, but no way to tie the
            # incoming channel and local bridge together (until the incoming
            # and destination channels are bridged). This in turn makes
            # get_dialing_channel() return the back part of the local bridge
            # (before the masquarade) or just the destination channel. This
            # makes Cacofonisk call hooks with bogus data.
            #
            # The way to remedy this is by tracking the AgentCalled events,
            # which, similar to the dials, tie the incoming channel and local
            # bridge together.
            #
            # IMPORTANT: This requires the `eventwhencalled` parameter to be
            # enabled on the Queue, or these events will not be raised (and
            # you'll get bogus data).
            source = self._registry.get_by_name(event['ChannelCalling'])
            target = self._registry.get_by_name(event['DestinationChannel'])

            assert not target.back_queue_caller

            source.fwd_queue_agents.append(target)
            target.back_queue_caller = source

        elif event_name == 'UserEvent':
            self.on_user_event(event)
        else:
            pass

    # ===================================================================
    # Event handler translators
    # ===================================================================

    def _raw_a_dial(self, channel):
        """
        Handle the event where the caller phone hears the ring tone.

        We don't want this. It's work to get all the values right, and
        when we do, this would look just like on_b_dial.
        Further, the work we do to get on_transfer right makes getting
        consistent on_a_dials right even harder.

        Args:
            channel (Channel):
        """
        pass

    def _raw_b_dial(self, channel):
        """
        Handle the event where the callee phone starts to ring.

        Args:
            channel (Channel): The channel of the B side.
        """
        if channel.is_sip:
            a_chan = channel.get_dialing_channel()
            b_chan = channel
            caller = a_chan.callerid
            callee = b_chan.callerid

            if 'raw_blind_transfer' in a_chan.custom:
                # This is an interesting exception: we got a Blind
                # Transfer message earlier and recorded it in this
                # attribute. We'll translate this b_dial to first a
                # on_b_dial and then the on_transfer event.
                old_a_chan = a_chan.custom.pop('raw_blind_transfer')
                redirector = old_a_chan.callerid

                if old_a_chan.uniqueid < a_chan.uniqueid:
                    # This transfer was initiated on the A side.
                    merged_channel = old_a_chan
                    b_dial_chan = a_chan
                else:
                    # This transfer was initiated on the B side.
                    merged_channel = b_chan
                    b_dial_chan = b_chan

                # For the sake of being consistent with attentended transfers,
                # first "initiate" the call on the A/B -> C leg.
                self.on_b_dial(b_dial_chan.uniqueid, redirector, callee)

                # Send the transfer event.
                self.on_transfer(a_chan.uniqueid, merged_channel.uniqueid, redirector, caller, callee)

                if old_a_chan.uniqueid < a_chan.uniqueid:
                    self.on_hangup(merged_channel.uniqueid, redirector, caller, 'transferred')
                else:
                    self.on_hangup(merged_channel.uniqueid, redirector, callee, 'transferred')

            elif 'call_forwarding_transfer' in a_chan.custom:
                # Hey, a_chan was in a previous call which was forwarded. If
                # so, we don't want to send a regular dial notification, but
                # we want to pretend the call is being "transferred" to the
                # phone to which the calls are forwarded.
                old_b_chan = a_chan.custom.pop('call_forwarding_transfer')
                redirector = old_b_chan.callerid

                self.on_b_dial(old_b_chan.uniqueid, redirector, callee)
                self.on_transfer(a_chan.uniqueid, old_b_chan.uniqueid, redirector, caller, callee)
                self.on_hangup(old_b_chan.uniqueid, redirector, callee, 'transferred')
            else:
                self.on_b_dial(a_chan.uniqueid, caller, callee)

    def _raw_attended_transfer(self, channel, target):
        """
        Handle the attended transfer event.

        Args:
            channel (Channel): The original channel.
            target (Channel): The target channel.
        """
        redirector = target.callerid
        a_chan = channel.bridged_channel
        caller = a_chan.callerid

        if target.is_bridged:
            # The channel is bridged, things are easy.
            # (Attended transfer.)
            b_chan = target.bridged_channel
            b_chan._fired_on_b_dial = caller
            callee = b_chan.callerid
            merged_chan = channel.get_dialing_channel()

            self.on_transfer(target.uniqueid, merged_chan.uniqueid, redirector, caller, callee)

            if channel.uniqueid < a_chan.uniqueid:
                # This transfer was initiated on the A side.
                self.on_hangup(merged_chan.uniqueid, channel.callerid, caller, 'transferred')
            else:
                # This transfer was initiated on the B side.
                self.on_hangup(merged_chan.uniqueid, caller, channel.callerid, 'transferred')
        else:
            # The second channel is not bridged. Check the open dials.
            # (Blonde transfer.)

            # The oldest channel is the channel which is being merged
            # (and the lowest UniqueID is the oldest channel).
            merged_chan = channel if channel.uniqueid < a_chan.uniqueid else a_chan

            for b_chan in target.get_dialed_channels():
                callee = b_chan.callerid
                self.on_transfer(target.uniqueid, merged_chan.uniqueid, redirector, caller, callee)

                if channel.uniqueid < a_chan.uniqueid:
                    self.on_hangup(merged_chan.uniqueid, redirector, caller, 'transferred')
                else:
                    self.on_hangup(merged_chan.uniqueid, caller, redirector, 'transferred')

    def _raw_blind_transfer(self, channel, target):
        """
        Handle a blind (cold) transfer event.

        This Transfer event is earlier than the dial. We mark it and
        wait for the b_dial event. In on_b_dial we send out both the
        on_b_dial and the on_transfer.

        Args:
            channel (Channel): The channel to be transferred.
            target (Channel): The target channel.
            targetexten (str): The extension of the transfer target.
        """
        target.custom['raw_blind_transfer'] = channel

    def _raw_pickup_transfer(self, winner, loser):
        """
        Handle a call pickup event like it's a transfer.

        Args:
            winner (Channel): The channel of the phone picking up.
            loser (Channel): The channel of the phone which rang.
        """
        a_chan = loser.get_dialing_channel()
        caller = a_chan.callerid

        # The CLI of winner cannot be set properly. It has dialed in, so
        # we have no CLI. Whatever is in there is wrong. Instead, we
        # provide the destination details of loser, since that is what's
        # used to dial in.
        dest = loser.callerid
        callee = winner.callerid.replace(name=dest.name, number=dest.number, is_public=dest.is_public)

        # Call on_transfer and pretend the loser performed the transfer.
        self.on_b_dial(loser.uniqueid, dest, callee)
        self.on_transfer(a_chan.uniqueid, loser.uniqueid, dest, caller, callee)
        self.on_hangup(loser.uniqueid, dest, callee, 'transferred')

    def _raw_a_up(self, channel):
        """
        Handle a ChannelState event where A side comes up.

        Args:
            channel (Channel): The relevant A side channel.
        """
        if channel.is_sip:
            a_chan = channel
            b_chans = channel.get_dialed_channels()
            for b_chan in b_chans:
                if b_chan.is_up:
                    self.on_up(a_chan.uniqueid, a_chan.callerid, b_chan.callerid)

    def _raw_b_up(self, channel):
        """
        Handle a ChannelState event where B side comes up.

        Args:
            channel (Channel): The relevant B side channel.
        """
        if channel.is_sip:
            a_chan = channel.get_dialing_channel()
            b_chan = channel
            if a_chan.is_up:
                self.on_up(a_chan.uniqueid, a_chan.callerid, b_chan.callerid)

    def _raw_hangup(self, channel, event):
        """
        Handle a Hangup event from Asterisk.

        Args:
            channel (Channel): The channel which is hung up.
            event (Event): The data of the event.
        """
        if not channel.is_zombie:
            self._send_hangup_notifications_for_event(channel, event)

        # We've sent all relevant notifications regarding the channel
        # being gone, so we can forget it ourselves now as well.

        # Disconnect all channels linked to this channel.
        channel.do_hangup(event)

        # Remove the channel from our own list.
        self._registry.remove(channel)

        # If we don't have any channels, check whether we're completely clean.
        if not len(self._registry):
            self._reporter.trace_msg('(no channels left)')

    def _send_hangup_notifications_for_event(self, channel, event):
        """
        Check whether any hooks should be called for this hangup.

        Args:
            channel (Channel): The channel which is being hanged up.
            event (Event): A dict-like object with event data.
        """
        if channel.is_relevant:
            # This is a SIP/ channel being disconnected. Let's figure
            # out if this our A side or B side.
            if channel.back_dial or channel.back_queue_caller:
                # Channel has back dials, so channel is B.
                self._send_hangup_notifications_for_channels(channel.get_dialing_channel(), channel, event)
            elif channel.fwd_dials or channel.fwd_queue_agents:
                # Channel has forward dials, so channel is A.
                for b_chan in channel.get_dialed_channels():
                    self._send_hangup_notifications_for_channels(channel, b_chan, event)
            else:
                # This SIP/ channel does not have any dials, which means
                # it's probably one of the last ones to be removed.
                # The notifications should have been sent already.
                return

        elif channel.get_dialed_channels():
            # This is not a SIP/ channel, but it does have dials to callees.
            # Let's pull in the real a_chan and b_chan and check later if
            # they are real channels.
            a_chan = channel.get_dialing_channel()
            for b_chan in channel.get_dialed_channels():
                self._send_hangup_notifications_for_channels(a_chan, b_chan, event)
        else:
            # This is a local channel which does not have any links to any
            # callees. That probably means the channel chain is already
            # collapsing and we should have sent our notifications already.
            # We don't want to send more notifications now.
            return

    def _send_hangup_notifications_for_channels(self, a_chan, b_chan, event):
        """
        Check whether any hooks should be called for this hangup.

        Args:
            a_chan (Channel): The caller channel.
            b_chan (Channel): The callee channel.
            event (Event): A dict-like object with event data.
        """
        if 'raw_blind_transfer' in a_chan.custom or 'raw_blind_transfer' in b_chan.custom:
            # We're getting ready for a blind transfer, which means we don't
            # want to notify anyone yet.
            return

        if a_chan.is_relevant and b_chan.is_relevant:
            # Only send events when our caller and destination channels are
            # the real caller and destination.

            hangup_cause = int(event['Cause'])

            if b_chan.state < AST_STATE_RING:
                # This is a call where the b_chan has not yet come up. This
                # could happen if the callee is unavailable (e.g. the phone is
                # unplugged). For the sake of consistency, let's send a dial
                # so it looks like the phone rang before.
                self.on_b_dial(a_chan.uniqueid, a_chan.callerid, b_chan.callerid)

                if hangup_cause == AST_CAUSE_NORMAL_CLEARING:
                    # This is a special exception. Our b_chan never rang but
                    # the call was "normal"? That probably means that the call
                    # was forwarded from the phone itself. If that happens, we
                    # don't want to hang up the call yet, but send a dummy
                    # transfer when the other end comes up.
                    a_chan.custom['call_forwarding_transfer'] = b_chan
                    return

            # Map the Asterisk hangup causes to easy to understand strings.
            # See https://wiki.asterisk.org/wiki/display/AST/Hangup+Cause+Mappings
            if hangup_cause == AST_CAUSE_NORMAL_CLEARING:
                reason = 'completed'
            elif hangup_cause == AST_CAUSE_USER_BUSY:
                reason = 'busy'
            elif hangup_cause in (AST_CAUSE_NO_USER_RESPONSE, AST_CAUSE_NO_ANSWER):
                reason = 'no-answer'
            elif hangup_cause == AST_CAUSE_ANSWERED_ELSEWHERE:
                reason = 'answered-elsewhere'
            elif hangup_cause == AST_CAUSE_CALL_REJECTED:
                reason = 'rejected'
            elif hangup_cause == AST_CAUSE_UNKNOWN:
                # Sometimes Asterisk doesn't set a proper hangup cause.
                # If our a_chan is already up, this probably means the
                # call was successful. If not, that means A hanged up,
                # which we use to assign the "cancelled" status.
                if a_chan.is_up:
                    reason = 'completed'
                else:
                    reason = 'cancelled'
            else:
                reason = 'failed'

            self.on_hangup(a_chan.uniqueid, a_chan.callerid, b_chan.callerid, reason)

    # ===================================================================
    # Actual event handlers you should override
    # ===================================================================

    def on_b_dial(self, call_id, caller, callee):
        """
        Gets invoked when the B side of a call is initiated.

        In the common case, calls in Asterisk consist of two sides: A
        calls Asterisk and Asterisk calls B. This event is fired when
        Asterisk performs the second step.

        Args:
            call_id (str): Unique call ID string.
            caller (CallerId): The initiator of the call.
            callee (CallerId): The recipient of the call.
        """
        self._reporter.trace_msg('{} b_dial: {} --> {}'.format(call_id, caller, callee))

        self._reporter.on_b_dial(call_id, caller, callee)

    def on_transfer(self, call_id, merged_id, redirector, party1, party2):
        """
        Gets invoked when a call is transferred.

        In the common case, a call transfer consists of three parties
        where the redirector was speaking to party1 and party2. By
        transferring the call, he ties party1 and party2 together and
        leaves himself.

        But there are other cases, including the case where the
        redirector is the party that takes an incoming call and places
        himself on end of the bridge. In that case he is both the
        redirector and one of party1 or party2.

        Args:
            call_id (str): One of the previous call_id's which
                will be used for future calls.
            merged_id (str): One of the previous call_id's which
                will not be seen again (and can be considered closed).
                May be None if there is no call to merge (pickup transfer).
            redirector (CallerId): The initiator of the transfer.
            party1 (CallerId): One of the two parties that are tied
                together.
            party2 (CallerId): The other one.
        """
        self._reporter.trace_msg(
            '{} <== {} transfer: {} <--> {} (through {})'.format(call_id, merged_id, party1, party2, redirector)
        )

        self._reporter.on_transfer(call_id, merged_id, redirector, party1, party2)

    def on_user_event(self, event):
        """Handle custom UserEvent messages from Asterisk.

        Adding user events to a dial plan is a useful way to send additional
        information to Cacofonisk. You could add additional user info,
        parameters used for processing the events and more.

        Args:
            event (Message): Dict-like object with all attributes in the event.
        """
        self._reporter.trace_msg('user_event: {}'.format(event))
        self._reporter.on_user_event(event)

    def on_up(self, call_id, caller, callee):
        """Gets invoked when a call is connected.

        When two sides have connected and engaged in a conversation, an "up"
        event is sent. Usually, this event is sent after a b_dial event
        (i.e. a phone is picked up after ringing) but after a blind or blonde
        transfer an "up" is sent for the two *remaining* parties.

        Args:
            call_id (str): Unique call ID string.
            caller (CallerId): The initiator of the call.
            callee (CallerId): The recipient of the call.
        """
        self._reporter.trace_msg('{} up: {} --> {}'.format(call_id, caller, callee))
        self._reporter.on_up(call_id, caller, callee)

    def on_hangup(self, call_id, caller, callee, reason):
        """Gets invoked when a call is completed.

        There are two types of events which should be monitored to determine
        whether a call has ended: transfer and hangup. A hangup event is
        raised when the call is fully disconnected (no parties are connected).
        However, after a transfer has completed, the redirector of the
        transfer is also disconnected (as they quit the transfer).

        Args:
            call_id (str): Unique call ID string.
            caller (CallerId): The initiator of the call.
            callee (CallerId): The recipient of the call.
            reason (str): Why the call ended (completed, no-answer, busy,
                failed, answered-elsewhere).
        """
        self._reporter.trace_msg(
            '{} hangup: {} --> {} (reason: {})'.format(call_id, caller, callee, reason)
        )
        self._reporter.on_hangup(call_id, caller, callee, reason)


class DebugChannelManager(ChannelManager):
    """
    DebugChannel functions exactly like the default class:`ChannelManager`. The
    only difference is that this ChannelManager acts on all events, instead of
    dropping all events that are deemed 'not interesting'. This is usefull for
    creating debug logs.
    """
    INTERESTING_EVENTS = ('*',)
