from typing import IO, Any, List, Union

import shutil
import subprocess
import os
import sys
import glob
import threading
import warnings
from subprocess import Popen, PIPE, TimeoutExpired

from argparse import Namespace
from typing import Any, Dict, Generator, List, MutableMapping, Optional, Union
import logging
import warnings
import os
import sys

import numpy as np
import torch


def _convert_params(params: Union[Dict[str, Any], Namespace]) -> Dict[str, Any]:
    """Ensure parameters are a dict or convert to dict if necessary.
    Args:
        params: Target to be converted to a dictionary

    Returns:
        params as a dictionary

    """
    if isinstance(params, Namespace):
        params = vars(params)

    if params is None:
        params = {}

    return params


def _sanitize_callable_params(params: Dict[str, Any]) -> Dict[str, Any]:
    """Sanitize callable params dict, e.g. ``{'a': <function_**** at 0x****>} -> {'a': 'function_****'}``.

    Args:
        params: Dictionary containing the hyperparameters

    Returns:
        dictionary with all callables sanitized
    """

    def _sanitize_callable(val: Any) -> Any:
        if callable(val):
            try:
                _val = val()
                if callable(_val):
                    return val.__name__
                return _val
            except Exception:
                return getattr(val, "__name__", None)
        return val

    return {key: _sanitize_callable(val) for key, val in params.items()}


def _flatten_dict(params: Dict[Any, Any], delimiter: str = "/") -> Dict[str, Any]:
    """Flatten hierarchical dict, e.g. ``{'a': {'b': 'c'}} -> {'a/b': 'c'}``.

    Args:
        params: Dictionary containing the hyperparameters
        delimiter: Delimiter to express the hierarchy. Defaults to ``'/'``.

    Returns:
        Flattened dict.

    Examples:
        >>> _flatten_dict({'a': {'b': 'c'}})
        {'a/b': 'c'}
        >>> _flatten_dict({'a': {'b': 123}})
        {'a/b': 123}
        >>> _flatten_dict({5: {'a': 123}})
        {'5/a': 123}
    """

    def _dict_generator(
        input_dict: Any, prefixes: List[Optional[str]] = None
    ) -> Generator[Any, Optional[List[str]], List[Any]]:
        prefixes = prefixes[:] if prefixes else []
        if isinstance(input_dict, MutableMapping):
            for key, value in input_dict.items():
                key = str(key)
                if isinstance(value, (MutableMapping, Namespace)):
                    value = vars(value) if isinstance(value, Namespace) else value
                    yield from _dict_generator(value, prefixes + [key])
                else:
                    yield prefixes + [key, value if value is not None else str(None)]
        else:
            yield prefixes + [input_dict if input_dict is None else str(input_dict)]

    return {delimiter.join(keys): val for *keys, val in _dict_generator(params)}


def _sanitize_params(params: Dict[str, Any]) -> Dict[str, Any]:
    """Returns params with non-primitvies converted to strings for logging.

    >>> params = {"float": 0.3,
    ...           "int": 1,
    ...           "string": "abc",
    ...           "bool": True,
    ...           "list": [1, 2, 3],
    ...           "namespace": Namespace(foo=3),
    ...           "layer": torch.nn.BatchNorm1d}
    >>> import pprint
    >>> pprint.pprint(_sanitize_params(params))
    {'bool': True,
        'float': 0.3,
        'int': 1,
        'layer': "<class 'torch.nn.modules.batchnorm.BatchNorm1d'>",
        'list': '[1, 2, 3]',
        'namespace': 'Namespace(foo=3)',
        'string': 'abc'}
    """
    for k in params.keys():
        if isinstance(params[k], (np.bool_, np.integer, np.floating)):
            params[k] = params[k].item()
        elif type(params[k]) not in [bool, int, float, str, torch.Tensor]:
            params[k] = str(params[k])
    return params


def _add_prefix(metrics: Dict[str, float], prefix: str, separator: str) -> Dict[str, float]:
    """Insert prefix before each key in a dict, separated by the separator.

    Args:
        metrics: Dictionary with metric names as keys and measured quantities as values
        prefix: Prefix to insert before each key
        separator: Separates prefix and original key name

    Returns:
        Dictionary with prefix and separator inserted before each key
    """
    if prefix:
        metrics = {f"{prefix}{separator}{k}": v for k, v in metrics.items()}

    return metrics


def _name(loggers: List[Any], separator: str = "_") -> str:
    if len(loggers) == 1:
        return loggers[0].name
    else:
        return separator.join(dict.fromkeys(str(logger.name) for logger in loggers))


def _version(loggers: List[Any], separator: str = "_") -> Union[int, str]:
    if len(loggers) == 1:
        return loggers[0].version
    else:
        return separator.join(dict.fromkeys(str(logger.version) for logger in loggers))


def _add_logging_level(level_name, level_num, method_name=None):
    """
    Comprehensively adds a new logging level to the `logging` module and the
    currently configured logging class.

    `level_name` becomes an attribute of the `logging` module with the value
    `level_num`. `method_name` becomes a convenience method for both `logging`
    itself and the class returned by `logging.getLoggerClass()` (usually just
    `logging.Logger`). If `method_name` is not specified, `level_name.lower()` is
    used.

    To avoid accidental clobberings of existing attributes, this method will
    raise an `AttributeError` if the level name is already an attribute of the
    `logging` module or if the method name is already present

    Example
    -------
    >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
    >>> logging.getLogger(__name__).setLevel("TRACE")
    >>> logging.getLogger(__name__).trace('that worked')
    >>> logging.trace('so did this')
    >>> logging.TRACE
    5

    """
    if not method_name:
        method_name = level_name.lower()

    if hasattr(logging, level_name):
        warnings.warn('{} already defined in logging module'.format(level_name))
        return
    if hasattr(logging, method_name):
        warnings.warn('{} already defined in logging module'.format(method_name))
        return
    if hasattr(logging.getLoggerClass(), method_name):
        warnings.warn('{} already defined in logger class'.format(method_name))
        return

    def log_for_level(self, message, *args, **kwargs):
        if self.isEnabledFor(level_num):
            self._log(level_num, message, args, **kwargs)

    def log_to_root(message, *args, **kwargs):
        logging.log(level_num, message, *args, **kwargs)

    logging.addLevelName(level_num, level_name)
    setattr(logging, level_name, level_num)
    setattr(logging.getLoggerClass(), method_name, log_for_level)
    setattr(logging, method_name, log_to_root)


class CruiseLogger(object):
    def __new__(cls):
        if not hasattr(cls, 'instance'):
            level = logging.WARNING
            level_str = os.environ.get("CRS_LOGGING_LEVEL", "WARNING").upper()
            if level_str in logging._nameToLevel:
                level = logging._nameToLevel[level_str]
            formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d][%(module)s]'
                                          '[pid:%(process)d] - %(message)s',
                                          datefmt='%Y-%m-%d %H:%M:%S')
            handler = logging.StreamHandler(stream=sys.stdout)
            handler.setFormatter(formatter)
            cls.instance = logging.getLogger('cruise')
            cls.instance.addHandler(handler)
            cls.instance.setLevel(level)
            cls.instance.propagate = False
        return cls.instance


class CruiseDebugLogger(object):
    def __new__(cls):
        if not hasattr(cls, 'instance'):
            formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d][%(module)s]'
                                          '[pid:%(process)d] - %(message)s',
                                          datefmt='%Y-%m-%d %H:%M:%S')
            handler = logging.StreamHandler(stream=sys.stdout)
            handler.setFormatter(formatter)
            cls.instance = logging.getLogger('cruise_debug')
            cls.instance.addHandler(handler)
            cls.instance.setLevel(logging.DEBUG)
            cls.instance.propagate = False
        return cls.instance


def get_cruise_logger(need_debug=False):
    """Get cruise logger with logging level CRS_LOGGING_LEVEL, and output to stdout.
    """
    if need_debug:
        return CruiseDebugLogger()
    return CruiseLogger()




logger = get_cruise_logger()

ARNOLD_HDFS_NATIVE = os.getenv("ARNOLD_HDFS_NATIVE", "1")
arnold_hdfs_bin = "/.../.../arnold/hdfs_client/hdfs"
java_hdfs_bin = "/.../.../yarn_deploy/hadoop/bin/hdfs"
CHECK_EXIST_TIMEOUT = int(os.getenv("CRS_DEFAULT_HDFS_TEST_TIMEOUT", 60))

HADOOP_BIN = f"ARNOLD_HDFS_NATIVE={ARNOLD_HDFS_NATIVE} {arnold_hdfs_bin}"
arnold_hdfs_... = True
default_shuffle_... = True
default_thread_... = 32
default_chunk_thread_... = 1
default_chunk_size_... = 128
default_force_mv_... = True
ENABLE_UPLOAD_ENCRYPT = int(os.getenv("TRAIN_ENABLE_UPLOAD_ENCRYPT", '0'))
ENCRTPY_KEY = os.getenv("TRAIN_ENCRYPT_KEY", "data.aml.seed_model_keyring")

DEFAULT_HDFS_RETRY_TIMES = int(os.getenv("CRS_DEFAULT_HDFS_RETRY_TIMES", 3))
assert DEFAULT_HDFS_RETRY_TIMES > 0, "DEFAULT_HDFS_RETRY_TIMES should be at least 1"

if ENABLE_UPLOAD_ENCRYPT and ENCRTPY_KEY:
    try:
        import byted_encrypted_hdfs
    except ImportError as e:
        logger.error(f"Unable to import byted_encrypted_hdfs, err is {e}")
        raise Exception("Could not import byted_encrypted_hdfs, pls run `pip install byted_encrypted_hdfs`")

if not os.path.exists(arnold_hdfs_bin):
    logger.warning(f"{arnold_hdfs_bin} not exists, use {java_hdfs_bin} instead.")
    HADOOP_BIN = f"HADOOP_ROOT_LOGGER=ERROR,console {java_hdfs_bin}"
    arnold_hdfs_... = False
    default_shuffle_... = False
    default_thread_... = 1
    default_chunk_thread_... = 1
    default_chunk_size_... = 0
    default_force_mv_... = False

__all__ = [
    "hcopy",
    "hcountline",
    "hdu_dir",
    "hdu_file",
    "hexists",
    "hglob",
    "hisdir",
    "hisfile",
    "hlist_files",
    "hmget",
    "hmkdir",
    "hopen",
    "hput",
    "hrename",
    "hrm",
    "hdfs_open"
]


def _hdfs_cmd(args: str) -> str:
    warnings.warn("byted-hdfs-io is not installed! fall back to deprecated local hdfs_io.")
    return f"{HADOOP_BIN} dfs {args}"


class HdfsPipe:

    def __init__(
        self,
        hdfs_path,
        mode,
        timeout=600,
    ):
        """Create an IO Pipe."""
        self.timeout = timeout
        self.url_path = hdfs_path
        self.status = None
        self.offset = 0
        if mode.startswith("r"):
            if mode == 'rb':
                self.proc = Popen(_hdfs_cmd(f"-cat {hdfs_path}"), shell=True, stdout=PIPE)
            else:
                self.proc = Popen(_hdfs_cmd(f"-text {hdfs_path}"), shell=True, stdout=PIPE)
            self.stream = self.proc.stdout
            return
        if mode == "wa" or mode == "a":
            self.proc = Popen(_hdfs_cmd(f"-appendToFile - {hdfs_path}"), shell=True, stdin=PIPE)
            self.stream = self.proc.stdin
            return
        if mode.startswith("w"):
            self.proc = Popen(_hdfs_cmd(f"-put -f - {hdfs_path}"), shell=True, stdin=PIPE)
            self.stream = self.proc.stdin
            return
        raise RuntimeError(f"unsupported io mode: {mode}")

    def __str__(self):
        return f"<Pipe {self.url_path}>"

    def check_status(self):
        """Poll the process and handle any errors."""
        status = self.proc.poll()
        if status is not None:
            self.wait_for_child()

    def wait_for_child(self):
        """Check the status variable and raise an exception if necessary."""
        verbose = int(os.environ.get("GOPEN_VERBOSE", 0))
        if self.status is not None and verbose:
            return
        self.status = self.proc.wait()
        if verbose:
            print(
                f"file: {self.hdfs_path} mode: {self.mode} pipe exit [{self.status} {os.getpid()}:{self.proc.pid}]",
                file=sys.stderr,
            )

    def read(self, *args, **kw):
        """Wrap stream.read and checks status."""
        result = self.stream.read(*args, **kw)
        self.check_status()
        return result

    def write(self, *args, **kw):
        """Wrap stream.write and checks status."""
        result = self.stream.write(*args, **kw)
        self.check_status()
        return result

    def readLine(self, *args, **kw):
        """Wrap stream.readLine and checks status."""
        result = self.stream.readLine(*args, **kw)
        self.status = self.proc.poll()
        self.check_status()
        return result

    def close(self):
        """Wrap stream.close, wait for the subprocess, and handle errors."""
        self.stream.close()
        self.status = self.proc.wait(self.timeout)
        self.wait_for_child()

    def __enter__(self):
        """Context handler."""
        return self

    def __exit__(self, etype, value, traceback):
        """Context handler."""
        self.close()

    def tell(self, *args, **kw):
        """Fake file tell()."""
        return 0

    def __iter__(self):
        yield from self.stream

    def readlines(self):
        return [line for line in self.stream]


def hdfs_open(hdfs_path: str, mode: str = "r"):
    """
        打开一个 hdfs 文件, 用 contextmanager.

        Args:
            hdfs_path (str): hdfs文件路径
            mode (str): 打开模式，支持 ["r", "w", "wa"]
    """
    return hopen(hdfs_path, mode)


def hopen(hdfs_path: str, mode: str = "r") -> IO[Any]:
    is_hdfs = hdfs_path.startswith("hdfs")
    if is_hdfs:
        return HdfsPipe(hdfs_path, mode)
    elif hdfs_path.endswith(".gz"):
        import gzip
        return gzip.open(hdfs_path, mode)
    else:
        return open(hdfs_path, mode)


def hlist_files(folders: List[str], recursive: bool = False) -> List[str]:
    """
    罗列一些 hdfs 路径下的文件。

    Args:
        folders (List): hdfs文件路径的list
    Returns:
        一个list of hdfs 路径
    """
    files = []
    for folder in folders:
        if folder.startswith("hdfs"):
            is_recursive = " -R" if recursive else ""
            pipe = Popen(_hdfs_cmd(f"-ls{is_recursive} {folder}"), shell=True, stdout=PIPE)
            for line in pipe.stdout:
                line = line.strip()
                if len(line.split()) < 5:
                    continue
                hdfs_file_path = line.split()[-1].decode("utf8")
                if not hdfs_file_path.startswith("..."):
                    continue
                files.append(hdfs_file_path)
            pipe.stdout.close()
            pipe.wait()
        else:
            if os.path.isdir(folder):
                if recursive:
                    for root, _, folder_files in list(os.walk(folder)):
                        for folder_file in folder_files:
                            files.append(os.path.normpath(os.path.join(root, folder_file)))
                else:
                    files.extend([os.path.join(folder, d) for d in os.listdir(folder)])
            elif os.path.isfile(folder):
                files.append(folder)
            else:
                logger.info(f"Path {folder} is invalid")

    return files


def hexists(file_path: str, timeout: int = CHECK_EXIST_TIMEOUT, retry_enabled: bool = True):
    """ hdfs capable to check whether a file_path is exists """
    if file_path.startswith("hdfs"):
        return _run_cmd(_hdfs_cmd(f"-test -e {file_path}"), timeout=timeout, retry_enabled=retry_enabled) == 0
    return os.path.exists(file_path)


def hisdir(file_path: str, timeout: int = CHECK_EXIST_TIMEOUT, retry_enabled: bool = True) -> bool:
    """ hdfs capable to check whether a file_path is a dir """
    if file_path.startswith("hdfs"):
        return _run_cmd(_hdfs_cmd(f"-test -d {file_path}"), timeout=timeout, retry_enabled=retry_enabled) == 0
    return os.path.isdir(file_path)


def hisfile(file_name: str, timeout: int = CHECK_EXIST_TIMEOUT, retry_enabled: bool = True) -> bool:
    """ hdfs capable to check whether a file_name is a file """
    if file_name.startswith("hdfs"):
        return _run_cmd(_hdfs_cmd(f"-test -f {file_name}"), timeout=timeout, retry_enabled=retry_enabled) == 0
    return os.path.isfile(file_name)


def hmkdir(file_path: str) -> bool:
    """hdfs mkdir"""
    if file_path.startswith("hdfs"):
        _run_cmd(_hdfs_cmd(f"-mkdir -p {file_path}"))
    else:
        os.makedirs(file_path, exist_ok=True)
    return True


def _hcopy(from_path: str,
           to_path: str,
           shuffle: bool = default_shuffle_...,
           thread_num: int = default_thread_...,
           chunk_thread_num: int = default_chunk_thread_...,
           chunk_size: int = default_chunk_size_...,
           timeout=None) -> bool:
    """hdfs copy"""
    shuffle_flag = ""
    thread_flag = ""
    chunk_thread_flag = ""
    chunk_size_flag = ""
    if arnold_hdfs_...:
        if shuffle:
            shuffle_flag = "-s"
        if thread_num > 1:
            thread_flag = f"-t {thread_num}"
        if chunk_thread_num > 1:
            chunk_thread_flag = f"--ct {chunk_thread_num}"
            if chunk_size > 0:
                chunk_size_flag = f"-c {chunk_size}"
    returncode = -1
    if to_path.startswith("hdfs"):
        if from_path.startswith("hdfs"):
            returncode = _run_cmd(_hdfs_cmd(f"-cp -f {from_path} {to_path}"), timeout=timeout)
        else:
            returncode = _run_cmd(_hdfs_cmd(f"-put -f {thread_flag} {chunk_thread_flag} {from_path} {to_path}"),
                                  timeout=timeout)
    else:
        if from_path.startswith("hdfs"):
            returncode = _run_cmd(_hdfs_cmd(f"-get {shuffle_flag} {thread_flag} {chunk_thread_flag} {chunk_size_flag} \
                {from_path} {to_path}"),
                                  timeout=timeout)
        else:
            try:
                shutil.copy(from_path, to_path)
                returncode = 0
            except shutil.SameFileError:
                returncode = 0
            except Exception as e:
                logger.warning(f"copy {from_path} {to_path} failed: {e}")
                returncode = -1
    return returncode == 0


def hcopy(
    from_path: str,
    to_path: str,
    shuffle: bool = True,
    thread_num: int = default_thread_...,
    chunk_thread_num: int = default_chunk_thread_...,
    timeout=None,
    skip_encryption: bool = True,
) -> bool:
    """Returns True if the hdfs copy operation is successful."""
    if ENABLE_UPLOAD_ENCRYPT and ENCRTPY_KEY and not skip_encryption:
        logger.info(f"start copy {from_path} {to_path}")
        hdfs_prefix: str = "....../"
        if not from_path.startswith('hdfs') and to_path.startswith(hdfs_prefix) and "checkpoints" in to_path:
            logger.info(f"start upload encrypt {from_path} {to_path}")
            to_path = to_path.replace(hdfs_prefix, "/")
            cmd = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'tools', 'hcopy_encrypted_files.py'))
            try:
                p = subprocess.run([
                    'python3', cmd, '--action=upload', f'--key={ENCRTPY_KEY}', f'--from-path={from_path}',
                    f'--to-path={to_path}'
                ])
                return p.returncode == 0
            except Exception as e:
                logger.warn(f"upload encrypt {from_path} {to_path} failed: {e}")
                return False
        if from_path.startswith(hdfs_prefix) and "checkpoints" in from_path and not to_path.startswith('hdfs'):
            logger.info(f"start download decrypt {from_path} {to_path}")
            from_path = from_path.replace(hdfs_prefix, "/")
            cmd = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'tools', 'hcopy_encrypted_files.py'))
            try:
                p = subprocess.run([
                    'python3', cmd, '--action=download', f'--key={ENCRTPY_KEY}', f'--from-path={from_path}',
                    f'--to-path={to_path}'
                ])
                return p.returncode == 0
            except Exception as e:
                logger.warn(f"download decrypt {from_path} {to_path} failed: {e}")
                return False
    return _hcopy(from_path, to_path, shuffle, thread_num, chunk_thread_num, timeout=timeout)


def hglob(search_path, sort_by_time=False):
    """hdfs glob"""
    if search_path.startswith("hdfs"):
        hdfs_command = _hdfs_cmd(f"-ls {search_path}")
        if sort_by_time:
            hdfs_command += " | sort -k6,7"
        path_list = []
        files = os.popen(hdfs_command).read()
        files = files.split("\n")
        for file in files:
            if "..." in file:
                startindex = file.index("...")
                path_list.append(file[startindex:])
        return path_list
    else:
        files = glob.glob(search_path)
        if sort_by_time:
            files = sorted(files, key=lambda x: os.path.getmtime(x))
    return files


def _htext_list(files, target_folder):
    for fn in files:
        name = os.path.basename(fn)
        _run_cmd(_hdfs_cmd(f"-text {fn} > {target_folder}/{name}"))


def hmget(files, target_folder, num_thread=16):
    """将整个hdfs 文件夹 get下来，但是不是简单的get，因为一些hdfs文件是压缩的，需要解压"""
    part = len(files) // num_thread
    thread_list = []
    for i in range(num_thread):
        start = part * i
        if i == num_thread - 1:
            end = len(files)
        else:
            end = start + part
        t = threading.Thread(
            target=_htext_list,
            kwargs={
                "files": files[start:end],
                "target_folder": target_folder
            },
        )
        thread_list.append(t)

    for t in thread_list:
        t.setDaemon(True)
        t.start()

    for t in thread_list:
        t.join()


def hcountline(path):
    """
    count line in file
    """
    count = 0
    if path.startswith("hdfs"):
        with hopen(path, "r") as f:
            for line in f:
                count += 1
    else:
        with open(path, "r") as f:
            for line in f:
                count += 1
    return count


def hrm(path):
    if path.startswith("hdfs"):
        _run_cmd(_hdfs_cmd(f"-rm -r {path}"))
    else:
        _run_cmd(f"rm -rf {path}")


def hdu_dir(path):
    """hdfs du command to get sizes for all files under a directory. Output is List[Tuple]"""
    if path.startswith("hdfs"):
        cmd_output = os.popen(_hdfs_cmd(f"-du {path}")).read()
        files_info = cmd_output.splitlines()
        rst = []
        for info in files_info:
            size, _, file_path = info.split()
            rst.append((file_path, int(size)))
        return rst
    else:
        cmd_output = os.popen(f"du -b {path}").read()
        files_info = cmd_output.splitlines()
        rst = []
        for info in files_info:
            size, file_path = info.split()
            rst.append((file_path, int(size)))
        return rst


def hdu_file(path):
    """hdfs du command to get size for a specific file. Output is a Tuple"""
    if path.startswith("hdfs"):
        cmd_output = os.popen(_hdfs_cmd(f"-du {path}")).read()
        size, _, file_path = cmd_output.split()
        return (file_path, int(size))
    else:
        cmd_output = os.popen(f"du -b {path}").read()
        size, file_path = cmd_output.split()
        return (file_path, int(size))


def hput(from_path, to_path, force=True, thread_num=default_thread_..., timeout=None):
    """hdfs put a file/directory to remote using native hdfs command"""
    if not from_path.startswith("hdfs") and to_path.startswith("hdfs"):
        hmkdir(to_path)
        assert os.path.exists(from_path), f"{from_path} does not exist."
        force_flag = "-f" if force else ""
        thread_flag = f"-t {thread_num}" if thread_num else ""
        return _run_cmd(_hdfs_cmd(f"-put {force_flag} {thread_flag} {from_path} {to_path}"), timeout=timeout)
    else:
        msg = "Invalid hput pattern, must copy from local to hdfs. "
        msg += f"Given {from_path}->{to_path}"
        raise ValueError(msg)


def _run_cmd(cmd, timeout=None, stderr=PIPE, stdout=PIPE, retry_enabled=False):
    """"This function use subprocess method to run command. With this function
    user can set timeout on the command, also non-fatal error message can be
    hided. Output is return code of the command
    """
    retry_times = DEFAULT_HDFS_RETRY_TIMES if retry_enabled else 1
    for i in range(retry_times):
        p = Popen(cmd, shell=True, stderr=stderr, stdout=stdout)
        try:
            out, error = p.communicate(timeout=timeout // retry_times if timeout else timeout)
            if out or error:
                logger.info(f"Cmd '{cmd}' has stderr message: {error.decode()}, stdout: {out.decode()}"
                            f" and return code: {p.returncode}")
            return p.returncode
        except TimeoutExpired:
            logger.info(f"[Failed] Cmd '{cmd}' is failed due to timeout as {timeout // retry_times}s"
                        f" with {i + 1} attempts and {retry_times - (i + 1)} times left.")
            p.kill()
    logger.warning(f"[Failed] Cmd '{cmd}' fails "
                   f"and use up {retry_times} attempts.")
    return -1


def hrename(from_path: str, to_path: str, force=default_force_mv_..., timeout: Union[int, None] = None):
    """rename a hdfs path"""
    from_is_hdfs = from_path.startswith("hdfs")
    to_is_hdfs = to_path.startswith("hdfs")
    force_flag = "-f" if force else ""
    if from_is_hdfs and to_is_hdfs:
        return _run_cmd(_hdfs_cmd(f"-mv {force_flag} {from_path} {to_path}"), timeout=timeout) == 0
    elif not from_is_hdfs and not to_is_hdfs:
        os.replace(from_path, to_path)
        return True
    else:
        raise NotImplementedError(
            f"Rename on different devices are not supported. from_path: {from_path}, to_path: {to_path}")
