from __future__ import annotations
import logging
import typing
from dataclasses import dataclass, field
from pathlib import Path
from typing import Text
import uuid

import bf_nlu.utils.common
import bf_nlu.utils.io

if typing.TYPE_CHECKING:
    from bf_nlu.engine.storage.storage import ModelStorage

logger = logging.getLogger(__name__)


@dataclass
class Resource:
    """Represents a persisted graph component in the graph.

    Attributes:
        name: The unique identifier for the `Resource`. Used to locate the associated
            data from a `ModelStorage`. Normally matches the name of the node which
            created it.
        output_fingerprint: An unique identifier for a specific instantiation of a
            `Resource`. Used to distinguish a specific persistence for the same
            `Resource` when saving to the cache.

    """

    name: Text
    output_fingerprint: Text = field(
        default_factory=lambda: uuid.uuid4().hex,
        # We do not use this for comparison as it is not consistent after serialization.
        compare=False,
    )

    @classmethod
    def from_cache(
        cls,
        node_name: Text,
        directory: Path,
        model_storage: ModelStorage,
        output_fingerprint: Text,
    ) -> Resource:
        """Loads a `Resource` from the cache.

        This automatically loads the persisted resource into the given `ModelStorage`.

        Args:
            node_name: The node name of the `Resource`.
            directory: The directory with the cached `Resource`.
            model_storage: The `ModelStorage` which the cached `Resource` will be added
                to so that the `Resource` is accessible for other graph nodes.
            output_fingerprint: The fingerprint of the cached `Resource`.

        Returns:
            The ready-to-use and accessible `Resource`.
        """
        logger.debug(f"Loading resource '{node_name}' from cache.")

        resource = Resource(node_name, output_fingerprint=output_fingerprint)
        if not any(directory.glob("*")):
            logger.debug(f"Cached resource for '{node_name}' was empty.")
            return resource

        try:
            with model_storage.write_to(resource) as resource_directory:
                bf_nlu.utils.common.copy_directory(directory, resource_directory)
        except ValueError:
            # This might happen during finetuning as in this case the model storage
            # is already filled
            if not bf_nlu.utils.io.are_directories_equal(directory, resource_directory):
                # We skip caching in case we see the cached output and output
                # from the model which we want to finetune are not the same
                raise

        logger.debug(f"Successfully initialized resource '{node_name}' from cache.")

        return resource

    def to_cache(self, directory: Path, model_storage: ModelStorage) -> None:
        """Persists the `Resource` to the cache.

        Args:
            directory: The directory which receives the persisted `Resource`.
            model_storage: The model storage which currently contains the persisted
                `Resource`.
        """
        try:
            with model_storage.read_from(self) as resource_directory:
                bf_nlu.utils.common.copy_directory(resource_directory, directory)
        except ValueError:
            logger.debug(
                f"Skipped caching resource '{self.name}' as no persisted "
                f"data was found."
            )

    def fingerprint(self) -> Text:
        """Provides fingerprint for `Resource`.

        A unique fingerprint is created on initialization of a `Resource` however we
        also allow a value to be provided for when we retrieve a `Resource` from the
        cache (see `Resource.from_cache`).

        Returns:
            Fingerprint for `Resource`.
        """
        return self.output_fingerprint
