"""
所有和网络请求相关函数。

:author: assassing
:contact: https://github.com/hxz393
:copyright: Copyright 2024, hxz393. 保留所有权利。
"""
import time
from typing import Union, List, Any

import requests
from retrying import retry

from src.constants import HEADERS, BASE_URL


class Network:
    """
    网络请求相关类。
    注意，此类函数用 retry 来处理网络问题，达到重试次数则由主函数来捕获异常，中断运行。只要请求能返回数据，都可以正确处理。
    请求地址没有取全参数（params），只留关键参数。
    """

    def __init__(self):
        """
        初始化会话、请求头和 bdstoken。
        """
        self.s = requests.Session()
        self.headers = HEADERS
        self.bdstoken = ''
        # 忽略证书验证警告
        requests.packages.urllib3.disable_warnings()

    @retry(stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=2000)
    def get_bdstoken(self) -> Union[str, int]:
        """
        获取 bdstoken，用于创建、转存等操作，是所有其他请求的先决条件。
        获取到的 token 在整个会话中通用。

        :return: 获取成功返回 bdstoken，获取失败返回错误代码
        """
        url = f'{BASE_URL}/api/gettemplatevariable'
        params = {
            'clienttype': '0',
            'app_id': '38824127',
            'web': '1',
            'fields': '["bdstoken","token","uk","isdocuser","servertime"]'
        }

        r = self.s.get(url=url, params=params, headers=self.headers, timeout=10, allow_redirects=False, verify=False)
        if r.json()['errno'] != 0:
            return r.json()['errno']

        return r.json()['result']['bdstoken']

    @retry(stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=2000)
    def get_dir_list(self, folder_name: str) -> Union[List[Any], int]:
        """
        获取指定目录下的文件或目录列表。
        用于创建目录前，检查目录是否已存在；
        用于批量分享时，生成任务列表。

        :param folder_name: 指定要获取列表的目录名
        :return: 获取成功时返回文件列表，获取失败时返回错误代码
        """
        url = f'{BASE_URL}/api/list'
        params = {
            'order': 'time',
            'desc': '1',
            'showempty': '0',
            'web': '1',
            'page': '1',
            'num': '1000',
            'dir': folder_name,
            'bdstoken': self.bdstoken
        }

        r = self.s.get(url=url, params=params, headers=self.headers, timeout=15, allow_redirects=False, verify=False)
        if r.json()['errno'] != 0:
            return r.json()['errno']

        return r.json()['list']

    @retry(stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=2000)
    def create_dir(self, folder_name: str) -> int:
        """
        新建指定目录。
        用于批量转存前，建立缺失的目标目录。

        :param folder_name: 指定要建立的目录名
        :return: 获取请求返回的代码，成功时返回 0
        """
        url = f'{BASE_URL}/api/create'
        params = {
            'a': 'commit',
            'bdstoken': self.bdstoken
        }
        data = {
            'path': folder_name,
            'isdir': '1',
            'block_list': '[]',
        }

        r = self.s.post(url=url, params=params, headers=self.headers, data=data, timeout=15, allow_redirects=False, verify=False)

        return r.json()['errno']

    @retry(stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=2000)
    def verify_pass_code(self, link_url: str, pass_code: str) -> Union[str, int]:
        """
        验证提取码是否正确。
        如果正确，则会返回转存所必须的 randsk，也就是 bdclnd 参数。

        :param link_url: 网盘地址
        :param pass_code: 提取码
        :return: 成功时返回 randsk 字符串，失败时返回错误代码
        """
        url = f'{BASE_URL}/share/verify'
        params = {
            # 可放心用暴力切片
            'surl': link_url[25:48],
            'bdstoken': self.bdstoken,
            # 当前时间的毫秒级时间戳
            't': str(int(round(time.time() * 1000))),
            # 下面是不明所以的固定参数
            'channel': 'chunlei',
            'web': '1',
            'clienttype': '0'
        }
        data = {
            'pwd': pass_code,
            # 并没有发现下面两个参数的用途
            'vcode': '',
            'vcode_str': ''
        }

        r = self.s.post(url=url, params=params, headers=self.headers, data=data, timeout=10, allow_redirects=False, verify=False)
        if r.json()['errno'] != 0:
            return r.json()['errno']

        return r.json()['randsk']

    @retry(stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=2000)
    def get_transfer_params(self, url: str) -> str:
        """
        更新 bdclnd 到 cookie 后，再次请求网盘链接，获取响应内容。
        请求需要允许跳转。
        请求不再需要提取码。

        :param url: 网盘地址
        :return: 返回原始请求内容，丢给 parse_response 函数取处理
        """
        r = self.s.get(url=url, headers=self.headers, timeout=15, verify=False)

        return r.content.decode("utf-8")

    @retry(stop_max_attempt_number=5, wait_random_min=1000, wait_random_max=2000)
    def transfer_file(self, params_list: List[str], folder_name: str) -> int:
        """
        转存百度网盘文件。

        :param params_list: 带有 shareid、share_uk 和 fs_id 的列表
        :param folder_name: 转存目标目录
        :return: 返回转存请求结果代码
        """
        url = f'{BASE_URL}/share/transfer'
        params = {
            # shareid 是文件 id
            'shareid': params_list[0],
            # share_uk 猜是分享者的 id
            'from': params_list[1],
            'bdstoken': self.bdstoken,
            'channel': 'chunlei',
            'web': '1',
            'clienttype': '0'
        }
        data = {
            # 针对一个分享链接带有多个分享文件的情况，转换一下列表格式
            'fsidlist': f"[{','.join(params_list[2])}]",
            # 目标目录为空，则直接等于根目录 '/'
            'path': f'/{folder_name}'
        }

        r = self.s.post(url=url, params=params, headers=self.headers, data=data, timeout=30, allow_redirects=False, verify=False)

        return r.json()['errno']

    @retry(stop_max_attempt_number=3, wait_random_min=1000, wait_random_max=2000)
    def create_share(self, fs_id: int, expiry: str, password: str) -> Union[str, int]:
        """
        生成百度网盘分享链接。

        :param fs_id: 文件或目录独一无二的 id
        :param expiry: 自定义失效时长
        :param password: 自定义提取码
        :return: 成功时返回生成的分享链接，失败时返回错误代码
        """
        url = f'{BASE_URL}/share/set'
        params = {
            'channel': 'chunlei',
            'bdstoken': self.bdstoken,
            'clienttype': '0',
            'app_id': '250528',
            'web': '1'
        }
        data = {
            'period': expiry,
            'pwd': password,
            'eflag_disable': 'true',
            'channel_list': '[]',
            'schannel': '4',
            'fid_list': f'[{fs_id}]'
        }

        r = self.s.post(url=url, params=params, headers=self.headers, data=data, timeout=15, allow_redirects=False, verify=False)
        if r.json()['errno'] != 0:
            return r.json()['errno']

        return r.json()['link']
