import os
import re
import json
import time
import stat
import shutil
import tarfile
import zipfile
import logging
import filecmp
import datetime
import configparser
from pathlib import Path


logger = logging.getLogger("DBox")


def check_path_is_exits(src_path: str | Path, path_type=None):
    """检查目录或文件是否存在
    :param src_path: 源路径
    :param path_type: 源路径类型，可选值：file/dir
    """
    if isinstance(src_path, str):
        src_path = Path(src_path)

    if not src_path.exists():
        raise FileNotFoundError(f"目录或文件不存在：{src_path}")

    if path_type and path_type.lower() not in ("file", "dir"):
        raise ValueError("参数path_type非法，可选值有：file/dir")

    if path_type and path_type.lower() == "file":
        if not src_path.is_file():
            raise FileNotFoundError(f"{src_path}不是有效的文件！")

    if path_type and path_type.lower() == "dir":
        if not src_path.is_dir():
            raise FileNotFoundError(f"{src_path}不是有效的目录！")


def ensure_empty_dir(target: str | Path, mkdir=True, parents=True, *args, **kwargs) -> None:
    """确保目录为空目录
    :param target: 目标目录路径
    :param mkdir: bool, 目标目录不存在时自动创建
    :param parents: bool, 是否递归创建目录
    """
    if isinstance(target, str):
        target = Path(target)

    # 删除目录时，时常报拒绝访问的错误，增加重试次数
    for _ in range(10):
        try:
            if target.exists():
                if target.is_dir() and len(os.listdir(str(target))) == 0:
                    logger.info(f"已经是空目录：{target}")
                else:
                    if target.is_dir():
                        shutil.rmtree(target, ignore_errors=False)
                        logger.info(f"删除非空目录成功：{target}")
                    else:
                        target.unlink()
                        logger.info(f"删除文件成功：{target}")
                    # 防止报错，有时rmtree删除命令返回成功，但目录并还没有被删除完成，直接创建目录会报错
                    time.sleep(2)
                    if mkdir:
                        target.mkdir(parents=parents)
                        logger.info(f"创建目录成功：{target}")
            else:
                if mkdir:
                    target.mkdir(parents=parents)
                    logger.info(f"创建目录成功：{target}")
        except Exception as e:
            logger.exception(e)
            time.sleep(1)
        else:
            break
    else:
        raise ValueError(f"删除目录出错：{target}")


def compress_zip(src_path: str, compress_abs_path: str) -> None:
    """压缩zip文件
    :param src_path: 待压缩文件或目录路径
    :param compress_abs_path: 压缩包输出绝对路径
    """
    if not os.path.exists(src_path):
        raise FileNotFoundError(src_path)

    with zipfile.ZipFile(compress_abs_path, "w", zipfile.ZIP_DEFLATED) as compress_file:
        if os.path.isdir(src_path):
            for abs_dir_path, dir_list, file_list in os.walk(src_path):
                relative_path = abs_dir_path.replace(src_path, "")
                relative_path = (relative_path and relative_path + os.sep) or ""
                for filename in file_list:
                    compress_file.write(os.path.join(abs_dir_path, filename), relative_path + filename)
        else:
            filename = os.path.basename(src_path)
            compress_file.write(src_path, filename)


def extract_zip(src_zip: str | Path, dst_dir: str | Path) -> None:
    """解压zip文件
    :param src_zip: str or Path, 需要解压的zip文件绝对路径
    :param dst_dir: str or Path, 解压后存储的目标目录
    """
    if isinstance(src_zip, str):
        src_zip = Path(src_zip)
    if isinstance(dst_dir, str):
        dst_dir = Path(dst_dir)

    # zip文件不存在时报错
    if not src_zip.exists():
        raise ValueError(f"zip文件不存在：{src_zip}")

    # 解压目录不存在时新建
    if not dst_dir.exists():
        dst_dir.mkdir(parents=True)

    if dst_dir.is_file():
        raise ValueError(f"{dst_dir}不是一个有效的目录")

    if zipfile.is_zipfile(src_zip):
        fz = zipfile.ZipFile(src_zip, "r")
        for file in fz.namelist():
            fz.extract(file, dst_dir)
    else:
        raise ValueError(f"{src_zip}不是一个有效的zip文件")


def compress_tgz(source_files: str, compress_name: str):
    """将源文件打包成tar.gz格式
    :param source_files：str, 源文件路径，传入相对路径时，压缩包中也为相当路径，为绝对路径时，压缩包中同样为绝对路径
    :param compress_name: str, 生成的压缩包路径
    """
    # 判断文件是否存在，不存在时抛错
    if not os.path.exists(source_files):
        raise FileNotFoundError(source_files)

    with tarfile.open(compress_name, "w:gz") as tar:
        if os.path.isdir(source_files):
            for root, _dir, files in os.walk(source_files):
                for file in files:
                    fullpath = os.path.join(root, file)
                    tar.add(fullpath)
        else:
            tar.add(source_files)


def uncompress_tgz(compress_file, target_path="."):
    """解压tar.gz格式文件
    :param compress_file: str, 压缩包路径
    :param target_path: str, 解压后存储路径
    """
    tar = tarfile.open(compress_file)
    names = tar.getnames()
    for name in names:
        tar.extract(name, path=target_path)
    tar.close()


def rm(src_path: str | Path, *args, **kwargs):
    """删除文件或目录
    :param src_path: str, 源文件或目录路径
    """
    ignore_error = False
    if "ignore_error" in kwargs:
        ignore_error = kwargs["ignore_error"]

    try:
        check_path_is_exits(src_path)
    except Exception as _err:
        if ignore_error:
            logger.warning(f"文件不存在，无需删除：{src_path}")
            return
        else:
            raise _err

    # 统一转换成Path处理
    if isinstance(src_path, str):
        src_path = Path(src_path)

    if src_path.is_file():
        src_path.unlink()
    else:
        shutil.rmtree(str(src_path), onerror=rm_readonly)

    logger.info(f"删除成功：{src_path}")


def rm_safe(src_path: str | Path):
    """安全删除"""
    rm(src_path=src_path, ignore_error=True)


def rm_readonly(fn, tmp, info):
    """删除只读文件"""
    os.chmod(tmp, stat.S_IWRITE)
    if os.path.isfile(tmp):
        os.remove(tmp)
    elif os.path.isdir(tmp):
        shutil.rmtree(tmp)


def move_to_dir(src_path: str | Path, dst_path: str | Path, *args, **kwargs):
    """移动文件
    :param src_path: str or Path, 源文件或目录路径
    :param dst_path: str or Path, 目标文件或目录路径
    """
    src_path = Path(src_path)
    dst_path = Path(dst_path)

    check_path_is_exits(src_path)
    if not dst_path.exists():
        dst_path.mkdir(parents=True)
    shutil.move(str(src_path), str(dst_path))


def copy_to_target(src_path: str | Path, dst_path: str | Path, *args, **kwargs):
    """复制文件或目标到目标路径
    :param src_path: str, 源文件或目录路径，不支持正则表达式；
    :param dst_path: str, 目标字符串路径；
    """
    src_path = Path(src_path)
    dst_path = Path(dst_path)
    check_path_is_exits(src_path)

    if not dst_path.parent.exists():
        dst_path.parent.mkdir(parents=True)

    # 当源对象为文件时
    if src_path.is_file():
        shutil.copy2(str(src_path), str(dst_path))

    # 当源对象为目录时
    if src_path.is_dir():
        if dst_path.exists():
            # 将目录复制到已经存在的目录下
            shutil.copytree(str(src_path), str(dst_path / src_path.name), dirs_exist_ok=True)
        else:
            # 复制源目标生成指定的新目录
            shutil.copytree(str(src_path), str(dst_path), dirs_exist_ok=True)
            return


def copy_to_target_by_pattern(
    src_path: str | Path,
    dst_path: str | Path,
    recursion: bool = True,
    excludes: str | list = "",
):
    """通过表达式复制文件或目标到目标路径
    :param src_path: str, 源文件或目录路径，支持正则表达式；
    :param dst_path: str, 目标字符串路径；
    :param recursion: bool, 递归子目录；
    :param excludes: str, 排除表达式，排除单个目录如："x86"，同时排除多个目录用分号分隔如"x86;x64"；
    """
    src_path = str(src_path)
    dst_path = str(dst_path)
    # 两种情况，表达式与绝对路径
    if os.path.exists(src_path):
        # 绝对路径
        pattern = r".+"
    else:
        # 表达式
        last_sep_index = src_path.rfind(os.sep)
        pattern = src_path[last_sep_index + 1 :]
        src_path = src_path[:last_sep_index]
    # 排除项表达式
    if isinstance(excludes, str):
        if excludes:
            excludes = excludes.split(";")
        else:
            excludes = []
    elif isinstance(excludes, list):
        excludes = excludes
    else:
        excludes = []

    # 遍历源目录
    for child in os.listdir(src_path):
        # 跳过子目录
        if not recursion and os.path.isdir(src_path + os.sep + child):
            continue

        # 判断是否为排除的项
        is_exclude = False
        for _p in excludes:
            if _p and re.match(_p, child, re.I):
                is_exclude = True
                break
        if is_exclude:
            continue

        # 判断是否为匹配的项
        if re.match(pattern, child, re.I):
            copy_to_target(src_path + os.sep + child, dst_path)


def read_file_stream(file_path: str, start_index, end_index):
    """读取文件流
    :param file_path: str, 文件路径
    :param start_index: int, 读取起始位置
    :param end_index: int, 读取结尾位置
    """
    check_path_is_exits(file_path, path_type="file")
    with open(file_path, mode="rb") as _file:
        _file.seek(start_index)
        return _file.read(end_index - start_index)


def read_file_raw_content(file_path: str | Path, encoding=None) -> tuple:
    """读取文件内容
    :param file_path: 文件路径
    :param encoding: 文件字符编码，没有指定时会按照utf-8，GBK，GB2312，GB18030依次读取
    :return 返回文件类型与读取成功的文件编码
    """
    check_path_is_exits(file_path)
    file_path = Path(file_path)

    if encoding:
        encoding_list = [encoding]
    else:
        encoding_list = ["utf-8", "GBK", "GB2312", "GB18030"]

    error = None
    for encoding in encoding_list:
        try:
            with open(file_path, encoding=encoding) as _file:
                return _file.read(), encoding
        except Exception as e:
            error = e
    else:
        if error is not None:
            raise error
        else:
            raise ValueError(f"无法读取文件：{file_path}")


def read_file_content(file_path: str | Path, encoding=None, _return=None, default=None):
    """读取文件内容
    :param file_path: 文件路径
    :param encoding: 文件字符编码，没有指定时会按照utf-8，GBK，GB2312，GB18030依次读取
    :param _return: 为json时返回json格式数据，否则返回str
    :param default: 文件内容为空时返回的默认值
    :return 返回文件类型与读取成功的文件编码
    """
    if default is None:
        default = {}
    _raw_content, _ = read_file_raw_content(file_path, encoding)
    if _return and _return.lower() == "json":
        _raw_content = _raw_content.strip()
        if _raw_content:
            return json.loads(_raw_content)
        else:
            return default
    else:
        return _raw_content


def save_obj_to_file(obj, file_abs_path: str, exist_ok=True, encoding="utf-8"):
    """保存对象到文件
    :param obj: 待保存对象，仅限文件对象，不能是二进制对象
    :param file_abs_path: 保存路径
    :param exist_ok: 为True时覆盖已经存在的文件，为False时抛错
    :param encoding: 文件编码
    """
    if isinstance(obj, (dict, list, tuple)):
        return save_json_to_file(obj, file_abs_path, exist_ok, encoding)

    if isinstance(obj, bytes):
        obj = obj.decode(encoding)
    else:
        obj = str(obj)

    with open(file_abs_path, mode="w", encoding=encoding) as _file:
        return _file.write(obj)


def save_json_to_file(
    content: dict | list | tuple,
    file_abs_path: Path | str,
    exist_ok=True,
    encoding="utf-8",
    ensure_ascii=False,
    indent=2,
    cls=None,
):
    """将字典、列表、元组保存到文件中
    :param content: 待保存对象，仅限dict,list,tuple
    :param file_abs_path: 保存路径
    :param exist_ok: 为True时覆盖已经存在的文件，为False时抛错
    :param encoding: 文件编码
    :param ensure_ascii:
    :param indent: int, 格式化JSON对象时缩进字符数量
    :param cls: json dumps 的转换类
    """
    file_abs_path = Path(file_abs_path)
    if not exist_ok:
        if file_abs_path.exists():
            raise FileExistsError(f"文件已经存在：{file_abs_path}")

    if not file_abs_path.parent.exists():
        file_abs_path.parent.mkdir(parents=True)

    with open(file_abs_path, mode="w", encoding=encoding) as _file:
        return json.dump(content, _file, ensure_ascii=ensure_ascii, indent=indent, cls=cls)


def get_newest_file(target: str, _type: str = "c") -> str | None:
    """获取目录下最新的文件
    :param target: str，目标目录；
    :param _type: str，类型，c按创建时间，m按修改时间，a按访问时间
    """
    if _type.lower() not in ("c", "m", "a"):
        raise ValueError(f"_type参数非法，正确的取值为：a,m,c")
    else:
        _method = getattr(os.path, f"get{_type}time")

    if os.path.isdir(target):
        file_list = os.listdir(target)
        file_list.sort(key=lambda _file: _method(target + os.sep + _file))
        return target + os.sep + file_list[-1]


def get_file_time(file_path: str | Path, _type: str = "c", _return=None):
    """根据文件修改日期获取插件版本号"""
    file_path = Path(file_path)
    if _type.lower() not in ("c", "m", "a"):
        raise ValueError(f"_type参数非法，正确的取值为：a,m,c")
    else:
        _method = getattr(file_path.stat(), f"st_{_type}time")
        _time_obj = datetime.datetime.fromtimestamp(_method)
        if _return == datetime.datetime:
            return _time_obj
        elif _return == datetime.date:
            return _time_obj.date()
        elif _return == float:
            return _time_obj.timestamp()
        elif _return == int:
            return round(_time_obj.timestamp())
        else:
            return _time_obj.strftime("%Y-%m-%d %H:%M:%S")


def get_ini_config_object(config_path: Path):
    """获取config.ini配置文件对象"""
    __config = configparser.ConfigParser()
    __config.read(config_path, encoding="utf-8")
    return __config


def save_ini_config_object(config_path: Path, _object: configparser.ConfigParser):
    """保存配置文件到config.int中"""
    with open(config_path, mode="w", encoding="utf-8") as _file:
        _object.write(_file)


def compare_dir(target1: str, target2: str) -> bool:
    """比较两个目标是否完成一致"""
    logger.info(f"开始比较目录：{target1}与{target2}")

    def print_diff_files(dcmp):
        for name in dcmp.diff_files:
            logger.error("diff_file %s found in %s and %s" % (name, dcmp.left, dcmp.right))
        for sub_dcmp in dcmp.subdirs.values():
            print_diff_files(sub_dcmp)

    cmp_res = filecmp.dircmp(target1, target2)
    print_diff_files(cmp_res)
    if cmp_res.diff_files:
        logger.error("两个目录不完全相同")
        return False
    else:
        logger.info("两个目录完全相同")
        return True


def compare_file(file1: str, file2: str) -> bool:
    """比较两个文件是否一致"""
    return filecmp.cmp(file1, file2, shallow=False)
