# -*- coding: utf-8 -*-
"""
文件上传组合模块
提供完整的文件上传流程，包括秒传、分片上传等功能
"""

import os
import hashlib
import asyncio
import httpx
from typing import Union, Optional, Callable, Dict, Any
from pathlib import Path
from ...utils.slice import FileSlice, DataSlice, RangeSlice
from ...utils.slice.base import SimpleSlice
from ...const import API_PATHS
from ...crypto.filename_encryptor import FilenameEncryptor, create_encryptor
from v123pan.exceptions import (
    FileUploadError,
    FileReadError,
    HashComputationError
)


class FileUploadProgress:
    """
    文件上传进度信息
    
    Attributes:
        uploaded_bytes: 已上传字节数
        total_bytes: 总字节数
        uploaded_parts: 已上传分片数
        total_parts: 总分片数
        current_part: 当前分片号
        percentage: 上传进度百分比（0-100）
    """

    def __init__(self, uploaded_bytes: int = 0, total_bytes: int = 0,
                 uploaded_parts: int = 0, total_parts: int = 0,
                 current_part: int = 0):
        self.uploaded_bytes = uploaded_bytes
        self.total_bytes = total_bytes
        self.uploaded_parts = uploaded_parts
        self.total_parts = total_parts
        self.current_part = current_part

    @property
    def percentage(self) -> float:
        """上传进度百分比"""
        if self.total_bytes == 0:
            return 0.0
        return (self.uploaded_bytes / self.total_bytes) * 100

    def __repr__(self) -> str:
        return (f"FileUploadProgress(uploaded={self.uploaded_bytes}/{self.total_bytes}, "
                f"parts={self.uploaded_parts}/{self.total_parts}, "
                f"progress={self.percentage:.2f}%)")


ProgressCallback = Callable[[FileUploadProgress], None]


class FileUploader:
    """
    文件上传器
    
    封装完整的文件上传流程，支持秒传和分片上传。
    
    特性:
    - 自动选择秒传或分片上传
    - 支持进度回调
    - 完整的错误处理和重试
    - 支持文件路径和字节数据
    - 使用 FileSlice 进行高效的分片读取
    
    使用示例:
        >>> uploader = FileUploader(client)
        >>> result = await uploader.upload(
        ...     "/path/to/file.dat",
        ...     parent_id=0,
        ...     progress_callback=lambda p: print(f"进度: {p.percentage}%")
        ... )
        >>> file_id = result["fileID"]
    """

    _MAX_RETRIES: int = 3
    _RETRY_DELAY: float = 1.0
    _REQUEST_TIMEOUT: float = 30.0

    def __init__(self, client):
        """
        初始化文件上传器
        
        Args:
            client: V123Pan 客户端实例
        """
        self.client = client
        self._http_client: Optional[httpx.AsyncClient] = None

    async def _get_http_client(self) -> httpx.AsyncClient:
        """获取或创建 HTTP 客户端"""
        if self._http_client is None:
            self._http_client = httpx.AsyncClient(timeout=300.0)
        return self._http_client

    async def close(self) -> None:
        """关闭资源"""
        if self._http_client is not None:
            await self._http_client.aclose()
            self._http_client = None

    async def upload(
        self,
        file_path: Union[str, bytes, SimpleSlice],
        filename: Optional[str] = None,
        parent_id: int = 0,
        duplicate: Optional[int] = None,
        progress_callback: Optional[ProgressCallback] = None,
        use_sha1_reuse: bool = True
    ) -> Dict[str, Any]:
        """
        上传文件
        
        自动选择秒传或分片上传方式：
        1. 优先尝试 SHA1 秒传（如果启用）
        2. 如果秒传失败，使用 MD5 分片上传
        
        Args:
            file_path: 文件路径、字节数据或 SimpleSlice 对象
            filename: 文件名，如果为 None 则从文件路径提取
            parent_id: 父目录ID，根目录为 0
            duplicate: 文件处理策略，1保留两者，2覆盖原文件
            progress_callback: 进度回调函数
            use_sha1_reuse: 是否使用 SHA1 秒传，默认为 True
        
        Returns:
            Dict[str, Any]: 上传结果，包含 fileID
        
        Raises:
            FileUploadError: 上传失败
        
        Example:
            >>> result = await uploader.upload(
            ...     "/path/to/file.dat",
            ...     filename="myfile.dat",
            ...     parent_id=0,
            ...     progress_callback=lambda p: print(f"{p.percentage:.1f}%")
            ... )
            >>> file_id = result["fileID"]
        """
        if isinstance(file_path, SimpleSlice):
            file_slice = file_path
            if filename is None:
                raise FileUploadError("SimpleSlice 上传必须指定 filename")
            file_size = len(file_slice)
            close_slice = False
        elif isinstance(file_path, bytes):
            if filename is None:
                raise FileUploadError("字节数据上传必须指定 filename")
            file_slice = DataSlice(file_path)
            file_size = len(file_slice)
            close_slice = True
        else:
            if filename is None:
                filename = os.path.basename(file_path)
            file_slice = FileSlice(file_path)
            file_size = len(file_slice)
            close_slice = True

        try:
            return await self._upload_by_parts(
                file_slice,
                filename,
                file_size,
                parent_id,
                duplicate,
                progress_callback
            )
        finally:
            if close_slice:
                await file_slice.close()

    async def _upload_by_parts(
        self,
        file_slice: Union[FileSlice, DataSlice],
        filename: str,
        file_size: int,
        parent_id: int,
        duplicate: Optional[int],
        progress_callback: Optional[ProgressCallback]
    ) -> Dict[str, Any]:
        """
        分片上传文件
        
        Args:
            file_slice: 文件分片对象
            filename: 文件名
            file_size: 文件大小
            parent_id: 父目录 ID
            duplicate: 文件处理策略
            progress_callback: 进度回调函数
        
        Returns:
            Dict[str, Any]: 上传结果
        
        Raises:
            FileUploadError: 上传失败
        """
        hash_result = await file_slice.compute_hash()
        md5 = hash_result["md5"]

        self.client.logger.info(f"开始分片上传: {filename}, 大小: {file_size} 字节")

        create_result = await self.client.file(
            parent_file_id=parent_id,
            filename=filename,
            etag=md5,
            size=file_size,
            duplicate=duplicate
        )

        self.client.logger.info(f"创建文件响应: {create_result}")

        if create_result.get("data") is None:
            raise FileUploadError(f"创建文件失败，响应数据为空: {create_result}")

        if create_result["data"]["reuse"]:
            self.client.logger.info(f"MD5 秒传成功: {filename}")
            return create_result

        preupload_id = create_result["data"]["preuploadID"]
        slice_size = create_result["data"]["sliceSize"]
        total_parts = (file_size + slice_size - 1) // slice_size

        self.client.logger.info(f"需要上传 {total_parts} 个分片，每片 {slice_size} 字节")

        await self._upload_slices(
            file_slice,
            preupload_id,
            slice_size,
            total_parts,
            progress_callback
        )

        return await self._complete_upload(preupload_id)

    async def _upload_slices(
        self,
        file_slice: FileSlice,
        preupload_id: str,
        slice_size: int,
        total_parts: int,
        progress_callback: Optional[ProgressCallback]
    ) -> None:
        """
        上传所有分片（串行）
        
        Args:
            file_slice: 文件分片对象
            preupload_id: 预上传ID
            slice_size: 分片大小
            total_parts: 总分片数
            progress_callback: 进度回调函数
        
        Raises:
            FileUploadError: 上传失败
        """
        total_bytes = len(file_slice)
        uploaded_parts = 0

        for part_no in range(1, total_parts + 1):
            await self._upload_single_slice(
                file_slice,
                preupload_id,
                part_no,
                slice_size
            )
            
            uploaded_parts += 1
            uploaded_bytes = min(uploaded_parts * slice_size, total_bytes)
            
            if progress_callback is not None:
                progress = FileUploadProgress(
                    uploaded_bytes=uploaded_bytes,
                    total_bytes=total_bytes,
                    uploaded_parts=uploaded_parts,
                    total_parts=total_parts,
                    current_part=part_no
                )
                progress_callback(progress)
            
            self.client.logger.debug(f"分片 {part_no}/{total_parts} 上传完成")

    async def _upload_single_slice(
        self,
        file_slice: Union[FileSlice, DataSlice],
        preupload_id: str,
        part_no: int,
        slice_size: int
    ) -> None:
        """
        上传单个分片
        
        Args:
            file_slice: 文件分片对象
            preupload_id: 预上传ID
            part_no: 分片序号
            slice_size: 分片大小
        
        Raises:
            FileUploadError: 上传失败
        """
        url_result = await self._upload_with_retry(
            lambda: self.client.url(preupload_id, part_no)
        )
        upload_url = url_result["data"]["presignedURL"]

        start_pos = (part_no - 1) * slice_size
        end_pos = min(start_pos + slice_size, len(file_slice))

        try:
            await self._upload_slice_with_retry(upload_url, file_slice, start_pos, end_pos)
        except Exception as e:
            raise FileUploadError(f"分片 {part_no} 上传失败: {e}") from e

    async def _upload_slice_with_retry(
        self,
        upload_url: str,
        file_slice: Union[FileSlice, DataSlice],
        start_pos: int,
        end_pos: int
    ) -> None:
        """
        带重试的分片上传
        
        Args:
            upload_url: 上传地址
            file_slice: 文件分片对象
            start_pos: 起始位置
            end_pos: 结束位置
        
        Raises:
            FileUploadError: 上传失败
        """
        http_client = await self._get_http_client()

        for attempt in range(self._MAX_RETRIES):
            try:
                range_slice = RangeSlice(file_slice, start_pos, end_pos)
                data = await range_slice.read_abstract()
                response = await http_client.put(
                    upload_url,
                    content=data,
                    headers={'Content-Type': 'application/octet-stream'}
                )
                response.raise_for_status()
                return
            except httpx.HTTPStatusError as e:
                if attempt == self._MAX_RETRIES - 1:
                    raise FileUploadError(f"分片上传失败: {e}")
                self.client.logger.warning(f"分片上传失败，第 {attempt + 1} 次重试: {e}")
                await asyncio.sleep(self._RETRY_DELAY * (attempt + 1))
            except Exception as e:
                if attempt == self._MAX_RETRIES - 1:
                    raise FileUploadError(f"分片上传异常: {e}")
                self.client.logger.warning(f"分片上传异常，第 {attempt + 1} 次重试: {e}")
                await asyncio.sleep(self._RETRY_DELAY * (attempt + 1))

    async def _upload_with_retry(self, upload_func) -> Dict[str, Any]:
        """
        带重试的上传操作
        
        Args:
            upload_func: 上传函数
        
        Returns:
            Dict[str, Any]: API 响应
        
        Raises:
            FileUploadError: 上传失败
        """
        for attempt in range(self._MAX_RETRIES):
            try:
                return await asyncio.wait_for(upload_func(), timeout=self._REQUEST_TIMEOUT)
            except asyncio.TimeoutError:
                if attempt == self._MAX_RETRIES - 1:
                    raise FileUploadError(f"上传操作超时: {self._REQUEST_TIMEOUT}秒")
                self.client.logger.warning(f"上传操作超时，第 {attempt + 1} 次重试")
                await asyncio.sleep(self._RETRY_DELAY * (attempt + 1))
            except Exception as e:
                if attempt == self._MAX_RETRIES - 1:
                    raise FileUploadError(f"上传操作失败: {e}")
                self.client.logger.warning(f"上传操作失败，第 {attempt + 1} 次重试: {e}")
                await asyncio.sleep(self._RETRY_DELAY * (attempt + 1))

    async def _complete_upload(self, preupload_id: str) -> Dict[str, Any]:
        """
        完成上传
        
        Args:
            preupload_id: 预上传ID
        
        Returns:
            Dict[str, Any]: 上传结果
        
        Raises:
            FileUploadError: 上传失败
        """
        complete_result = await self._upload_with_retry(
            lambda: self.client.complete(preupload_id)
        )

        if not complete_result["data"]["async"]:
            self.client.logger.info("上传完成")
            return complete_result

        self.client.logger.info("服务器正在异步处理，开始轮询")
        return await self._poll_upload_result(preupload_id)

    async def _poll_upload_result(self, preupload_id: str) -> Dict[str, Any]:
        """
        轮询上传结果
        
        Args:
            preupload_id: 预上传ID
        
        Returns:
            Dict[str, Any]: 上传结果
        
        Raises:
            FileUploadError: 上传失败
        """
        max_polls = 60
        poll_interval = 1.0

        for poll_count in range(max_polls):
            result = await self._upload_with_retry(
                lambda: self.client.result(preupload_id)
            )

            if result["data"]["completed"]:
                self.client.logger.info(f"异步处理完成，轮询 {poll_count + 1} 次")
                return result

            self.client.logger.debug(f"异步处理中，等待 {poll_interval} 秒后重试")
            await asyncio.sleep(poll_interval)

        raise FileUploadError(f"异步处理超时，已轮询 {max_polls} 次")

    async def __aenter__(self) -> 'FileUploader':
        """异步上下文管理器入口"""
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """异步上下文管理器退出"""
        await self.close()

    async def upload_crypt(
        self,
        file: Union[str, bytes, SimpleSlice],
        filename: Optional[str] = None,
        parent_id: int = 0,
        duplicate: Optional[int] = None,
        progress_callback: Optional[ProgressCallback] = None,
        encrypt_password: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        上传加密文件
        
        如果提供了 encrypt_password，会对文件名进行加密后再上传。
        文件内容保持不变（如果需要加密内容，请使用 ChaCha20Poly1305Slice）。
        
        Args:
            file: 文件路径、字节数据或 SimpleSlice 对象
            filename: 文件名，如果为 None 则从文件路径提取
            parent_id: 父目录ID，根目录为 0
            duplicate: 文件处理策略，1保留两者，2覆盖原文件
            progress_callback: 进度回调函数
            encrypt_password: 文件名加密密码，如果为 None 则不加密文件名
        
        Returns:
            Dict[str, Any]: 上传结果，包含 fileID
        
        Raises:
            FileUploadError: 上传失败
        
        Example:
            >>> result = await uploader.upload_crypt(
            ...     "/path/to/file.dat",
            ...     filename="myfile.dat",
            ...     parent_id=0,
            ...     encrypt_password="my_password"
            ... )
            >>> file_id = result["fileID"]
        """
        if encrypt_password is not None:
            if filename is None:
                if isinstance(file, str):
                    filename = os.path.basename(file)
                else:
                    raise FileUploadError("加密文件名必须指定 filename")
            
            encryptor = create_encryptor(encrypt_password)
            encrypted_filename = encryptor.encrypt(filename)
            
            self.client.logger.info(f"加密文件名：{filename} -> {encrypted_filename}")
            filename = encrypted_filename
        
        return await self.upload(
            file,
            filename=filename,
            parent_id=parent_id,
            duplicate=duplicate,
            progress_callback=progress_callback
        )
