#!/usr/bin/env python3

"""
    This is a low level script that communicate with a device over the network.

    Currently only a swupdate-www-push subcommand is available.

    It can be used this way:
        coreos-device swupdate-www-push ~/coreos-image-all-features-vm-x64.swu 192.168.122.21

    This script doesn't interact with bitbake so it can't retrieve variables from
    bitbake. For a more user friendly usage you can use devtool:

    devtool swupdate-www-push coreos-image-all-features 192.168.122.21
"""

import os
import sys
import pathlib
import ipaddress
import asyncio
import json

# We need some modules that are not part of the Python Standard Library
# If they are not found, let's fail with an appropriate error message
try:
    import click
    import aiohttp
except ModuleNotFoundError as e:
    print(f"Error: {e}", file=sys.stderr)
    print(f"Please install the python3-{e.name} package", file=sys.stderr)
    sys.exit(1)

@click.group()
def cli():
    pass

@cli.command()
@click.argument( 'image', type=click.Path(
    exists=True, resolve_path=True, readable=True, dir_okay=False,
    path_type=pathlib.Path,
))
@click.argument('device', type=ipaddress.ip_address)
@click.option('--port', type=click.IntRange(min=1, max=65535), default=8080)
def swupdate_www_push(image, device, port):
    loop = asyncio.new_event_loop()
    returncode = loop.run_until_complete(swupdate_www_push_async(image, device, port))
    loop.close()

    if returncode == 0:
        click.secho("coreos-device: Update was successfull", bg="green")
    else:
        click.secho(f"coreos-device: Update has failed!", bg="red")

    sys.exit(returncode)


async def swupdate_www_push_async(image, device, port):
    url = f'http://{device}:{port}'

    async with aiohttp.ClientSession() as session:
        tasks = []
        
        tasks.append(asyncio.ensure_future(swupdate_www_push_data(session, url, image)))
        tasks.append(asyncio.ensure_future(swupdate_www_websocket_logger(session, url)))

        responses = await asyncio.gather(*tasks)

        # Return the sum of return value of all ours tasks
        returncode = 0
        for response in responses:
            returncode += response
        return returncode
        

async def swupdate_www_push_data(session, url, image):

    with open(image, 'rb') as f:
        data = {'file': f}
        try:
            async with session.post(f"{url}/upload", data=data) as resp:
                if resp.status == 200:
                    click.secho(f"coreos-device: http status {resp.status}", bg="white", fg="black")
                    return 0
                else:
                    click.secho(f"coreos-device: http status {resp.status}, {await resp.text()}", bg="red")
                    return resp.status
        except Exception as e:
            click.secho(f"coreos-device: ERROR: {e}", bg="red")
            return 1        


async def swupdate_www_websocket_logger(session, url):
    return_code = 0
    try:
        async with session.ws_connect(f"{url}/ws") as ws:
            async for msg in ws:
                data = json.loads(msg.data)
                if data["type"] == "message":
                    msg = data["text"]
                    if "Waiting for requests" in msg:
                        await ws.close()
                        return 1
                    elif "ERROR" in msg or "not successful" in msg:
                        click.secho("swupdate: " + data["text"], fg='red')
                        return_code = 1
                    elif "successful" in msg:
                        click.secho("swupdate: " + data["text"], fg="green")
                    else:
                        click.secho("swupdate: " + data["text"])
                    
                if data["type"] == "status":
                    if data["status"] == "DONE":
                        await ws.close()
                        return return_code
    except Exception as e:
        click.secho(f"coreos-device: ERROR: {e}", bg="red")
        return 1 

if __name__ == '__main__':
    cli()
