"""A structured set of metadata representing a deployed Cape function.

A :class:`FunctionRef` is intended to capture any/all metadata related to a Cape
function. The metadata is generally user-supplied, provided to them with the output of
the Cape CLI's ``deploy`` command.

**Usage**

::

    fid = "asdf231lkg1324afdg"
    fchecksum = str(b"2l1h21jhgb2k1jh3".hex())
    fref = FunctionRef(fid, fchecksum)

    cape = Cape()
    cape.connect(fref)
"""
from __future__ import annotations

import json
import os
import pathlib
from typing import Optional
from typing import Union


class FunctionRef:
    """A reference to a Cape function.

    Args:
        id: Required string denoting the function ID of the deployed Cape function.
            Typically given with the output of the Cape CLI's ``deploy`` command.
        token: Required string containing a Cape function token generated by the Cape
            CLI during ``cape token``.
        checksum: Optional string denoting the checksum of the deployed Cape function.
            If supplied as part of a ``FunctionRef``, the :class:`~pycape.cape.Cape`
            client will verify that enclave responses includes a matching checksum
            whenever the ``FunctionRef`` is included in Cape requests.
    """

    def __init__(
        self,
        id: str,
        token: str,
        checksum: Optional[str] = None,
    ):
        id_ = id
        if not isinstance(id_, str):
            raise TypeError(f"Function id must be a string, found {type(id_)}.")
        if not isinstance(token, str):
            raise TypeError(f"Function token must be a string, found {type(token)}.")
        if checksum is not None and not isinstance(checksum, str):
            raise TypeError(
                f"Function checksum must be a string, found {type(checksum)}."
            )
        self._id = id_
        self._checksum = checksum
        self._token = token

    @property
    def id(self):
        return self._id

    @property
    def checksum(self):
        return self._checksum

    @property
    def token(self):
        return self._token

    @classmethod
    def from_json(cls, function_json: Union[str, os.PathLike]) -> FunctionRef:
        """Construct a :class:`~.function_ref.FunctionRef` from a JSON string or file.

        Args:
            function_json: a JSON string or filepath containing function ID, token, and
                optional checksum, e.g. as generated by the Cape CLI ``token`` command.

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

        Raises:
            ValueError: if the json token file doesn't exist or, the token file
                doesn't contain a `function_id` or a `function_token`.
        """
        if isinstance(function_json, pathlib.Path):
            function_config = _try_load_json_file(function_json)
            if function_config is None:
                raise ValueError(f"JSON file not found @ {str(function_json)}")

        elif isinstance(function_json, str):
            # try to treat function_json as filepath str
            json_path = pathlib.Path(function_json)
            function_config = _try_load_json_file(json_path)
            # if file not found, treat function_json as json str
            function_config = function_config or json.loads(function_json)

        else:
            raise TypeError(
                "The function_json argument expects a json string or "
                f"a path to a json file, found: {type(function_json)}."
            )

        function_id = function_config.get("function_id")
        if function_id is None:
            raise ValueError("Couldn't find a `function_id` in the token file provided")

        function_token = function_config.get("function_token")
        if function_token is None:
            raise ValueError(
                "Couldn't find a `function_token` in the token file provided"
            )

        function_checksum = function_config.get("function_checksum")

        return cls(function_id, function_token, function_checksum)

    def to_json(self, path: Optional[Union[str, os.PathLike]] = None) -> Optional[str]:
        """Write this :class:`~.function_ref.FunctionRef` to a JSON string or file.

        Args:
            path: Optional file path to write the resulting JSON to.

        Returns:
            If ``path`` is None, a string with this :class:`~.function_ref.FunctionRef`
            as a JSON struct.
        """

        fn_ref_dict = {
            "function_id": self._id,
            "function_token": self._token,
            "function_checksum": self._checksum,
        }

        if path is None:
            return json.dumps(fn_ref_dict)

        with open(path, "w") as f:
            json.dump(fn_ref_dict, f)


def _try_load_json_file(json_file: pathlib.Path):
    if json_file.exists():
        with open(json_file, "r") as f:
            json_output = json.load(f)
        return json_output
