"""..."""
import hashlib
import os
import shutil
from datetime import datetime
from typing import Callable
from typing import Dict, Union
from typing import Optional

from aligo.core import *
from aligo.request import GetFileListRequest, MoveFileToTrashRequest, CreateFolderRequest
from aligo.types import BaseFile


def utc_str_to_timestamp(utc: str) -> int:
    """'2022-04-16T11:07:03.276Z' -> timestamp"""
    return int(datetime.fromisoformat(utc[:-1]).timestamp()) + 28800


class SyncFolder(Core):
    """sync folder

    冲突：本地和云端冲突，一端是文件，另一端是文件夹，此种情况跳过，统一不处理

    三种同步模式：
        1. 双端同步
            差异文件保持最新；独有文件上传和下载，保持共有。

        2. 本地为主
            差异文件，本地为主；本地独有文件，上传；云端独有文件，忽略。

        3. 云端为主
            差异文件，云端为主；云端独有文件，下载；本地独有文件，忽略。
    """

    def sync_folder(
            self,
            local_folder: str,
            remote_folder: str,
            flag: Optional[bool] = None,
            file_filter: Callable[[Union[str, BaseFile]], bool] = lambda x: False,
            ignore_content: bool = False,
            follow_delete: bool = False,
            drive_id: str = None
    ):
        """
        sync folder
        :param local_folder: 本地文件夹路径
        :param remote_folder: 云端文件夹 file_id
        :param flag: 同步标志（类型），默认：None, 另外可选：True, False
            None：双端同步
            True：以本地为主
            False：以云端为主
        :param file_filter: 文件过滤函数，参数为 本地文件绝对路径 / 云端文件 BaseFile，返回值为 `True` 则过滤，可用于实现 只同步 特定文件 或 排除某些文件
        :param ignore_content: 是否忽略文件内容，默认：False
        :param follow_delete: 是否跟随删除，默认：False
        :param drive_id: 云端文件夹 drive_id
        :return:

        :Example:
        >>> from aligo import Aligo
        >>> ali = Aligo()
        >>> ali.sync_folder('<本地路径>', '<云端文件夹 file_id>')
        """
        if flag is None:
            self._auth.log.info('sync_folder: 双端同步')
        elif flag:
            self._auth.log.info('sync_folder: 本地为主')
        else:
            self._auth.log.info('sync_folder: 云端为主')

        if not os.path.exists(local_folder):
            self._auth.log.warning('本地文件夹不存在: %s', local_folder)
            return
        self.__sync_folder(local_folder, remote_folder, flag, file_filter, ignore_content, follow_delete, drive_id)

    def __sync_folder(
            self,
            local_folder: str,
            remote_folder: str,
            flag: Optional[bool],
            file_filter: Callable[[Union[str, BaseFile]], bool],
            ignore_content: bool,
            follow_delete: bool,
            drive_id: str):
        """..."""
        # 获取local_folder下的所有文件 # 转为 {文件名:文件路径} 字典模式
        local_files = {}
        for f in os.listdir(local_folder):
            local_file = os.path.join(local_folder, f)
            # 过滤文件只处理文件，跳过 文件夹
            if os.path.isfile(local_file) and file_filter(local_file):
                self._auth.log.debug(f'过滤本地文件 {local_file}')
                continue
            local_files[f] = local_file
        # 获取remote_folder下的所有文件 # 转为 {文件名:BaseFile对象} 字典模式
        remote_files: Dict[str, BaseFile] = {}
        for f in self._core_get_file_list(
                GetFileListRequest(remote_folder, drive_id=drive_id)
        ):
            remote_file = f.name
            if f.type == 'file' and file_filter(f):
                self._auth.log.debug(f'过滤云端文件 {remote_file}')
                continue
            remote_files[remote_file] = f

        if flag is None:
            self.__sync_all(
                drive_id, file_filter, flag, local_files, local_folder, remote_files, remote_folder, ignore_content,
                follow_delete)
        elif flag:
            self.__sync_local(
                drive_id, local_files, remote_files, remote_folder, flag, file_filter, local_folder, ignore_content,
                follow_delete)
        else:
            self.__sync_remote(drive_id, local_files, remote_files, local_folder, flag, file_filter, ignore_content,
                               follow_delete)

    def __sync_all(
            self, drive_id, file_filter, flag, local_files, local_folder, remote_files, remote_folder, ignore_content,
            follow_delete
    ):
        # 双端同步
        for f in list(local_files):
            # 在字典中获取并删除 local_file
            local_file = local_files.pop(f)
            # 文件夹则递归
            if os.path.isdir(local_file):
                # 判断云端是否存在
                if f in remote_files:
                    # 如果有，则判断是不是文件夹，不是就跳过
                    remote_file = remote_files.pop(f)
                    if remote_file.type == 'file':
                        # 跳过冲突
                        self._auth.log.warning(f'冲突：本地是文件夹，云端是文件，不处理 {f}')
                        continue
                else:
                    # 如果没有，则创建文件夹
                    self._auth.log.debug(f'本地是文件夹，云端不存在，创建云端文件夹，并递归 {f}')
                    remote_file = self._core_create_folder(
                        CreateFolderRequest(name=f, parent_file_id=remote_folder, drive_id=drive_id,
                                            check_name_mode='overwrite')
                    )
                self.__sync_folder(local_file, remote_file.file_id, flag, file_filter, ignore_content, follow_delete,
                                   drive_id)
                continue

            # 如果本地文件存在，且在云端也存在，则依次比较 size，sha1，时间戳
            if f in remote_files:
                # 在字典中获取并删除 remote_file
                remote_file = remote_files.pop(f)
                # 先判断文件类型
                if remote_file.type == 'folder':
                    # 没有主次端，不自动处理冲突
                    self._auth.log.warning(f'冲突：本地为文件，云端为文件夹，不处理 {f}')
                    continue

                # 获取 f 文件大小
                local_size = os.path.getsize(local_file)
                # 比较大小
                if local_size == remote_file.size:
                    # 跳过对比文件内容
                    if ignore_content:
                        self._auth.log.warning(f'忽略文件内容: 不处理 {f}')
                        continue

                    # 计算 f 文件 sha1
                    local_sha1 = self._core_sha1(local_file).lower()
                    # 如果sha1值相同，则跳过
                    if local_sha1 == remote_file.content_hash.lower():
                        self._auth.log.debug(f'文件 {f} 已存在，且sha1值相同，跳过')
                        continue
                # 否则对比时间戳，删除较旧文件，同步最新文件
                # 获取 f 文件的最后修改时间
                local_time = os.path.getmtime(local_file)
                local_time = int(local_time)
                # 获取云端文件的最后修改时间
                remote_time = remote_file.updated_at
                # 将UTC时间字符串转换为时间戳
                remote_time = utc_str_to_timestamp(remote_time)
                # 比较时间戳，删除较旧文件，同步最新文件
                if local_time > remote_time:
                    # 覆盖方式上传文件
                    self._auth.log.debug(f'本地文件较新，上传本地文件 {f}')
                    self.upload_file(file_path=local_file, parent_file_id=remote_folder, name=f,
                                     drive_id=drive_id, check_name_mode='overwrite')
                elif local_time < remote_time:
                    # 云端较新，删除本地，下载云端文件
                    self._auth.log.debug(f'云端较新，删除本地，下载云端文件 {f}')
                    os.remove(local_file)
                    self.download_files([remote_file], local_folder)
            else:
                # 不存在则直接上传
                self._auth.log.debug(f'云端不存在，上传本地文件 {f}')
                self.upload_file(file_path=local_file, parent_file_id=remote_folder, name=f,
                                 drive_id=drive_id, check_name_mode='overwrite')
        # 剩下的就是云端文件夹中有，本地文件夹中没有的文件，下载到本地
        if len(remote_files) != 0:
            self._auth.log.debug(f'本地不存在，下载云端文件 {len(remote_files)} {list(remote_files.keys())}')
            for remote_file in remote_files.values():
                if remote_file.type == 'file':
                    self.download_files([remote_file], local_folder)
                else:
                    self.download_folder(remote_file.file_id, local_folder)  # type: ignore

    def __sync_remote(self, drive_id, local_files, remote_files,
                      local_folder, flag, file_filter, ignore_content, follow_delete):
        # 以云端为主
        for f in list(remote_files):
            remote_file = remote_files.pop(f)
            # 文件夹则递归
            if remote_file.type == 'folder':
                # 判断本地是否存在
                if f in local_files:
                    # 如果有，则判断是否是文件夹，不是就忽略
                    local_file = local_files.pop(f)
                    if os.path.isfile(local_file):
                        # 跳过冲突
                        self._auth.log.warning(f'冲突：本地是文件，云端是文件夹，不处理 {f}')
                else:
                    # 如果没有，则创建本地文件夹
                    local_file = os.path.join(local_folder, f)
                    self._auth.log.debug(f'云端是文件夹，本地没有，创建文件夹，并递归 {local_file}')
                    if not os.path.exists(local_file):
                        os.mkdir(local_file)
                self.__sync_folder(local_file, remote_file.file_id, flag, file_filter, ignore_content, follow_delete,
                                   drive_id)
                continue

            # 如果云端文件存在，且在本地也存在，则依次比较 size，sha1，时间戳
            if f in local_files:
                local_file = local_files.pop(f)
                # 先判断文件类型
                if os.path.isdir(local_file):
                    # 云端为文件，本地却是文件夹，则递归删除文件夹，再下载文件
                    self._auth.log.debug('云端为文件，本地却是文件夹，则递归删除文件夹，再下载文件')
                    # 递归删除文件夹
                    shutil.rmtree(local_file)
                    # 下载文件
                    self.download_files([remote_file], local_folder)
                    continue

                # 获取 f 文件大小
                remote_size = remote_file.size
                # 比较大小
                if remote_size == os.path.getsize(local_file):
                    # 跳过对比文件内容
                    if ignore_content:
                        self._auth.log.warning(f'忽略文件内容: 不处理 {f}')
                        continue

                    # 计算 f 文件 sha1
                    local_sha1 = self._core_sha1(local_file).lower()
                    # 如果sha1值相同，则跳过
                    if local_sha1 == remote_file.content_hash.lower():
                        self._auth.log.debug(f'文件 {local_file} 已存在，且sha1值相同，跳过')
                        continue
                os.remove(local_file)
                self.download_files([remote_file], local_folder)
            else:
                # 不存在直接下载
                local_file = os.path.join(local_folder, f)
                self._auth.log.debug(f'本地不存在，下载云端文件 {local_file}')
                self.download_files([remote_file], local_folder)

        # 剩下的就是云端文件夹中没有，本地文件夹中有的文件，删除
        if follow_delete and len(local_files) != 0:
            self._auth.log.debug(f'云端不存在，本地存在 删除 {len(local_files)} {list(local_files.keys())}')
            for local_file in local_files.values():
                if os.path.isfile(local_file):
                    os.remove(local_file)
                else:
                    shutil.rmtree(local_file)

    def __sync_local(self, drive_id, local_files, remote_files,
                     remote_folder, flag, file_filter, local_folder, ignore_content, follow_delete):
        # 以本地为主
        for f in list(local_files):
            local_file = local_files.pop(f)
            # 文件夹则递归
            if os.path.isdir(local_file):
                # 判断云端是否存在
                if f in remote_files:
                    # 如果有，则判断是不是文件夹，不是就删除
                    remote_file = remote_files.pop(f)
                    if remote_file.type == 'file':
                        # 删除云端文件并创建文件夹
                        self._auth.log.warning(f'冲突：本地是文件夹，云端是文件，不处理 {f}')
                else:
                    # 如果没有，则创建文件夹
                    self._auth.log.debug(f'本地是文件夹，云端不存在，创建云端文件夹，并递归 {f}')
                    remote_file = self._core_create_folder(
                        CreateFolderRequest(name=f, parent_file_id=remote_folder, drive_id=drive_id,
                                            check_name_mode='overwrite')
                    )
                self.__sync_folder(local_file, remote_file.file_id, flag, file_filter, ignore_content, follow_delete,
                                   drive_id)
                continue

            # 如果本地文件存在，且在云端也存在，则依次比较 size，sha1，时间戳
            if f in remote_files:
                # 在字典中删除remote_file
                remote_file = remote_files.pop(f)
                # 先判断文件类型
                if remote_file.type == 'folder':
                    # 以本地文件夹为主，那只有删除云端文件夹，再上传本地文件
                    self._auth.log.debug(f'本地为文件，云端为文件夹，删除云端文件夹，并上传本地文件 {f}')
                    self._core_move_file_to_trash(
                        MoveFileToTrashRequest(file_id=remote_file.file_id, drive_id=drive_id)
                    )
                    self._auth.log.debug(f'上传本地文件 {f}')
                    self.upload_file(file_path=local_file, parent_file_id=remote_folder, name=f,
                                     drive_id=drive_id, check_name_mode='overwrite')
                    continue

                # 获取 f 文件大小
                local_size = os.path.getsize(local_file)
                # 比较大小
                if local_size == remote_file.size:
                    # 跳过对比文件内容
                    if ignore_content:
                        self._auth.log.warning(f'忽略文件内容: 不处理 {f}')
                        continue

                    # 计算 f 文件 sha1
                    local_sha1 = self._core_sha1(local_file).lower()
                    # 如果sha1值相同，则跳过
                    if local_sha1 == remote_file.content_hash.lower():
                        self._auth.log.debug(f'文件 {f} 已存在，且sha1值相同，跳过')
                        continue
                    else:
                        self.upload_file(file_path=local_file, parent_file_id=remote_folder, name=f,
                                         drive_id=drive_id, check_name_mode='overwrite')
                else:
                    self.upload_file(file_path=local_file, parent_file_id=remote_folder, name=f,
                                     drive_id=drive_id, check_name_mode='overwrite')
            else:
                # 不存在则直接上传
                self._auth.log.debug(f'云端不存在，上传本地文件 {f}')
                self.upload_file(file_path=local_file, parent_file_id=remote_folder, name=f,
                                 drive_id=drive_id, check_name_mode='overwrite')

        # 剩下的就是本地文件夹中没有，云端文件夹中有的文件，删除
        if follow_delete and len(remote_files) != 0:
            self._auth.log.debug(f'本地不存在，云端存在 删除 {len(remote_files)} {list(remote_files.keys())}')
            self.batch_move_to_trash([remote_file.file_id for remote_file in remote_files.values()])  # type: ignore

    @staticmethod
    def _core_sha1(param):
        """计算文件sha1"""
        sha1 = hashlib.sha1()
        with open(param, 'rb') as f:
            while True:
                data = f.read(8192)
                if not data:
                    break
                sha1.update(data)
        return sha1.hexdigest()
