"""
Same structure as aiohttp.web.Application
"""
import sys
import asyncio
import logging
import aiodns
from contextlib import suppress
import traceback

__all__ = ['Application']

from collections.abc import MutableMapping

from . import __version__
from .dialog import Dialog
from .dialplan import BaseDialplan
from .protocol import UDP, TCP, WS
from .peers import UDPConnector, TCPConnector, WSConnector
from .message import Response
from .contact import Contact
from .via import Via


LOG = logging.getLogger(__name__)

DEFAULTS = {
    'user_agent': 'Python/{0[0]}.{0[1]}.{0[2]} aiosip/{1}'.format(sys.version_info, __version__),
    'override_contact_host': None,
    'dialog_closing_delay': 10
}

class Request:
    def __init__(self, app, peer, msg, call_id):
        self.app = app
        self.peer = peer
        self.msg = msg
        self.call_id = call_id
        self.dialog = None

    def _create_dialog(self, dialog_factory=Dialog, **kwargs):
        if not self.dialog:
            self.dialog = self.peer._create_dialog(
                method=self.msg.method,
                from_details=Contact.from_header(self.msg.headers['To']),
                to_details=Contact.from_header(self.msg.headers['From']),
                call_id=self.call_id,
                inbound=True,
                dialog_factory=dialog_factory,
                **kwargs
            )
        return self.dialog

    async def prepare(self, status_code, *args, **kwargs):
        dialog = self._create_dialog()

        await dialog.reply(self.msg, status_code, *args, **kwargs)
        if status_code >= 300:
            await dialog.close()
            return None
        return dialog

class Application(MutableMapping):

    def __init__(self, *,
                 loop=None,
                 middleware=(),
                 defaults=None,
                 debug=False,
                 dialplan=None,
                 dns_resolver=None,
                 ):

        if loop is None:
            loop = asyncio.get_event_loop()

        if defaults:
            self.defaults = {**DEFAULTS, **defaults}
        else:
            self.defaults = DEFAULTS

        self.debug = debug
        self.dns = dns_resolver or aiodns.DNSResolver(loop=loop)
        self._finish_callbacks = []
        self._state = {}
        self._dialogs = {}
        self._connectors = {UDP: UDPConnector(self, loop=loop),
                            TCP: TCPConnector(self, loop=loop),
                            WS: WSConnector(self, loop=loop)}
        self._middleware = middleware
        self._tasks = list()

        self.dialplan = dialplan or BaseDialplan()
        self.loop = loop

    @property
    def peers(self):
        for connector in self._connectors.values():
            yield from connector._peers.values()

    @property
    def dialogs(self):
        yield from set(self._dialogs.values())

    async def connect(self, remote_addr, protocol=UDP, *, local_addr=None, **kwargs):
        connector = self._connectors[protocol]
        peer = await connector.create_peer(remote_addr, local_addr=local_addr, **kwargs)
        return peer

    async def run(self, *, local_addr=None, protocol=UDP, sock=None, **kwargs):

        if not local_addr and not sock:
            raise ValueError('One of "local_addr", "sock" is mandatory')
        elif local_addr and sock:
            raise ValueError('local_addr, sock are mutually exclusive')
        elif not local_addr:
            local_addr = None, None

        connector = self._connectors[protocol]
        server = await connector.create_server(local_addr, sock, **kwargs)
        return server

    async def _call_route(self, peer, route, msg):
        for middleware_factory in reversed(self._middleware):
            route = await middleware_factory(route)

        app = self
        call_id = msg.headers['Call-ID']
        request = Request(app, peer, msg, call_id)
        await route(request, msg)

    async def _dispatch(self, protocol, msg, addr):
        call_id = msg.headers['Call-ID']
        dialog = None

        # First incoming request of dialogs do not yet have a tag in 'to' headers
        if 'tag' in msg.to_details['params']:
            dialog = self._dialogs.get(frozenset((msg.to_details['params']['tag'],
                                                  msg.from_details['params']['tag'],
                                                  call_id)))

        # First response of dialogs have a tag in the 'to' header but the dialog is not
        # yet aware of it. Try to match only with the 'from' header tag
        if dialog is None:
            dialog = self._dialogs.get(frozenset((None, msg.from_details['params']['tag'], call_id)))

        if dialog is not None:
            await dialog.receive_message(msg)
            return

        # If we got a response without an associated message (likely a stale retransmission, drop it)
        # If we got an ACK, but nowhere to deliver it, drop it. 
        if isinstance(msg, Response) or msg.method == 'ACK':
            LOG.debug('Discarding incoming message: %s', msg)
            return

        await self._run_dialplan(protocol, msg)

    async def _run_dialplan(self, protocol, msg):
        call_id = msg.headers['Call-ID']
        via_header = msg.headers['Via']

        connector = self._connectors[type(protocol)]
        via = Via.from_header(via_header)
        via_addr = via['host'], int(via['port'])
        # get_peer handles connection part, either creates or finds existing one
        peer = await connector.get_peer(protocol, via_addr)

        async def reply(*args, **kwargs):
            dialog = peer._create_dialog(
                method=msg.method,
                from_details=Contact.from_header(msg.headers['To']),
                to_details=Contact.from_header(msg.headers['From']),
                call_id=call_id,
                inbound=True
            )

            await dialog.reply(*args, **kwargs)
            await dialog.close()

        try:
            route = await self.dialplan.resolve(
                method=msg.method,
                message=msg,
                protocol=peer.protocol,
                local_addr=peer.local_addr,
                remote_addr=peer.peer_addr
            )

            if not route or not asyncio.iscoroutinefunction(route):
                await reply(msg, status_code=501)
                return

            t = asyncio.create_task(self._call_route(peer, route, msg))
            # TODO Use a weakref here
            self._tasks.append(t)
            await t
            self._tasks.remove(t)
        except asyncio.CancelledError:
            pass
        except Exception as e:
            LOG.exception(e)
            payload = None
            if self.debug:
                with suppress(Exception):
                    payload = traceback.format_exc()
            await reply(msg, status_code=500, payload=payload)

    def _connection_lost(self, protocol):
        connector = self._connectors[type(protocol)]
        connector.connection_lost(protocol)
        # for task in self._tasks:
        #     task.cancel()

    async def finish(self):
        callbacks = self._finish_callbacks
        self._finish_callbacks = []

        for (cb, args, kwargs) in callbacks:
            try:
                res = cb(self, *args, **kwargs)
                if (asyncio.iscoroutine(res) or
                        isinstance(res, asyncio.Future)):
                    await res
            except Exception as exc:
                self.loop.call_exception_handler({
                    'message': "Error in finish callback",
                    'exception': exc,
                    'application': self,
                })

    def register_on_finish(self, func, *args, **kwargs):
        self._finish_callbacks.insert(0, (func, args, kwargs))

    async def close(self, timeout=5):
        for dialog in set(self._dialogs.values()):
            try:
                await dialog.close(timeout=timeout)
            except asyncio.TimeoutError:
                pass
        for connector in self._connectors.values():
            await connector.close()
        for task in self._tasks:
            task.cancel()

    # def __repr__(self):
    #     return "<Application>"

    # MutableMapping API
    def __eq__(self, other):
        return self is other

    def __getitem__(self, key):
        return self._state[key]

    def __setitem__(self, key, value):
        self._state[key] = value

    def __delitem__(self, key):
        del self._state[key]

    def __len__(self):
        return len(self._state)

    def __iter__(self):
        return iter(self._state)
