import json
import os
import pathlib
import random
import re
import string
import subprocess
from typing import Optional
from typing import Union

from pycape import _config as cape_config
from pycape import function_ref as fref
from pycape import token as tkn
from pycape.cape import _synchronizer


@_synchronizer
async def deploy(
    deploy_path: Union[str, os.PathLike],
    url: Optional[str] = None,
    public: bool = False,
) -> fref.FunctionRef:
    """Deploy a directory or a zip file containing a Cape function declared in
    an app.py script.

    This method calls `cape deploy` and `cape token` from the Cape CLI to deploy
    a Cape function then returns a `~.function_ref.FunctionRef` representing
    the deployed function. This `~.function_ref.FunctionRef` will hold a function ID or
    function name, and a function checksum. Note that the ``deploy_path`` has to point
    to a directory or a zip file containing a Cape function declared in an app.py file
    and the size of its content is currently limited to 1GB.

    Args:
        deploy_path: A path pointing to a directory or a zip file containing
            a Cape function declared in an app.py script.
        url: The Cape platform's websocket URL, which is responsible for forwarding
            client requests to the proper enclave instances. If None, tries to load
            value from the ``CAPE_ENCLAVE_HOST`` environment variable. If no such
            variable value is supplied, defaults to ``"https://app.capeprivacy.com"``.
        public: Boolean determining if the function should be publicly accessible or
            not.

    Returns:
        A :class:`~.function_ref.FunctionRef` representing the deployed Cape
        function.

    Raises:
        RuntimeError: if the websocket response or the enclave attestation doc is
            malformed, if the function path is not pointing to a directory
            or a zip file, if folder size exceeds 1GB, or if the Cape CLI cannot
            be found on the device.
    """
    url = url or cape_config.ENCLAVE_HOST
    deploy_path = pathlib.Path(deploy_path)

    cmd_deploy = f"cape deploy {deploy_path} -u {url} -o json"
    if public:
        cmd_deploy += " --public"
    out_deploy, err_deploy = _call_cape_cli(cmd_deploy)
    err_deploy = err_deploy.decode()
    out_deploy = out_deploy.decode()

    err_deploy = err_deploy.split("\n")
    error_output = None

    # Parse err_deploy to get potential errors
    for msg in err_deploy:
        if "Error" in msg:
            error_output = msg
            error_msg = error_output.partition("Error:")[2]
            raise RuntimeError(f"Cape deploy error - {error_msg}")

    # Parse out_deploy to get function ID and function checksum
    out_deploy = json.loads(out_deploy.split("\n")[0])
    function_id = out_deploy.get("function_id")
    function_checksum = out_deploy.get("function_checksum")

    if function_id is None:
        raise RuntimeError(
            f"Function ID not found in 'cape.deploy' response: \n{err_deploy}"
        )

    return fref.FunctionRef(id=function_id, checksum=function_checksum)


@_synchronizer
async def token(
    name: Optional[str] = None,
    description: Optional[str] = None,
    expiry: Optional[str] = None,
) -> tkn.Token:
    """Generate a Personal Access Token.

    This method calls `cape token` from the Cape CLI to generate a Personal Access
    Token. Tokens can be created statically (long expiration and bundled with your
    application) or created dynamically (short-lived) and have an owner-specified
    expiration. This PAT is required along with the function name or function ID when
    calling a Cape function.

    Args:
        name: Optional name for the token.
        description: Optional description for the token.
        expiry: Amount of time in seconds until the function token expires. Defaults to
            1h.

    Returns:
        A :class:`~.token.Token` representing the PAT.

    Raises:
        RuntimeError: if the Cape CLI cannot be found on the device, if the CLI failed
            to generate the token, or if PyCape failed to parse the token from the CLI's
            output.
    """
    if name is None:
        token_suffix = "".join(random.choices(string.ascii_lowercase, k=8))
        name = f"pycape-deploy-{token_suffix}"

    if description is None:
        description = '"An ephemeral token generated by pycape.experimental.cli."'

    cmd_token = f"cape token create -n {name} -d {description}"

    if expiry:
        cmd_token += f" -e {expiry}"

    out_token, err_token = _call_cape_cli(cmd_token)
    err_token = err_token.decode()
    out_token = out_token.decode()

    err_lines = err_token.split("\n")
    error_output = None
    # Parse err_token to get potential errors
    for i in err_lines:
        if "Error" in i:
            error_output = i
            error_msg = error_output.partition("Error:")[2]
            raise RuntimeError(f"Cape token error - {error_msg}")

    # Parse out_token to get function token
    token_match = re.match("Success! Your token: (.*)", out_token)
    if token_match is None:
        raise RuntimeError(
            "Cape token error - could not parse token output: "
            f"\n{out_token}\n{err_lines}"
        )
    function_token = token_match.group(1)

    if function_token is None:
        raise RuntimeError(
            "Function token not found in 'cape.token' response: "
            f"\n{out_token}\n{err_lines}"
        )

    return tkn.Token(function_token)


def _check_if_cape_cli_available():
    exitcode, output = subprocess.getstatusoutput("cape")
    if exitcode != 0:
        raise RuntimeError(
            f"Please make sure Cape CLI is installed on your device: {output}"
        )


def _call_cape_cli(cape_cmd):
    _check_if_cape_cli_available()
    proc = subprocess.Popen(
        cape_cmd,
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    out, err = proc.communicate()
    return out, err
