"""
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._exten = event['Exten']

        self._side = None

        self._callerid = CallerId(
            code=int(event['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} '
            '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):
        return self._callerid

    @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

    @property
    def exten(self):
        """
        Get the extension of the channel.

        For calling channels, the extension is the phone number which was
        dialed by the user.

        Returns:
            str: The channel extension.
        """
        return self._exten

    @property
    def is_calling_chan(self):
        """
        Check whether this channel is a calling other channels.

        Returns:
            bool: True if this channel is calling other channels, else False.
        """
        return self._side == 'A'

    @property
    def is_called_chan(self):
        """
        Check whether this channel is being called by other channels.

        Returns:
            bool: True if this channel is being called, False otherwise.
        """
        return self._side == 'B'

    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)
        if event['CallerIDNum'] == str(self._callerid.code):
            # If someone uses call pickups, the CallerIDNum will be the
            # same as the AccountCode. However, broadcasting that is a bit
            # of a security leak.
            # Instead, we ignore this new number and use whatever we already
            # have.
            caller_id_number = self._callerid.number
        else:
            caller_id_number = event['CallerIDNum']

        self._callerid = self._callerid.replace(
            name=event['CallerIDName'],
            number=caller_id_number,
            is_public=('Allowed' in event['CID-CallingPres']))

        self._trace('set_callerid {} -> {}'.format(old_cli, self._callerid))

    def set_accountcode(self, event):
        """
        set_accountcode sets the code of the _callerid 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=''>
        """
        if not self._callerid.code:
            old_accountcode = self._callerid.code
            self._callerid = self._callerid.replace(code=int(event['AccountCode']))
            self._trace('set_accountcode {} -> {}'.format(old_accountcode, self._callerid.code))
        else:
            self._trace('set_accountcode ignored {} -> {}'.format(self._callerid.code, event['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
        # 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._callerid = other.callerid

        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
        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_warm_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, party1 and party2 are of type CallerId.
                pass

            def on_cold_transfer(self, call_id, merged_id, transferor, party1, targets):
                # Your code here. call_id and merged_id are unique strings to
                # identify two conversations being merged into one.
                # transferor and caller are of type CallerId. targets is a
                # list of CallerID objects.
                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_call_pickup(clone, original)
                    original._state = AST_STATE_UP
                    self._raw_b_up(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, event['TransferExten'])
            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_dial

            source.fwd_dials.append(target)
            target.back_dial = 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):
        """
        channel._side = 'A'
        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.
        """
        channel._side = 'B'

        if channel.is_sip:
            if 'ignore_b_dial' in channel.custom:
                # Notifications were already sent for this channel.
                # Unset the flag and move on.
                del (channel.custom['ignore_b_dial'])
                return

            a_chan = channel.get_dialing_channel()
            a_chan._side = 'A'

            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.
                redirector_chan = a_chan.custom.pop('raw_blind_transfer')

                redirector = redirector_chan.callerid
                target_chans = a_chan.get_dialed_channels()
                targets = [party.callerid for party in target_chans]

                for target in target_chans:
                    # To prevent notifications from being sent multiple times,
                    # we set a flag on all other channels except for the one
                    # starting to ring right now.
                    if target != channel:
                        target.custom['ignore_b_dial'] = True

                # The dial from the transferree was setup by the transfer app,
                # so it contains garbage codes like ID12345 as the extension
                # rather than a dialed number.
                a_chan._exten = channel.callerid.number

                # We're going to want to simulate a pre-flight dial event for
                # consistency with attended transfers. In this dial, the
                # redirector supposedly calls the person to who the call is
                # going to be redirected to.
                #
                # It's important that a b_dial has been sent for the call
                # we're going to be left with afterwards, but also that the
                # call ID is different from the call before the transfer.
                if redirector_chan.is_calling_chan:
                    # This transfer was initiated on the A side, which means
                    # we're going to be left with B -> C afterwards. No dial
                    # event was triggered with B as caller, so we should do
                    # that now.
                    self.on_b_dial(a_chan.uniqueid, redirector, a_chan.exten, targets)
                else:
                    # This transfer was initiated on the B side, which means
                    # we're going to be left with A -> C afterwards. A dial
                    # event with A was already generated, so we could (ab)use
                    # any old channel here to simulate a merged call.
                    # So why specifically use redirector_chan? Just read on...
                    self.on_b_dial(redirector_chan.uniqueid, redirector, redirector_chan.exten, targets)

                # Now it's time to send a transfer event. dialing_channel is
                # always the channel we're going to be left with (regardless
                # of whether it was originally A or B), so that's our new
                # call_id.
                #
                # See, redirector_chan is always the call we'll want to merge.
                # if the call was initiated on the A side, redirector_chan is
                # the original call which we will end. If the transfer was
                # initiated on the B side, then it's our dummy channel.
                self.on_cold_transfer(a_chan.uniqueid, redirector_chan.uniqueid,
                                      redirector, a_chan.callerid, redirector_chan.exten, targets)
            else:
                # We'll want to send one ringing event for all targets. So
                # let's figure out to whom a_chan has open dials. To ensure
                # only one event is raised, we'll check all the uniqueids and
                # only send an event for the channel with the lowest uniqueid.
                # if not a_chan.is_up:
                open_dials = a_chan.get_dialed_channels()
                targets = [dial.callerid for dial in open_dials]

                for b_chan in open_dials:
                    if b_chan == channel:
                        # Ensure a notification is only sent once.
                        self.on_b_dial(a_chan.uniqueid, a_chan.callerid, a_chan.exten, targets)
                    else:
                        # To prevent notifications from being sent multiple times,
                        # we set a flag on all other channels except for the one
                        # starting to ring right now.
                        b_chan.custom['ignore_b_dial'] = True

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

        Args:
            channel (Channel): The original channel is the channel which the
                redirector used to talk with the person who's being
                transferred.
            target (Channel): The target channel is the channel which the
                redirector used to set up the call to the person to whom the
                call is being transferred.
        """
        if target.is_bridged:
            # The redirector already has an audio bridge open with the new
            # callee. This means that we're handling an attended transfer.

            transferred_channel = channel.bridged_channel
            c_chan = target.bridged_channel

            if channel.is_called_chan:
                # Channel has a back dial, meaning it was B who started the
                # transfer. That means channel is bridged with A.
                old_a_chan = channel.bridged_channel
            else:
                # Channel doesn't have a back dialing, meaning it was A who
                # started the transfer. That means channel is bridged with B.
                old_a_chan = channel

                # Mark the channel as being transferred so we don't send
                # hangup notifications for it.
                channel.custom['ignore_a_hangup'] = True

            self.on_warm_transfer(target.uniqueid, old_a_chan.uniqueid,
                                  target.callerid, transferred_channel.callerid, c_chan.callerid)
        else:
            # The redirector doesn't have an audio bridge with the new callee.
            # This means the redirector started the transfer before talking to
            # the redirection target, which is a blonde transfer.

            if channel.is_called_chan:
                # The transferrer was the B side.
                old_a_chan = channel.get_dialing_channel()
                new_caller = old_a_chan
            elif channel.is_calling_chan:
                # The transferrer was the A side.
                old_a_chan = channel

                # Because the channel is not bridged, figuring out who the
                # redirector first talked to is a bit more complicated.
                # Fortunately, there should only be one open dial left.
                dialed_channels = channel.get_dialed_channels()
                assert len(dialed_channels) == 1
                new_caller = list(dialed_channels)[0]

                # Mark the channel as ignored so we don't send another
                # hangup notification after the transfer.
                channel.custom['ignore_a_hangup'] = True
            else:
                # This channel doesn't have sides. Probably garbage data.
                return

            targets = [c_chan.callerid for c_chan in target.get_dialed_channels()]
            self.on_cold_transfer(target.uniqueid, old_a_chan.uniqueid,
                                  target.callerid, new_caller.callerid, target.exten, targets)

    def _raw_blind_transfer(self, channel, target, transfer_exten):
        """
        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.
            transfer_exten (str): The phone number being transferred to.
        """
        if channel.is_called_chan:
            target.custom['raw_blind_transfer'] = channel
        else:
            target.custom['raw_blind_transfer'] = channel

        # Mark the original channel as ignored, so we don't report a hangup
        # just after the transfer.
        channel.custom['ignore_a_hangup'] = True
        channel._exten = transfer_exten

    def _raw_call_pickup(self, winner, loser):
        """
        Handle a call pickup event.

        Args:
            winner (Channel): The channel of the phone picking up.
            loser (Channel): The channel of the phone which rang.
        """
        # 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.
        winner._callerid = winner.callerid.replace(
            name=loser.callerid.name,
            number=loser.callerid.number,
            is_public=loser.callerid.is_public
        )

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

        Args:
            channel (Channel): The relevant A side channel.
        """
        channel._side = 'A'

        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, a_chan.exten, 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.
        """
        channel._side = 'B'

        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, a_chan.exten, 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 channel.is_relevant:
            if 'raw_blind_transfer' in channel.custom:
                # Panic! This channel had a blind transfer coming up but it's
                # being hung up! That probably means the blind transfer target
                # could not be reached.
                # Ideally, we would simulate a full blind transfer having been
                # completed but hanged up with an error. However, no channel
                # to the third party has been created.
                redirector = channel.custom.pop('raw_blind_transfer')

                if redirector.is_calling_chan:
                    a_chan = redirector
                    b_chan = channel
                else:
                    a_chan = channel
                    b_chan = redirector

                # TODO: Maybe give another status code than 'completed' here?
                self.on_a_hangup(a_chan.uniqueid, a_chan.callerid, b_chan.callerid.number, 'completed')

            elif 'ignore_a_hangup' in channel.custom:
                # This is a calling channel which performed an attended
                # transfer. Because the call has already been "hanged up"
                # with the transfer, we shouldn't send a hangup notification.
                pass

            elif channel.is_calling_chan:
                # The caller is being disconnected, so we should notify the
                # user.

                hangup_cause = int(event['Cause'])

                # 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:
                    # If channel is not up, the call never really connected.
                    # This happens when call confirmation is unsuccessful.
                    if channel.is_up:
                        reason = 'completed'
                    else:
                        reason = 'no-answer'
                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 assign the "cancelled" status.
                    if channel.is_up:
                        reason = 'completed'
                    else:
                        reason = 'cancelled'
                else:
                    reason = 'failed'

                self.on_a_hangup(channel.uniqueid, channel.callerid, channel.exten, reason)

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

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

    def on_b_dial(self, call_id, caller, to_number, targets):
        """
        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.
            to_number (str): The number which was dialed by the user.
            targets (list): A list of recipients of the call.
        """
        self._reporter.trace_msg('{} ringing: {} --> {} ({})'.format(call_id, caller, to_number, targets))
        self._reporter.on_b_dial(call_id, caller, to_number, targets)

    def on_warm_transfer(self, call_id, merged_id, redirector, caller, destination):
        """
        Gets invoked when an attended transfer is completed.

        In an attended transfer, one of the participants of a conversation
        calls a third participant, waits for the third party to answer, talks
        to the third party and then transfers their original conversation
        partner to the third party.

        Args:
            call_id (str): The unique ID of the resulting call.
            merged_id (str): The unique ID of the call which will end.
            redirector (CallerId): The caller ID of the party performing the
                transfer.
            caller (CallerId): The caller ID of the party which has been
                transferred.
            destination (CallerId): The caller ID of the party which received the
                transfer.
        """
        self._reporter.trace_msg(
            '{} <== {} attn xfer: {} <--> {} (through {})'.format(call_id, merged_id, caller, destination, redirector),
        )
        self._reporter.on_warm_transfer(call_id, merged_id, redirector, caller, destination)

    def on_cold_transfer(self, call_id, merged_id, redirector, caller, to_number, targets):
        """
        Gets invoked when a blind or blonde transfer is completed.

        In a blind transfer, one of the call participant transfers their
        conversation partner to a third party. However, unlike with an
        attended transfer, the redirector doesn't wait for the other end to
        pick up, but just punches in the number and sends their conversation
        party away. Because of this, multiple phones may actually be addressed
        by this transfer, hence the multiple targets. The real participant can
        be recovered later on when someone answers the transferred call.

        A blonde is a middle road between blind transfers and attended
        transfers. With a blond transfer, the redirector requests an attended
        transfer but doesn't wait for the receiving end to pick up. Since the
        data of blind and blonde transfers looks identical, they don't have
        special hooks.

        Args:
            call_id (str): The unique ID of the resulting call.
            merged_id (str): The unique ID of the call which will end.
            redirector (CallerId): The caller ID of the party performing the
                transfer.
            caller (CallerId): The caller ID of the party which has been
                transferred.
            to_number (str): The number which was dialed by the user.
            targets (list): A list of CallerId objects whose phones are
                ringing for this transfer.
        """
        self._reporter.trace_msg(
            '{} <== {} bld xfer: {} <--> {} (through {})'.format(call_id, merged_id, caller, targets, redirector),
        )
        self._reporter.on_cold_transfer(call_id, merged_id, redirector, caller, to_number, targets)

    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, to_number, 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.
            to_number (str): The number which was dialed by the user.
            callee (CallerId): The recipient of the call.
        """
        self._reporter.trace_msg('{} up: {} --> {} ({})'.format(call_id, caller, to_number, callee))
        self._reporter.on_up(call_id, caller, to_number, callee)

    def on_a_hangup(self, call_id, caller, to_number, 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.
            to_number (str): The number which was dialed by the user.
            reason (str): Why the call ended (completed, no-answer, busy,
                failed, answered-elsewhere).
        """
        self._reporter.trace_msg(
            '{} hangup: {} --> {} (reason: {})'.format(call_id, caller, to_number, reason)
        )
        self._reporter.on_hangup(call_id, caller, to_number, 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 = ('*',)
