import os
import shutil
import uuid

from malevich._dev.singleton import SingletonMeta


class CacheController:
    def __init__(
        self,
        user_cache_path: os.PathLike,
        catalog_path: os.PathLike
    ) -> None:
        self._user_cache_path = user_cache_path
        self._cat_path = catalog_path

    @property
    def path(self) -> str:
        return os.path.join(self._user_cache_path, self._cat_path)

    def get_entry_path(
        self,
        entry_name: str | None = None,
        entry_group: os.PathLike | None = None,
    ) -> str:
        """Constructs the path to a new cache entry

        Args:
            - entry_name: The name of cache entity (file).
                Autogenerated by default.
            - entity_group: Path-like string to denote path to
                the entity within the cache catalog
        """
        if not entry_name:
            entry_name = uuid.uuid4().hex

        if entry_group:
            return os.path.join(
                self.path,
                entry_group,
                entry_name
            )
        else:
            return os.path.join(
                self.path,
                entry_name
            )


    def probe_new_entry(
        self,
        entry_name: str | None = None,
        entry_group: str | None = None,
    ) -> tuple[bool, str]:
        """Ensures the entry under specified name and group can be created

        Args:
            - `entry_name (str, optional)`: The name of cache entity (file).
                Autogenerated by default.
            - `entity_group (str, optional)`: Path-like string to denote path to
                the entity within the cache catalog

        Returns:
            - `ok (bool)` - whether the entry is free
            - `path (str)` - a path to an entry
        """
        path_ = self.get_entry_path(
            entry_name,
            entry_group,
        )

        if os.path.exists(path_):
            return False, path_

        os.makedirs(
            os.path.dirname(path_),
            exist_ok=True
        )

        return True, path_


    def write_entry(
        self,
        data: str | bytes,
        *,
        entry_name: str | None = None,
        entry_group: os.PathLike | None = None,
        force_overwrite: bool = False,
        append: bool = False,
    ) -> bool:
        """Writes to the cache entry

        Args:
            - data (str or bytes): Text or byte data to write
            - entry_name (str, optional): Name of the entry.
                Autogenerated by default.
            - entry_group (str, optional): Path-like name of group.
            - force_overwrite (bool, optional): If true, overwrite entry if exists
        """
        ok_, path_ = self.probe_new_entry(
            entry_name,
            entry_group
        )
        if not ok_ and not force_overwrite:
            raise Exception(
                "Cache entry exists. "
                f"cat: {self._cat_path}, "
                f"entry g.: {entry_group or '!not given'}, "
                f"entry n.: {entry_name}, "
                f"entry full path: {path_}"
            )

        if isinstance(data, bytes):
            if append:
                mode = 'ab+'
            else:
                mode = 'wb+'
        elif isinstance(data, str):
            if append:
                mode = 'a+'
            else:
                mode = 'w+'
        else:
            raise TypeError(
                "expected arg. `data` to be either bytes or str, "
                f" but got {type(data)}"
            )

        with open(
            path_,
            mode=mode
        ) as file_:
            file_.write(data)

        return path_

    def cache_file(
        self,
        path: os.PathLike,
        entry_name: str | None = None,
        entry_group: os.PathLike | None = None,
    ) -> os.PathLike:
        """Caches entire file

        Args:
            - path (str): Path to the file to cache
            - entry_name (str, optional): Name of the entry.
                Autogenerated by default.
            - entry_group (str, optional): Path-like name of group.

        Returns:
            - path (str): Path to the cached file
        """
        ok_, path_ = self.probe_new_entry(
            entry_name=entry_name,
            entry_group=entry_group
        )

        if not ok_:
            raise ValueError(
                f"There exists a record for entry_name={entry_name} "
                f"and entry_group={entry_group}"
            )

        shutil.move(
            path,
            path_
        )

        return path_

    def cache_dir(
        self,
        path: os.PathLike,
        entry_name: str | None = None,
        entry_group: os.PathLike | None = None
    ) -> os.PathLike:
        """Caches entire directory

        Args:
            - path (str): Path to the directory to cache
            - entry_name (str, optional): Name of the entry.
                Autogenerated by default.
            - entry_group (str, optional): Path-like name of group.

        Returns:
            - path (str): Path to the cached directory
        """
        ok_, path_ = self.probe_new_entry(
            entry_name=entry_name,
            entry_group=entry_group
        )

        if not ok_:
            raise ValueError(
                f"There exists a record for entry_name={entry_name} "
                f"and entry_group={entry_group}"
            )

        shutil.move(
            path,
            path_
        )

        return path_

    def is_dir(
        self,
        entry_name: str | None = None,
        entry_group: os.PathLike | None = None
    ) -> bool:
        """Check whether the entry is a directory"""
        pth = self.get_entry_path(
            entry_name=entry_name,
            entry_group=entry_group
        )
        return os.path.isdir(pth)

    def is_file(
        self,
        entry_name: str | None = None,
        entry_group: str | None = None,
    ) -> bool:
        """Check whether the entry is a file"""
        pth = self.get_entry_path(
            entry_name=entry_name,
            entry_group=entry_group
        )
        return os.path.isfile(pth)


class CacheManager(metaclass=SingletonMeta):
    def __init__(self) -> None:
        self._fs = os.path.expanduser(
            os.getenv("MALEVICH_CACHE_PATH", '~/.malevich/cache')
        )

    def __getattr__(self, __name: str) -> CacheController:
        if __name not in dir(self):
            return CacheController(self._fs, __name)
        else:
            object.__getattribute__(self, __name)
