"""
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 collections import defaultdict

from cacofonisk.constants import (AST_CAUSE_ANSWERED_ELSEWHERE, AST_CAUSE_NORMAL_CLEARING, AST_CAUSE_NO_ANSWER,
                                  AST_CAUSE_NO_USER_RESPONSE, 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']

        # 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

    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:
            if self._state in (AST_STATE_DIALING, AST_STATE_RING, AST_STATE_UP):
                self._channel_manager._raw_a_dial(self)
            elif self._state in (AST_STATE_RINGING, AST_STATE_UP):
                self._channel_manager._raw_b_dial(self)
        elif old_state == AST_STATE_RINGING and self._state == AST_STATE_UP:
            self._channel_manager._raw_b_up(self)

    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

        # 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
        # in tact. 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}'.format(self))

    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.
        
        * When a dial is started, the caller channel is set as _back_dial.
        * We look for those, while backwards over locally linked
          channels (the _back_local_bridge entries).
        """
        a_chan = self

        # We can do without recursion this time, since there will be
        # only one result.

        # Walk backwards through the dialing links to find the origin
        # channel.
        while a_chan._back_dial:
            a_chan = a_chan._back_dial

            # Check if the channel has a local bridge. If so, use that
            # instead (because the front part of the local bridge never has
            # a backlink, it has a back bridge instead).
            if a_chan._back_local_bridge:
                a_chan = a_chan._back_local_bridge

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

        return a_chan

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

        for b_chan in self._fwd_dials:

            # 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 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',
        # 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._channels_by_name = {}
        self._channels_by_uniqueid = {}

    def _get_chan_by_channame_from_evkey(self, event, event_key, pop=False):
        """
        _get_chan_by_channame_from_evkey returns the channel at `event_key` in
        `event`. If the Channel can not be found a MissingChannel error is
        raised.

        Args:
            event (dict): A dictionary containing an AMI event.
            event_key (str): The key to look up in event.
            pop (bool): Pop the item from event if True.
        """
        value = event[event_key]
        try:
            if pop:
                return self._channels_by_name.pop(value)
            return self._channels_by_name[value]
        except KeyError:
            raise MissingChannel(event_key, value)

    def _get_chan_by_uniqueid(self, uniqueid):
        try:
            return self._channels_by_uniqueid[uniqueid]
        except KeyError:
            raise MissingUniqueid(uniqueid)

    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 {}={!r} not in mem when processing event: '
                '{!r}'.format(e.args[0], e.args[1], 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._channels_by_name[channel.name] = channel
            self._channels_by_uniqueid[channel.uniqueid] = channel
        elif event_name == 'Newstate':
            channel = self._get_chan_by_channame_from_evkey(event, 'Channel')
            channel.set_state(event)
        elif event_name == 'NewCallerid':
            channel = self._get_chan_by_channame_from_evkey(event, 'Channel')
            channel.set_callerid(event)
        elif event_name == 'NewAccountCode':
            channel = self._get_chan_by_channame_from_evkey(event, 'Channel')
            channel.set_accountcode(event)
        elif event_name == 'LocalBridge':
            channel = self._get_chan_by_channame_from_evkey(event, 'Channel1')
            other = self._get_chan_by_channame_from_evkey(event, 'Channel2')
            channel.do_localbridge(other)
        elif event_name == 'Rename':
            channel = self._get_chan_by_channame_from_evkey(event, 'Channel', pop=True)
            channel.set_name(event['Newname'])
            self._channels_by_name[channel.name] = channel
        elif event_name in 'Bridge':
            channel1 = self._get_chan_by_channame_from_evkey(event, 'Channel1')
            channel2 = self._get_chan_by_channame_from_evkey(event, 'Channel2')
            if event['Bridgestate'] == 'Link':
                channel1.do_link(channel2)
            elif event['Bridgestate'] == 'Unlink':
                channel1.do_unlink(channel2)
            else:
                assert False, 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._get_chan_by_channame_from_evkey(event, 'Clone')
            original = self._get_chan_by_channame_from_evkey(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)

            original.do_masquerade(clone)
        elif event_name == 'Hangup':
            channel = self._get_chan_by_channame_from_evkey(event, 'Channel')
            self._raw_hangup(channel, event)

        elif event_name == 'Dial':
            if event['SubEvent'] == 'Begin':
                source = self._get_chan_by_uniqueid(event['UniqueID'])
                target = self._get_chan_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:
                assert False, event

        elif event_name == 'Transfer':
            # Both TargetChannel and TargetUniqueid can be used to match
            # the target channel; they can be used interchangeably.
            channel = self._get_chan_by_channame_from_evkey(event, 'Channel')
            target = self._get_chan_by_channame_from_evkey(event, 'TargetChannel')
            assert target == self._channels_by_uniqueid[event['TargetUniqueid']]
            if event['TransferType'] == 'Attended':
                self._raw_attended_transfer(channel, target)
            elif event['TransferType'] == 'Blind':
                self._raw_blind_transfer(channel, target, event['TransferExten'])
            else:
                raise NotImplementedError(event)

        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')
            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, targetexten):
        """
        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_transfer(a_chan.uniqueid, None, dest, caller, callee)

    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
            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.
        """
        a_chan = channel.get_dialing_channel()
        b_chan = channel
        hangup_cause = int(event['Cause'])

        if a_chan != b_chan:
            if channel.is_relevant and not a_chan.is_zombie:
                # When a call ends, two hangup events will be sent, one per
                # side of the call. After the first hangup event, Cacofonisk
                # will unlink the channels, after which get_dialing_channel()
                # will not return the linked channel but will return itself.
                # By checking whether both channels are not equal, we can be
                # sure we're only sending notifications for the initial
                # disconnect.
                #
                # Additionally, after a transfer the connected channel is a
                # a ZOMBIE. However, we already know the call was transferred
                # so we don't need to send an additional event.
                if hangup_cause == AST_CAUSE_NORMAL_CLEARING and not channel.is_up:
                    # Something very strange happened: a call completed
                    # "successfully" despite not having been answered.
                    # This is probably junk from call pickups, so it's safe to
                    # ignore.
                    return

                if 'raw_blind_transfer' in a_chan.custom or 'raw_blind_transfer' in b_chan.custom:
                    # This call is going to be blind transferred, which means
                    # we're going to see these channels again in a transfer.
                    # Because of that, we don't want to end these calls yet.
                    return

                if 'hangup_answered_elsewhere' in a_chan.custom:
                    # OK, we know that a_chan is a local channel and the link
                    # has been hung up before. Because these local channels
                    # don't contain anything useful, replace it with the
                    # original a_chan.
                    a_chan = a_chan.custom.pop('hangup_answered_elsewhere')

                # 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'
                else:
                    reason = 'failed'

                self.on_hangup(a_chan.uniqueid, a_chan.callerid, b_chan.callerid, reason)
            elif hangup_cause == AST_CAUSE_ANSWERED_ELSEWHERE:
                # This is frustrating. This call was answered elsewhere, so
                # Asterisk did a hangup. If we try to handle hangups the usual
                # way, Asterisk will already have disconnected the channels,
                # meaning we can't retrieve the original CallerID.
                # Additionally, because no Masquerade has been performed, we need
                # to attach the original channel to the linked channel in order to
                # be able to retrieve it later.
                assert b_chan._fwd_local_bridge is not None
                b_chan._fwd_local_bridge.custom['hangup_answered_elsewhere'] = a_chan

        # 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.
        del self._channels_by_name[channel.name]
        del self._channels_by_uniqueid[channel.uniqueid]

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

    # ===================================================================
    # 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 = ('*',)
