# -*- coding: utf-8 -*-
"""CLI UPnP client module."""

import argparse
import asyncio
import json
import logging
import sys
import time

import aiohttp
import aiohttp.web

from async_upnp_client import UpnpDevice
from async_upnp_client import UpnpFactory
from async_upnp_client import UpnpEventHandler
from async_upnp_client import UpnpRequester
from async_upnp_client import UpnpService
from async_upnp_client.dlna import dlna_handle_notify_last_change


logging.basicConfig()
_LOGGER = logging.getLogger('upnp_client')
_LOGGER.setLevel(logging.ERROR)
_LOGGER_TRAFFIC = logging.getLogger('async_upnp_client.traffic')
_LOGGER_TRAFFIC.setLevel(logging.ERROR)

DEFAULT_PORT = 11302


parser = argparse.ArgumentParser(description='upnp_client')
parser.add_argument('--device', required=True, help='URL to device description XML')
parser.add_argument('--debug', action='store_true', help='Show debug messages')
parser.add_argument('--debug-traffic', action='store_true', help='Show network traffic')
parser.add_argument('--pprint', action='store_true', help='Pretty-print (indent) JSON output')

subparsers = parser.add_subparsers(title='Subcommands')
subparser = subparsers.add_parser('call-action', help='Call an action')
subparser.add_argument('call-action', nargs='+', help='service/action param1=val1 param2=val2')
subparser = subparsers.add_parser('subscribe', help='Subscribe to services')
subparser.add_argument('service', nargs='+', help='service type or part or abbreviation')
subparser.add_argument('--bind', required=True, help='ip[:port], e.g., 192.168.0.10:8090')

args = parser.parse_args()
pprint_indent = 4 if args.pprint else None

event_handler = None


def bind_host_port():
    """Determine listening host/port."""
    bind = args.bind
    if ':' not in bind:
        bind = bind + ':' + str(DEFAULT_PORT)
    return bind.split(':')


def service_from_device(device: UpnpDevice, service_name: str) -> UpnpService:
    """Get UpnpService from UpnpDevice by name or part or abbreviation."""
    for service in device.services.values():
        part = service.service_id.split(':')[-1]
        abbr = ''.join([c for c in part if c.isupper()])
        if service_name == service.service_type or service_name == part or service_name == abbr:
            return service


def on_event(service, service_variables):
    """"""
    _LOGGER.debug('State variable change for %s, variables: %s',
                  service,
                  ','.join([sv.name for sv in service_variables]))
    obj = {
        'timestamp': time.time(),
        'service_id': service.service_id,
        'service_type': service.service_type,
        'state_variables': {sv.name: sv.value for sv in service_variables},
    }
    print(json.dumps(obj, indent=pprint_indent))

    # do some additional handling for DLNA LastChange state variable
    if len(service_variables) == 1 and \
       service_variables[0].name == 'LastChange':
        last_change = service_variables[0]
        dlna_handle_notify_last_change(last_change)


class AioHttpRequester(UpnpRequester):
    """Aiohttp requester."""

    async def async_do_http_request(self, method: str, url: str, headers=None, body=None, body_type='text'):
        """Do a HTTP request."""
        async with aiohttp.ClientSession() as session:
            response = await session.request(method, url, headers=headers, data=body)
            if body_type == 'text':
                response_body = await response.text()
            elif body_type == 'raw':
                response_body = await response.read()
            elif body_type == 'ignore':
                response_body = None

        return response.status, response.headers, response_body


async def handle_notify_request(request):
    global event_handler

    _LOGGER.debug('Received NOTIFY: %s', request)
    headers = request.headers
    body = await request.text()

    # ensure NOTIFY
    if request.method != 'NOTIFY':
        _LOGGER.debug('Not notify')
        return aiohttp.web.Response(status=405)

    status = await event_handler.handle_notify(headers, body)
    _LOGGER.debug('NOTIFY response status: %s', status)
    return aiohttp.web.Response(status=status)


async def call_action(device: UpnpDevice, call_action_args):
    """Call an action and show results."""
    service_name, action_name = call_action_args[0].split('/')
    args = {a.split('=', 1)[0]: a.split('=', 1)[1] for a in call_action_args[1:]}

    service = service_from_device(device, service_name)
    if not service:
        print('Unknown service: %s' % (service_name, ))
        sys.exit(1)
    action = service.action(action_name)
    if not action:
        print('Unknown action: %s' % (action_name, ))
        sys.exit(1)

    coerced_args = {}
    for key, value in args.items():
        in_arg = action.argument(key)
        if not in_arg:
            print('Unknown argument: %s, known arguments: %s' % (
                  key,
                  ','.join([a.name for a in action.in_arguments()])))
            sys.exit(1)
        coerced_args[key] = in_arg.coerce_python(value)

    _LOGGER.debug('Calling %s.%s, parameters:\n%s',
                  service.service_id, action.name,
                  '\n'.join(['%s:%s' % (key, value) for key, value in coerced_args.items()]))
    result = await action.async_call(**coerced_args)

    _LOGGER.debug('Results:\n%s',
                  '\n'.join(['%s:%s' % (key, value) for key, value in coerced_args.items()]))

    obj = {
        'timestamp': time.time(),
        'service_id': service.service_id,
        'service_type': service.service_type,
        'action': action.name,
        'in_parameters': coerced_args,
        'out_parameters': result,
    }
    print(json.dumps(obj, indent=pprint_indent))


async def subscribe(device: UpnpDevice, subscribe_args):
    """Subscribe to service(s) and output updates."""
    global event_handler

    server = aiohttp.web.Server(handle_notify_request)
    host, port = bind_host_port()
    callback_url = 'http://%s:%s/' % (host, port)
    _LOGGER.debug('Listening on: %s', callback_url)
    event_handler = UpnpEventHandler(callback_url, device.requester)
    await loop.create_server(server, host, port)

    # gather all services
    services = []
    for service_name in subscribe_args:
        if service_name == '*':
            services += device.services
            continue

        service = service_from_device(device, service_name)
        service.on_event = on_event
        services.append(service)

    for service in services:
        await event_handler.async_subscribe(service)

    # keep the webservice running
    while True:
        await asyncio.sleep(120)
        await event_handler.async_resubscribe_all()


async def async_main():
    """Main."""
    if args.debug:
        _LOGGER.setLevel(logging.DEBUG)
    if args.debug_traffic:
        _LOGGER_TRAFFIC.setLevel(logging.DEBUG)

    requester = AioHttpRequester()
    factory = UpnpFactory(requester)
    device = await factory.async_create_device(args.device)

    if hasattr(args, 'call-action'):
        await call_action(device, getattr(args, 'call-action'))
    elif hasattr(args, 'service'):
        await subscribe(device, args.service)
    else:
        parser.print_usage()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()

    try:
        loop.run_until_complete(async_main())
    except KeyboardInterrupt:
        loop.run_until_complete(event_handler.async_unsubscribe_all())
    finally:
        loop.close()
