import warnings
import subprocess
import sys

from .Base import Base
from .File import File
import os
from pathlib import Path
from ..common import exceptions
from ..common.retryable_download import perform_parallel_download
from typing import Literal, Optional, List, Union


class Directory(Base):
    def __init__(
        self,
        *,
        path: Union[str, Path],
        table: Optional["Table"] = None,
        query: Optional["Query"] = None,
        parent: Optional["Directory"] = None,
    ) -> None:
        if not table and not query:
            raise exceptions.ValueError(
                "All directories must either belong to a table or query."
            )

        self.path = path
        self.name = Path(path).name
        self.table = table
        self.query = query
        self.parent = parent
        self.children = {}
        self._mount_path = None
        self._last_cached_at = None

    def __repr__(self) -> str:
        return f"<Dir {str(self.path)}>"

    def list(
        self,
        max_results: Optional[int] = None,
        *,
        mode: Literal["all", "files", "directories"] = "all",
        recursive: bool = False,
    ) -> List[Union["Directory", File]]:
        if max_results is None:
            max_results = float("inf")
        children: List[Union["Directory", File]] = []
        # TODO: support glob pattern? or regex?
        for child in self.children.values():
            if len(children) >= max_results:
                break
            if recursive and isinstance(child, Directory):
                if mode == "all" or mode == "directories":
                    children.append(child)
                children.extend(
                    child.list(
                        mode=mode,
                        recursive=True,
                        max_results=max_results - len(children),
                    )
                )
            else:
                if (
                    mode == "all"
                    or (mode == "files" and isinstance(child, File))
                    or (mode == "directories" and isinstance(child, Directory))
                ):
                    children.append(child)
        return children

    def get(self, path: Union[str, Path]) -> Union["Directory", File, None]:
        is_explicit_dir = str(path).endswith("/")
        # This is to handle the case when the file was constructed via a file id, in legacy code
        if isinstance(path, str):
            # TODO: remove this after a bit
            split = path.split(".")
            client_id = split[0]
            if (
                len(split) == 2
                and len(client_id) == 14
                and len(client_id.split("-")) == 2
                and len(client_id.split("-")[0]) == 4
            ):
                warnings.warn(
                    "Passing file ids is deprecated, please use file names instead. E.g.: table.file('filename.png')",
                    FutureWarning,
                    stacklevel=2,
                )
                files = self.list(mode="files", recursive=True)
                for f in files:
                    if f.id == path:
                        return f
                return None

        # Normalize input to a Path
        if not isinstance(path, Path):
            path = Path(path)

        if path.is_absolute():
            # Need to use os.path.relpath instead of Path.relative_to to allow for paths that traverse above the current directory
            relative = Path(os.path.relpath(path, self.path))
        else:
            relative = path

        parts = list(relative.parts)
        if not parts:
            return self

        node = self
        for idx, part in enumerate(parts):
            # Handle current directory references
            if part == "." or part == "":
                continue

            # Handle parent directory traversal
            if part == "..":
                if node.parent is None:
                    # Can't traverse above the root of this tree
                    raise exceptions.ValueError(
                        "Path traverses above the root directory"
                    )
                node = node.parent
                # If '..' is the last segment, return the directory reached
                if idx == len(parts) - 1:
                    return node
                continue

            # Walk into children
            child = node.children.get(part)
            if not child:
                return None
            if isinstance(child, Directory):
                node = child
            else:
                # Must be the last part to match a file
                if idx == len(parts) - 1:
                    if is_explicit_dir:
                        return None
                    return child
                return None

        # If we finished traversing parts without returning, we resolved to a directory
        return node

    def mount(
        self, path: Optional[Union[str, Path]] = None, *, foreground: bool = False
    ) -> None:
        if (
            os.getenv("REDIVIS_NOTEBOOK_ID") is not None
            and os.getenv("REDIVIS_NOTEBOOK_ENABLE_FUSE") != "TRUE"
        ):
            raise RuntimeError(
                "Mounting directories is not supported within the default Redivis Notebooks. You must configure a custom machine to call directory.mount()"
            )
        from ..common.mount_directory import mount_directory

        if path is None:
            if self.parent is not None:
                default_name = self.name
            elif self.table:
                default_name = self.table.properties["name"]
            else:
                default_name = "redivis_mount"

            path = Path.cwd() / default_name
        elif isinstance(path, str):
            path = Path(path)

        mount_path = path.expanduser()
        mount_directory(self, mount_path, foreground=foreground)
        self._mount_path = mount_path
        return mount_path

    def unmount(self) -> None:
        if self._mount_path is None:
            raise exceptions.ValueError(
                "This directory is not currently mounted. Call mount() first."
            )

        mount_path = str(self._mount_path)

        try:
            if sys.platform == "darwin":
                subprocess.run(["umount", mount_path], check=True, capture_output=True)
            elif sys.platform == "win32":
                import ctypes

                kernel32 = ctypes.windll.kernel32
                if not kernel32.DefineDosDeviceW(0x00000002, mount_path, None):
                    raise OSError(f"Failed to unmount {mount_path}")
            else:
                subprocess.run(
                    ["fusermount", "-u", mount_path], check=True, capture_output=True
                )
        except subprocess.CalledProcessError as e:
            raise OSError(
                f"Failed to unmount {mount_path}: {e.stderr.decode().strip()}"
            )

        self._mount_path = None

        # The FUSE background thread also removes the directory on exit,
        # but attempt removal here as well in case that hasn't run yet.
        try:
            Path(mount_path).rmdir()
        except OSError:
            pass

        print(f"Unmounted directory at {mount_path}")

    def download(
        self,
        path: Optional[Union[str, Path]] = None,
        *,
        max_results: Optional[int] = None,
        max_parallelization: Optional[int] = None,
        max_concurrency: Optional[int] = None,
        overwrite: bool = False,
        progress: bool = True,
    ) -> None:
        if isinstance(path, str):
            path = Path(path)
        if path is None:
            path = Path.cwd() / self.name
        else:
            path = path.expanduser()

        if path.exists():
            if path.is_file():
                if overwrite:
                    path.unlink()
                else:
                    raise FileExistsError(
                        f"Destination path '{path}' exists and is a file; set overwrite=True to replace it."
                    )

        path.mkdir(parents=True, exist_ok=True)

        files_to_download = self.list(
            mode="files", recursive=True, max_results=max_results
        )

        uris = []
        download_paths = []
        sizes = []
        md5_hashes = []
        for f in files_to_download:
            file_relative_path = (Path("/") / f.path).relative_to(self.path)
            uris.append(f.uri)
            download_paths.append(str(path / file_relative_path))
            sizes.append(f.size)
            md5_hashes.append(f.hash)

        total_bytes = sum(s for s in sizes if s is not None) or None

        # TODO: should include query params for table / query being downloaded from
        perform_parallel_download(
            uris=uris,
            download_paths=download_paths,
            sizes=sizes,
            md5_hashes=md5_hashes,
            overwrite=overwrite,
            max_parallelization=max_parallelization,
            total_bytes=total_bytes,
            max_concurrency=max_concurrency,
            progress=progress,
        )

    def _add_file(self, file: File) -> None:
        relative_path_parts = (Path("/") / file.path).relative_to(self.path).parts

        if len(relative_path_parts) == 1:
            self.children[relative_path_parts[0]] = file
            file.directory = self
        else:
            subdir = self.children.get(str(relative_path_parts[0]))
            if not subdir:
                subdir = Directory(
                    parent=self,
                    path=self.path / relative_path_parts[0],
                    table=self.table,
                    query=self.query,
                )
                self.children[(str(relative_path_parts[0]))] = subdir
            subdir._add_file(file)
