"""
Bighead DFS 客户端模块

此模块提供了与Bighead DFS服务器交互的主要客户端接口。
"""

import os
import json
import hashlib
import base64
import time
import requests
import logging
from io import BytesIO
from typing import Optional, Dict, Any, Callable, IO
from urllib.parse import quote

# 配置日志
logger = logging.getLogger(__name__)

from bighead_dfs_client.config import BigheadDfsConfig


def _calculate_hash(stream: IO[bytes]) -> str:
    """计算文件的MD5哈希值，兼容C#客户端逻辑。

    参数:
        stream: 文件流。

    返回:
        文件的哈希值。
    """
    current_pos = stream.tell()
    stream.seek(0)

    hash_obj = hashlib.md5()
    chunk_size = 8192
    while True:
        chunk = stream.read(chunk_size)
        if not chunk:
            break
        hash_obj.update(chunk)

    stream.seek(current_pos)

    return hash_obj.hexdigest()


class BigheadDfsClient:
    """用于与Bighead DFS服务器交互的客户端。

    此客户端提供了在Bighead DFS服务器上上传、下载和管理文件的方法。
    实例化Client需要单例模式。
    """

    def __init__(self, config: BigheadDfsConfig):
        """初始化BigheadDfsClient的新实例。

        参数:
            config: DFS客户端的配置。
        """
        self.config = config
        # 禁用SSL验证
        self.session = requests.Session()
        self.session.verify = False

    def _add_sign_header(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
        """向请求添加签名头。

        参数:
            headers: 现有的头信息字典。

        返回:
            添加了签名信息的头信息字典。
        """
        if headers is None:
            headers = {}

        # 生成签名
        headers['Authorization'] = self.generate_signature()
        return headers

    def generate_signature(self) -> str:
        """生成签名，过期时间为12小时。

        返回:
            生成的签名。
        """
        if not self.config.app_id or not self.config.app_secret:
            return ""

        # 获取当前时间戳（毫秒级）
        time_stamp = int(time.time() * 1000)

        # 拼接字符串
        str_to_sign = f"{self.config.app_id}{time_stamp}{self.config.app_secret}"

        # 计算MD5
        md5_hash = hashlib.md5(str_to_sign.encode('utf-8'))
        sign = md5_hash.hexdigest()

        # 拼接内容
        content = f"{self.config.app_id}^{time_stamp}^{sign}"

        # Base64编码
        value = base64.b64encode(content.encode('utf-8')).decode('utf-8')

        return value

    def upload_resource_by_path(self, bucket_name: str, path: str, description: str = "") -> str:
        """通过本地路径上传资源。

        参数:
            bucket_name: 文件桶名称。
            path: 文件路径。
            description: 文件描述。

        返回:
            文件指纹。
        """
        if not os.path.exists(path):
            raise FileNotFoundError(f"文件不存在: {path}")

        with open(path, 'rb') as file_stream:
            file_bytes = file_stream.read()

        file_name = os.path.basename(path)
        return self.upload_resource_by_bytes(bucket_name, file_bytes, file_name, description)

    def upload_resource_by_bytes(self, bucket_name: str, buffer: bytes, file_name: Optional[str] = None,
                                 description: str = "") -> str:
        """通过字节数组上传资源，核心实现。

        参数:
            bucket_name: 文件桶名称。
            buffer: 文件字节数据。
            file_name: 文件名。
            description: 文件描述。

        返回:
            文件指纹。
        """
        if buffer is None:
            raise ValueError("buffer 不能为空")

        if not file_name:
            file_name = "upload.bin"

        file_name = os.path.basename(file_name)
        fingerprint = _calculate_hash(BytesIO(buffer))

        if self.resource_exist(bucket_name, fingerprint):
            return fingerprint

        url = f"{self.config.url}/Bighead/Api/UploadResource"
        headers = self._add_sign_header()

        files = {
            'file': (file_name, buffer)
        }
        data = {
            'resourceFingerprint': fingerprint,
            'bucketName': bucket_name,
            'description': description
        }

        response = self.session.post(url, headers=headers, files=files, data=data)

        if response.status_code != 200:
            error_message = f"HTTP Error: {response.status_code}, {response.text}"
            logger.error(error_message)
            raise Exception(error_message)

        try:
            response_payload_info = response.json()
        except json.JSONDecodeError as exc:
            logger.error("响应解析失败: %s", exc)
            raise Exception("响应解析失败") from exc

        if response_payload_info.get('isSuccess'):
            return fingerprint

        raise Exception(response_payload_info.get('message', '文件上传失败'))

    def parts_upload_resource(self, bucket_name: str, path: str, description: str = "",
                              progress_action: Optional[Callable[[int], None]] = None) -> str:
        """分片上传资源，一般适用于大文件。

        参数:
            bucket_name: 文件桶名称。
            path: 文件路径。
            description: 文件描述。
            progress_action: 上传进度回调。

        返回:
            文件的指纹（哈希值）。
        """
        # 计算文件指纹
        with open(path, 'rb') as f:
            fingerprint = _calculate_hash(f)

        # 文件秒传
        if self.resource_exist(bucket_name, fingerprint):
            return fingerprint

        # 分片大小
        chunk_size = 1024 * 1024 * 5  # 5MB
        file_size = os.path.getsize(path)

        # 获取分片上传记录
        parts_record = self.parts_resource_upload_record(bucket_name, fingerprint)
        if not parts_record:
            raise Exception("获取分片上传记录失败")

        already_uploaded_length = parts_record["alreadyUploadedLength"]

        with open(path, 'rb') as file_stream:
            # 分片上传
            while already_uploaded_length < file_size:
                file_stream.seek(already_uploaded_length)

                # 处理最后一块的大小
                current_chunk_size = chunk_size if (file_size - already_uploaded_length) >= chunk_size else (
                        file_size - already_uploaded_length)
                buffer = file_stream.read(current_chunk_size)

                # 准备上传数据
                url = f"{self.config.url}/Bighead/Api/PartsUploadResource"
                headers = self._add_sign_header()

                files = {'File': (os.path.basename(path), buffer)}
                data = {
                    'ResourceFingerprint': fingerprint,
                    'BucketName': bucket_name,
                    'Description': description,
                    'AlreadyUploadedLength': str(already_uploaded_length),
                    'ResourceTotalLength': str(file_size)
                }

                response = self.session.post(url, headers=headers, files=files, data=data)

                # 检查服务器返回的内容是否包含错误信息
                if response.status_code != 200:
                    error_message = f"HTTP Error: {response.status_code}, {response.text}"
                    logger.error(error_message)
                    raise Exception(error_message)

                # 解析响应
                response_payload_info = response.json()

                if not response_payload_info:
                    raise Exception("响应解析失败")

                if response_payload_info.get('code') == 2:
                    # 别人正在上传中
                    break

                if not response_payload_info.get('isSuccess'):
                    raise Exception(response_payload_info.get('message', '文件上传失败'))

                already_uploaded_length = response_payload_info.get('alreadyUploadedLength', already_uploaded_length)

                # 上传进度回调
                progress = int((already_uploaded_length / file_size) * 100)
                if progress_action:
                    progress_action(progress)

        if file_size == already_uploaded_length:
            return fingerprint
        else:
            raise Exception("文件上传失败")

    def parts_resource_upload_record(self, bucket_name: str, fingerprint: str):
        """获取该资源的分片记录信息。

        参数:
            bucket_name: 文件桶名称。
            fingerprint: 文件指纹。

        返回:
            分片上传记录信息。
        """
        url = f"{self.config.url}/Bighead/Api/PartsResourceUploadRecord?bucketName={quote(bucket_name)}&fingerprint={quote(fingerprint)}"
        headers = self._add_sign_header()

        response = self.session.get(url, headers=headers)

        if response.status_code == 200:
            record_data = response.json()
            if record_data:
                return record_data

        return None

    def resource_exist(self, bucket_name: str, fingerprint: str) -> bool:
        """检查资源是否存在。

        参数:
            bucket_name: 文件桶名称。
            fingerprint: 文件指纹。

        返回:
            如果资源存在返回True，否则返回False。
        """
        url = f"{self.config.url}/Bighead/Api/ResourceExist?bucketName={quote(bucket_name)}&fingerprint={quote(fingerprint)}"
        headers = self._add_sign_header()

        response = self.session.get(url, headers=headers)

        if not response.status_code == 200:
            raise Exception(f"请求失败, {response.status_code}")

        # 将response转为string
        response_text = response.text.strip()

        return response_text == "true"

    def generate_thumbnail_browse_url(self, bucket_name: str, fingerprint: str, width: int, height: int) -> str:
        """生成缩略图地址，支持视频。

        参数:
            bucket_name: 文件桶名称。
            fingerprint: 文件指纹。
            width: 缩略图宽度。
            height: 缩略图高度。

        返回:
            缩略图的访问URL。
        """
        sign = self.generate_signature()
        url = f"{self.config.url}/resizeWithToken/{quote(bucket_name)}/{quote(fingerprint)}/{sign}/{width}/{height}"
        return url

    def generate_browse_url(self, bucket_name: str, fingerprint: str) -> str:
        """生成浏览器访问地址。

        参数:
            bucket_name: 文件桶名称。
            fingerprint: 文件指纹。

        返回:
            文件的访问URL。
        """
        sign = self.generate_signature()
        url = f"{self.config.url}/{quote(bucket_name)}/{quote(fingerprint)}/{sign}"
        return url

    def video_duration(self, bucket_name: str, fingerprint: str) -> Any:
        """获取视频资源的时长。

        参数:
            bucket_name: 文件桶名称。
            fingerprint: 文件指纹。

        返回:
            视频时长信息。
        """
        url = f"{self.config.url}/VideoDuration/{quote(bucket_name)}/{quote(fingerprint)}"
        headers = self._add_sign_header()

        response = self.session.get(url, headers=headers)

        if not response.status_code == 200:
            raise Exception(f"请求失败, {response.status_code}")

        result = response.json()
        if result.get('isSuccess'):
            return result.get('data', {})

        return {}

    def _generate_browse_url_internal(self, bucket_name: str, fingerprint: str) -> str:
        """生成内部浏览URL。

        参数:
            bucket_name: 文件桶名称。
            fingerprint: 文件指纹。

        返回:
            内部浏览URL。
        """
        url = f"{self.config.url}/{quote(bucket_name)}/{quote(fingerprint)}"
        return url

    def download_file(self, path: str, bucket_name: str, fingerprint: str,
                      call_back: Optional[Callable[[str, str], None]] = None) -> None:
        """下载文件，支持断点续传。

        参数:
            path: 文件保存路径。
            bucket_name: 桶名称。
            fingerprint: 文件指纹。
            call_back: 下载完成后的回调。
        """
        # 检查文件是否已存在及大小
        existing_file_size = 0
        if os.path.exists(path):
            existing_file_size = os.path.getsize(path)

        url = self._generate_browse_url_internal(bucket_name, fingerprint)
        headers = self._add_sign_header()

        # 如果文件已经部分下载，则设置Range头
        if existing_file_size > 0:
            headers['Range'] = f"bytes={existing_file_size}-"

        response = self.session.get(url, headers=headers, stream=True)

        if response.status_code in [200, 206]:  # 200: 完整响应, 206: 部分内容
            # 确保目录存在
            os.makedirs(os.path.dirname(path), exist_ok=True)

            # 以追加模式写入文件
            mode = 'ab' if existing_file_size > 0 else 'wb'
            with open(path, mode) as file_stream:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        file_stream.write(chunk)

            if call_back:
                call_back(bucket_name, fingerprint)

    def offline_download(self, target_url: str, call_back_url: str, payload: str) -> bool:
        """离线下载。

        参数:
            target_url: 下载地址。
            call_back_url: 回调地址。
            payload: 创建下载任务携带的数据。

        返回:
            如果请求成功返回True，否则返回False。
        """
        request_data = {
            'TargetUrl': target_url,
            'CallBackUrl': call_back_url,
            'Payload': payload
        }

        url = f"{self.config.url}/Bighead/Api/OfflineDownload"
        headers = self._add_sign_header()
        headers['Content-Type'] = 'application/json'

        response = self.session.post(url, headers=headers, json=request_data)

        return response.status_code == 200
