"""
FakeHDFS — maps hdfs:// paths to a local directory tree.

Path resolution rules:
  hdfs://namenode/path/to/file  →  {root}/path/to/file
  hdfs:///path/to/file          →  {root}/path/to/file
  /absolute/local/path          →  /absolute/local/path  (pass-through)
  relative/path                 →  CWD/relative/path     (pass-through)

Configure root via:
  - SINGLESPARK_HDFS_ROOT environment variable
  - spark.config("spark.singlespark.hdfs.root", "/my/path")
  - FakeHDFS(root="/my/path")

Default: ~/.singlespark/hdfs/
"""
from __future__ import annotations

import glob as _glob
import os
import re
import shutil
from pathlib import Path


class FakeHDFS:
    ROOT_ENV = "SINGLESPARK_HDFS_ROOT"
    DEFAULT_ROOT = "~/.singlespark/hdfs"

    def __init__(self, root: str | None = None):
        env_root = os.environ.get(self.ROOT_ENV)
        chosen = env_root or root or self.DEFAULT_ROOT
        self.root = Path(chosen).expanduser().resolve()
        self.root.mkdir(parents=True, exist_ok=True)

    # ------------------------------------------------------------------
    # Path resolution
    # ------------------------------------------------------------------

    def resolve(self, path: str | Path) -> Path:
        """
        Resolve an HDFS or local path to a local filesystem Path.

        Examples
        --------
        >>> hdfs.resolve("hdfs://namenode/user/data")
        PosixPath('/home/user/.singlespark/hdfs/user/data')
        >>> hdfs.resolve("hdfs:///user/data")
        PosixPath('/home/user/.singlespark/hdfs/user/data')
        >>> hdfs.resolve("/absolute/path")
        PosixPath('/absolute/path')
        """
        p = str(path)
        if p.startswith("hdfs://"):
            # Strip scheme + authority (everything up to and including the third /)
            without_scheme = re.sub(r"^hdfs://[^/]*/", "/", p)
            # Handle hdfs:/// (no authority)
            without_scheme = re.sub(r"^hdfs:///", "/", without_scheme)
            rel = without_scheme.lstrip("/")
            return self.root / rel
        # Local paths pass through unchanged
        return Path(p)

    def resolve_str(self, path: str | Path) -> str:
        return str(self.resolve(path))

    # ------------------------------------------------------------------
    # Filesystem operations
    # ------------------------------------------------------------------

    def exists(self, path: str | Path) -> bool:
        return self.resolve(path).exists()

    def isdir(self, path: str | Path) -> bool:
        return self.resolve(path).is_dir()

    def isfile(self, path: str | Path) -> bool:
        return self.resolve(path).is_file()

    def ls(self, path: str | Path) -> list[str]:
        """List directory contents as resolved string paths."""
        resolved = self.resolve(path)
        if resolved.is_file():
            return [str(resolved)]
        if resolved.is_dir():
            return [str(p) for p in sorted(resolved.iterdir())]
        return []

    def mkdir(self, path: str | Path, parents: bool = True, exist_ok: bool = True) -> None:
        self.resolve(path).mkdir(parents=parents, exist_ok=exist_ok)

    def rm(self, path: str | Path, recursive: bool = False) -> None:
        resolved = self.resolve(path)
        if not resolved.exists():
            raise FileNotFoundError(f"No such file or directory: {path!r}")
        if resolved.is_dir():
            if not recursive:
                raise IsADirectoryError(f"Is a directory (use recursive=True): {path!r}")
            shutil.rmtree(resolved)
        else:
            resolved.unlink()

    def glob(self, pattern: str | Path) -> list[str]:
        """
        Expand a glob pattern (may be an HDFS path) and return resolved local paths.

        Example: hdfs:///data/2024/*.parquet
        """
        resolved_pattern = str(self.resolve(pattern))
        return sorted(_glob.glob(resolved_pattern))

    def move(self, src: str | Path, dst: str | Path) -> None:
        shutil.move(str(self.resolve(src)), str(self.resolve(dst)))

    def copyFromLocal(self, src: str | Path, dst: str | Path) -> None:
        """Copy a local file into the fake HDFS tree."""
        src_path = Path(src)
        dst_path = self.resolve(dst)
        dst_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(str(src_path), str(dst_path))

    def copyToLocal(self, src: str | Path, dst: str | Path) -> None:
        """Copy a file from the fake HDFS tree to a local path."""
        src_path = self.resolve(src)
        dst_path = Path(dst)
        dst_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(str(src_path), str(dst_path))

    def getsize(self, path: str | Path) -> int:
        """Return size in bytes of a file."""
        return self.resolve(path).stat().st_size

    def open(self, path: str | Path, mode: str = "rb"):
        """Open a file — forwards to builtin open() on the resolved path."""
        return open(self.resolve(path), mode)  # noqa: WPS515

    # ------------------------------------------------------------------
    # Helpers for DataFrameReader/Writer
    # ------------------------------------------------------------------

    def expand_paths(self, paths: list[str | Path]) -> list[str]:
        """
        Resolve and expand a list of paths (which may include globs).
        Returns a flat list of resolved local file paths.
        """
        result: list[str] = []
        for p in paths:
            resolved = str(self.resolve(p))
            if any(c in resolved for c in "*?["):
                result.extend(_glob.glob(resolved))
            else:
                result.append(resolved)
        return result

    def __repr__(self):
        return f"FakeHDFS(root={self.root!r})"
