import asyncio
import time
from typing import Optional

from clarabot_nano_common.utility.path.tracker import PathDependencyTracker


class DocumentCommitTracker(PathDependencyTracker):
    """
    Keep rack of documents that are not yet committed, so quickly revisiting them can be detected and an explicit
    commit can be requested. This provides serializable consistency when working with Solr from a single process.
    Since we cannot know or query the soft-commit state from solr we opt to do the periodic (auto) soft commit
    ourselves. This object tracks the time to the next commit after changes are added.
    The accessor_path tracking ensures that all items are built and removed as a tree consistently. There are
    fundamentally two different kinds of changes that must be tracked. Changing (adding/removing/updating) an item
    must be detectable by upstream items if they are doing a recursive operation. Deleting an item must be detectable
    by operations on its downstream items. The changes may loosely overlap, but opposite or competing changes will
    never happen, because the whole point of tracking is to detect and avoid them by commits.
    Additionally newly added directories must be tracked so tree-building consistency can be optimized.
    TODO: The (posix) path tracking helps optimize redundant updates to files from different directories.
        - true root accessor paths (/ or c:/) could conflict-match any "posix-path" ?
    """

    __slots__ = (
        "_max_time_until_commit",
        "_commit_at",
        "_added_directories",
        "new_time",
    )

    def __init__(self, max_time_until_commit: float):
        PathDependencyTracker.__init__(self)
        assert max_time_until_commit > 0
        self._max_time_until_commit = max_time_until_commit
        self._commit_at = 0.0  # indicates empty store as well
        self._added_directories = set()
        self.new_time = asyncio.Event()

    def _add_commit_time(self, delta: float):
        """
        Manage the time of the next commit. Set the new_time event if it is being updated.
        """
        t = time.perf_counter() + delta
        if not self._commit_at or t < self._commit_at:
            self._commit_at = t
            self.new_time.set()

    def clear(self):
        """
        Clear tracked documents and scheduled commit time. Call this after a successful commit to solr.
        Does not set the new_time event even if it is "changed".
        """
        PathDependencyTracker.clear(self)
        self._added_directories.clear()
        self._commit_at = 0.0

    def get_time_until_commit(self) -> Optional[float]:
        """
        Return the time/delay until a soft commit should be issued or None.
        Also acknowledge receiving the new_time event.
        Only use this method from the task that is managing the scheduled commit.
        """
        self.new_time.clear()
        if self._commit_at:
            return self._commit_at - time.perf_counter()

    def track_upstream(self, path: str):
        raise RuntimeError()  # do not use this

    def track_downstream(self, path: str):
        raise RuntimeError()  # do not use this

    def track(self, accessor_path: str, affects_downstream: bool = False):
        """
        Add document paths of absolute/real or accessor types for tracking until commit.
        """
        self._affects_upstream.add(accessor_path)
        if affects_downstream:
            self._affects_downstream.add(accessor_path)
        self._add_commit_time(self._max_time_until_commit)

    def add_commit_time(self, commit_within: float):
        """
        Record a relative commit time to now.
        """
        assert commit_within > 0
        self._add_commit_time(commit_within)

    def release(self, path: str):
        raise RuntimeError()  # do not use this

    def affects(self, path: str):
        raise RuntimeError()  # do not use this

    def check(self, accessor_path: str) -> bool:
        """
        Check if a soft-commit is needed before working with the accessor_path. This includes checking of the
        timeouts for already queued documents.
        """
        if self._commit_at:
            if self._commit_at <= time.perf_counter():
                return True
            for _ in PathDependencyTracker.affects(self, accessor_path):
                return True
        return False

    def add_directory(self, accessor_path: str):
        """
        Only use this after a .track() has been called with the same accessor_path!
        """
        self._added_directories.add(accessor_path)

    def is_directory_added(self, accessor_path: str) -> bool:
        """
        Return true if the document by the argument has recently been created and is of directory type.
        """
        return accessor_path in self._added_directories
