# asandbox.py
#
# Authors:
#   - Coumes Quentin <coumes.quentin@gmail.com>


"""An asynchronous implementation of the Sandbox API."""

import io
import json
import os
from contextlib import AbstractAsyncContextManager
from typing import BinaryIO, Optional, Union

import aiohttp

from .exceptions import status_exceptions
from .utils import ENDPOINTS


class ASandbox(AbstractAsyncContextManager):
    """Interface a Sandbox server asynchronously."""
    
    
    def __init__(self, url: str, total: Optional[float] = 60, connect: Optional[float] = None,
                 sock_connect: Optional[float] = None, sock_read: Optional[float] = None):
        """Initialize a sandbox with the given URL.
        
        Default timeout for the whole operation is one minute, use the following
        argument to override :
            
            * total : The whole operation time including connection
                    establishment, request sending and response reading.
            * connect : The time consists connection establishment for a new
                    connection or waiting for a free connection from a pool if
                    pool connection limits are exceeded.
            * sock_connect : A timeout for connecting to a peer for a new
                    connection, not given from a pool.
            * sock_read : The maximum allowed timeout for period between reading
                    a new data portion from a peer.
        """
        self.url = url
        self.session = aiohttp.ClientSession(
            timeout=aiohttp.ClientTimeout(total, connect, sock_connect, sock_read)
        )
    
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()
    
    
    async def close(self):
        """Close the aiohttp ClientSession."""
        await self.session.close()
    
    
    async def _build_url(self, endpoint: str, *args: str):
        """Build the url corresponding to <endpoint> with the given <args>."""
        return os.path.join(self.url, ENDPOINTS[endpoint] % tuple(args))
    
    
    async def libraries(self) -> dict:
        """Asynchronously retrieve libraries installed in the containers of the
        sandbox."""
        async with self.session.get(await self._build_url("libraries")) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return await response.json()
    
    
    async def specifications(self) -> dict:
        """Asynchronously retrieve specifications of the sandbox."""
        async with self.session.get(await self._build_url("specifications")) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return await response.json()
    
    
    async def usage(self) -> dict:
        """Asynchronously retrieve current usage stats of the sandbox."""
        async with self.session.get(await self._build_url("usages")) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return await response.json()
    
    
    async def download(self, uuid: str, path: str = None) -> BinaryIO:
        """Asynchronously download an environment or a specific file inside an
        environment."""
        if path is None:
            url = await self._build_url("environments", uuid)
        else:
            url = await self._build_url("files", uuid, path)
        
        async with self.session.get(url) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return io.BytesIO(await response.read())
    
    
    async def check(self, uuid: str, path: str = None) -> int:
        """Asynchronously check if an environment or a specific file inside an
        environment exists."""
        if path is None:
            url = await self._build_url("environments", uuid)
        else:
            url = await self._build_url("files", uuid, path)
        
        async with self.session.head(url) as response:
            if response.status not in [200, 404]:  # pragma: no cover
                raise status_exceptions(response)

            return 0 if response.status == 404 else response.headers["Content-Length"]
    
    
    async def execute(self, config: Union[dict], environ: Optional[BinaryIO] = None) -> dict:
        """Asynchronously execute commands on the sandbox according to <config>
        and <environ>, returning the response's json as a dict.
        
        <environ>, if not None, will be consumed and closed and shall not be
        used further."""
        data = aiohttp.FormData()
        data.add_field("config", json.dumps(config))
        if environ is not None:
            data.add_field("environment", environ)
        
        async with self.session.post(await self._build_url("execute"), data=data) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return await response.json()


    async def load(self, environ: dict) -> dict:
        """Asynchronously execute commands on the sandbox according to <config>
        and <environ>, returning the response's json as a dict.
        
        <environ>, if not None, will be consumed and closed and shall not be
        used further."""
        data = aiohttp.FormData()
        data.add_field("data", json.dumps(environ))
     
        async with self.session.post(await self._build_url("load/fr"), data=data) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return await response.json()


    async def demo(self, environ: dict) -> dict:
        """Asynchronously execute commands on the sandbox according to <config>
        and <environ>, returning the response's json as a dict.
        
        <environ>, if not None, will be consumed and closed and shall not be
        used further."""
        data = aiohttp.FormData()
        data.add_field("data", json.dumps(environ))
        data.add_field("demo", True)
        
        async with self.session.post(await self._build_url("demo"), data=data) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return await response.json()


    async def playexo(self, config: dict, environ: dict) -> dict:
        """Asynchronously execute commands on the sandbox according to <config>
        and <environ>, returning the response's json as a dict.
        
        <environ>, if not None, will be consumed and closed and shall not be
        used further."""
        data = aiohttp.FormData()
        data.add_field("data", json.dumps(environ))
        data.add_field("config", json.dumps(config))
        
        async with self.session.post(await self._build_url("exo"), data=data) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return await response.json()

    async def exec(self, datas: dict = {}) -> dict:
        """Asynchronously execute commands on the sandbox according to <config>
        and <environ>, returning the response's json as a dict.
        
        <environ>, if not None, will be consumed and closed and shall not be
        used further."""
        data = aiohttp.FormData()
        data.add_field("data", json.dumps(datas))
        for key, value in datas.items():
            data.add_field(str(key), value)
        
        async with self.session.post(await self._build_url("exec"), data=data) as response:
            if response.status != 200:
                raise status_exceptions(response)
            
            return await response.json()

[((126, 15, 126, 33), 'aiohttp.FormData', 'aiohttp.FormData', ({}, {}), '()', False, 'import aiohttp\n'), ((144, 15, 144, 33), 'aiohttp.FormData', 'aiohttp.FormData', ({}, {}), '()', False, 'import aiohttp\n'), ((160, 15, 160, 33), 'aiohttp.FormData', 'aiohttp.FormData', ({}, {}), '()', False, 'import aiohttp\n'), ((177, 15, 177, 33), 'aiohttp.FormData', 'aiohttp.FormData', ({}, {}), '()', False, 'import aiohttp\n'), ((193, 15, 193, 33), 'aiohttp.FormData', 'aiohttp.FormData', ({}, {}), '()', False, 'import aiohttp\n'), ((127, 33, 127, 51), 'json.dumps', 'json.dumps', ({(127, 44, 127, 50): 'config'}, {}), '(config)', False, 'import json\n'), ((145, 31, 145, 50), 'json.dumps', 'json.dumps', ({(145, 42, 145, 49): 'environ'}, {}), '(environ)', False, 'import json\n'), ((161, 31, 161, 50), 'json.dumps', 'json.dumps', ({(161, 42, 161, 49): 'environ'}, {}), '(environ)', False, 'import json\n'), ((178, 31, 178, 50), 'json.dumps', 'json.dumps', ({(178, 42, 178, 49): 'environ'}, {}), '(environ)', False, 'import json\n'), ((179, 33, 179, 51), 'json.dumps', 'json.dumps', ({(179, 44, 179, 50): 'config'}, {}), '(config)', False, 'import json\n'), ((194, 31, 194, 48), 'json.dumps', 'json.dumps', ({(194, 42, 194, 47): 'datas'}, {}), '(datas)', False, 'import json\n'), ((44, 20, 44, 82), 'aiohttp.ClientTimeout', 'aiohttp.ClientTimeout', ({(44, 42, 44, 47): 'total', (44, 49, 44, 56): 'connect', (44, 58, 44, 70): 'sock_connect', (44, 72, 44, 81): 'sock_read'}, {}), '(total, connect, sock_connect, sock_read)', False, 'import aiohttp\n')]