import errno
import os
from pathlib import Path
from typing import Iterator, NewType

from fuse import FuseOSError, Operations, fuse_get_context

from ..core.utils import full_path, log, on_error

ModeT = NewType("ModeT", int)
FileHandleT = NewType("FileHandleT", int)
FileDescriptorT = NewType("FileDescriptorT", int)


class Passthrough(Operations):
    def __init__(self, root: str):
        self.root = Path(root)

    def _full_path(self, partial) -> Path:
        return self.root / partial.lstrip("/")

    @full_path
    @log
    def access(self, path: str, mode: ModeT):
        if not os.access(path, mode):
            raise FuseOSError(errno.EACCES)

    @full_path
    @log
    def chmod(self, path: str, mode: ModeT) -> None:
        try:
            os.chmod(path, mode)
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def chown(self, path: str, uid: int, gid: int) -> None:
        try:
            os.chown(path, uid, gid)
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def getattr(self, path: str, fh=None) -> dict:
        st = os.lstat(path)
        return dict(
            (key, getattr(st, key))
            for key in (
                "st_atime",
                "st_ctime",
                "st_gid",
                "st_mode",
                "st_mtime",
                "st_nlink",
                "st_size",
                "st_uid",
            )
        )

    @full_path
    @log
    def readdir(self, path: str, fh) -> Iterator[str]:
        dirents = [".", ".."]
        if os.path.isdir(path):
            dirents.extend(os.listdir(path))
        for r in dirents:
            yield r

    @full_path
    @log
    def readlink(self, path) -> str:
        try:
            pathname = os.readlink(path)
        except OSError as exc:
            on_error(exc)
        else:
            if pathname.startswith("/"):
                return os.path.relpath(pathname, self.root)
            else:
                return pathname

    @full_path
    @log
    def mknod(self, path: str, mode: ModeT, dev) -> None:
        try:
            os.mknod(path, mode, dev)
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def rmdir(self, path) -> None:
        try:
            os.rmdir(path)
        except FileNotFoundError as exc:
            on_error(exc)
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def mkdir(self, path: str, mode: ModeT) -> None:
        try:
            os.mkdir(path, mode)
        except FileExistsError as exc:
            on_error(exc)
        except FileNotFoundError as exc:
            on_error(exc)
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def statfs(self, path: str) -> dict:
        try:
            stv = os.statvfs(path)
        except OSError as exc:
            on_error(exc)
        else:
            return dict(
                (key, getattr(stv, key))
                for key in (
                    "f_bavail",
                    "f_bfree",
                    "f_blocks",
                    "f_bsize",
                    "f_favail",
                    "f_ffree",
                    "f_files",
                    "f_flag",
                    "f_frsize",
                    "f_namemax",
                )
            )

    @full_path
    @log
    def unlink(self, path) -> None:
        try:
            os.unlink(path)
        except FileNotFoundError as exc:
            on_error(exc)
        except OSError as exc:
            on_error(exc)

    @log
    def symlink(self, name, target, *args) -> None:
        path = self._full_path(name)
        uid, gid, pid = fuse_get_context()
        try:
            os.symlink(target, path)
            os.chown(path, uid, gid, follow_symlinks=False)
        except OSError as exc:
            on_error(exc)

    @log
    def rename(self, old: str, new: str) -> None:
        try:
            os.rename(self._full_path(old), self._full_path(new))
        except FileExistsError as exc:
            on_error(exc)
        except IsADirectoryError as exc:
            on_error(exc)
        except NotADirectoryError as exc:
            on_error(exc)
        except OSError as exc:
            on_error(exc)

    @log
    def link(self, target, name) -> None:
        try:
            os.link(self._full_path(name), self._full_path(target))
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def utimens(self, path: str, times=None) -> None:
        try:
            os.utime(path, times)
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def open(self, path: str, flags) -> FileDescriptorT:
        try:
            return FileDescriptorT(os.open(path, flags))
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def create(self, path: str, mode: ModeT, fi=None) -> FileDescriptorT:
        uid, gid, pid = fuse_get_context()
        try:
            fd = os.open(path, os.O_WRONLY | os.O_CREAT, mode)
            os.chown(path, uid, gid)
        except OSError as exc:
            on_error(exc)
        else:
            return FileDescriptorT(fd)

    @log
    def read(self, path: str, length, offset, fh) -> bytes:
        os.lseek(fh, offset, os.SEEK_SET)
        return os.read(fh, length)

    @log
    def write(self, path: str, buf, offset, fh) -> int:
        os.lseek(fh, offset, os.SEEK_SET)
        return os.write(fh, buf)

    @full_path
    @log
    def truncate(self, path: str, length, fh: FileHandleT = None) -> None:
        try:
            with open(path, "r+") as f:
                f.truncate(length)
        except OSError as exc:
            on_error(exc)

    @log
    def flush(self, path: str, fh) -> None:
        try:
            os.fsync(fh)
        except OSError as exc:
            on_error(exc)

    @log
    def release(self, path: str, fh: FileHandleT) -> None:
        try:
            os.close(fh)
        except OSError as exc:
            on_error(exc)

    @full_path
    @log
    def fsync(self, path: str, fdatasync, fh: FileHandleT) -> None:
        self.flush(path, fh)
