# SPDX-FileCopyrightText: 2025-2026 Global Computing Lab
#
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

from __future__ import annotations

from pathlib import Path
from typing import Any

from a4x.orchestration.site import Directory, PersistencyType, Site, StorageType
from a4x.orchestration.utils import StrCompatPathLike, StrCompatPathLikeForIsInstance


class File:
    """A class representing a file in the workflow."""

    def __init__(self, p: StrCompatPathLike, directory: Directory = None) -> None:
        """Initialize the File.

        :param p: the relative file path or file name
        :param directory: the directory where the file resides
        """
        if not isinstance(p, StrCompatPathLikeForIsInstance):
            raise TypeError("The file name must be either a string or a path-like object")
        self.path_attr = Path(p)
        self.resolved_path = None
        self.directory = None
        self.storage_type_attr = None
        self.persistency_attr = None
        self.is_resolved = False
        if directory is not None:
            self.set_directory(directory)

    def set_directory(self, directory: Directory) -> None:
        """Set or update the directory where the file exists.

        :param directory: the new directory
        """
        if not isinstance(directory, Directory):
            raise TypeError("The 'directory' argument must be of type 'a4x.orchestration.Directory'")
        old_directory = self.directory
        self.directory = directory
        # Note: this conditional for Directory._add_file and Directory._remove_file
        #       must come after setting self.directory. Otherwise, the set hashing that
        #       happens in _add_file and _remove_file will not work properly
        if old_directory is None:
            directory._add_file(self)  # noqa: SLF001
        else:
            old_directory._remove_file(self)  # noqa: SLF001
            directory._add_file(self)  # noqa: SLF001
        if self.is_resolved:
            self.is_resolved = False
            self.storage_type_attr = None
            self.persistency_attr = None
            self.resolved_path = None

    def resolve(self) -> None:
        """Determine if the file is virtual or not, and, if not, resolve the internal file path."""
        if self.directory is not None:
            self.resolved_path = self.directory.path / self.path_attr
            self.storage_type_attr = self.directory.storage_type
            self.persistency_attr = self.directory.persistency
        else:
            self.resolved_path = self.path_attr
            self.storage_type_attr = None
            self.persistency_attr = None
        self.is_resolved = True

    @property
    def storage_type(self) -> StorageType | None:
        """The storage type of the directory in which the file resides.

        This property will return None if the file is virtual or if :code:`File.resolve` has not already been called.
        """
        return self.storage_type_attr

    @property
    def persistency(self) -> PersistencyType | None:
        """The persistency of the directory in which the file resides.

        This property will return None if the file is virtual or if :code:`File.resolve` has not already been called.
        """
        return self.persistency_attr

    @property
    def site(self) -> Site | None:
        """The site (e.g., system) in which the file resides.

        This property will return None if the file is virtual or if :code:`File.resolve` has not already been called.
        """
        if self.directory is None:
            return None
        return self.directory.site

    @property
    def path(self) -> Path | None:
        """The resolved path of the file.

        This property will return None if :code:`File.resolve` has not already been called. To get the relative
        or non-resolved path, use the :code:`File.path_attr` property.
        """
        if not self.is_resolved or self.resolved_path is None:
            return None
        return self.resolved_path

    @property
    def is_virtual(self) -> bool:
        """Specifies whether the file is virtual or not."""
        return self.directory is None

    def __eq__(self, other: Any) -> bool:
        # NOTE: getattr logic is to work around partial initialization issues that can
        #       be caused by pickling and copy.deepcopy
        return (
            isinstance(other, File)
            and getattr(self, "path_attr", None) == getattr(other, "path_attr", None)
            and getattr(self, "directory", None) == getattr(other, "directory", None)
        )

    def __hash__(self) -> int:
        # NOTE: hasattr logic is to work around partial initialization issues that can
        #       be caused by pickling and copy.deepcopy
        if not hasattr(self, "path_attr") or not hasattr(self, "directory"):
            return id(self)
        return hash((self.path_attr, self.directory))

    def __repr__(self) -> str:
        # NOTE: hasattr logic is to work around partial initialization issues that can
        #       be caused by pickling and copy.deepcopy
        if not hasattr(self, "path_attr"):
            path = "UNKNOWN"
        else:
            path = str(self.path_attr)
        if not hasattr(self, "directory"):
            directory = "UNKNOWN"
        else:
            directory = repr(self.directory)
        repr_str = f"File(path={path!s}"
        if hasattr(self, "directory") and self.directory is not None:
            repr_str += f",dir={directory}"
        repr_str += ")"
        return repr_str

    def __getstate__(self) -> dict[str, Any]:
        """Get the state of the File object for the pickle protocol."""
        return self.__dict__

    def __setstate__(self, state: dict[str, Any]) -> None:
        """Set the state of the File object for the pickle protocol."""
        self.__dict__.update(state)

        if hasattr(self, "directory") and self.directory is not None:
            if not hasattr(self.directory, "files"):
                self.directory.files = set()
            self.set_directory(self.directory)
