"""认证模块"""
import _thread
import base64
import json
import logging
import os
import sys
import tempfile
import time
from dataclasses import asdict
from http.server import HTTPServer
from pathlib import Path
from typing import Callable, overload, List, NoReturn, Dict, Tuple

import coloredlogs
import qrcode
import qrcode_terminal
import requests
import yagmail

from aligo.core.Config import *
from aligo.types import *
from aligo.types.Enum import *
from .LoginServer import LoginServer

aligo_config_folder = Path.home().joinpath('.aligo')
aligo_config_folder.mkdir(parents=True, exist_ok=True)


def get_configurations() -> List[str]:
    """获取配置文件列表"""
    list_: List[str] = []
    file_: os.DirEntry
    for file_ in os.scandir(aligo_config_folder):
        list_.append(os.path.splitext(file_.name)[0])
    return list_


class Auth:
    """..."""

    def debug_log(self, response: requests.Response) -> NoReturn:
        """打印错误日志, 便于分析调试"""
        r = response.request
        self.log.warning(f'[method status_code] {r.method} {response.status_code}')
        self.log.warning(f'[url] {response.url}')
        self.log.warning(f'[headers] {r.headers}')
        self.log.warning(f'[request body] {r.body}')
        self.log.warning(f'[response body] {response.text[:200]}')

    def error_log_exit(self, response: requests.Response) -> NoReturn:
        """打印错误日志并退出"""
        self.debug_log(response)
        exit(-1)

    @overload
    def __init__(
            self,
            name: str = 'aligo',
            show: Callable[[str], NoReturn] = None,
            level=logging.DEBUG,
            proxies: Dict = None,
            port: int = None,
            email: Tuple[str, str] = None,
    ):
        """扫描二维码登录"""

    @overload
    def __init__(
            self,
            name: str = 'aligo',
            refresh_token: str = None,
            level=logging.DEBUG,
            proxies: Dict = None,
            port: int = None,
            email: Tuple[str, str] = None,
    ):
        """refresh_token 登录"""

    def __init__(
            self, name: str = 'aligo',
            refresh_token: str = None,
            show: Callable[[str], NoReturn] = None,
            level: int = logging.DEBUG,
            proxies: Dict = None,
            port: int = None,
            email: Tuple[str, str] = None,
    ):
        """登录验证

        :param name: (可选, 默认: aligo) 配置文件名称, 便于使用不同配置文件进行身份验证
        :param refresh_token:
        :param show: (可选) 显示二维码的函数
        :param level: (可选) 控制控制台输出
        :param proxies: (可选) 自定义代理 [proxies={"https":"localhost:10809"}],支持 http 和 socks5（具体参考requests库的用法）
        :param port: (可选) 开启 http server 端口，用于网页端扫码登录. 提供此值时，将不再弹出或打印二维码
        :param email: (可选) 发送扫码登录邮件 ("接收邮件的邮箱地址", "防伪字符串"). 提供此值时，将不再弹出或打印二维码
            关于防伪字符串: 为了方便大家使用, aligo 自带公开邮箱, 省去邮箱配置的麻烦.
                        所以收到登录邮件后, 一定要对比确认防伪字符串和你设置一致才可扫码登录, 否则将导致: 包括但不限于云盘文件泄露.
        """
        self._name_name = name
        self._name = aligo_config_folder.joinpath(f'{name}.json')
        self._port = port
        self._webServer: HTTPServer = None  # type: ignore
        self._email = email
        self.log = logging.getLogger(f'{__name__}:{name}')

        fmt = f'%(asctime)s.%(msecs)03d {name}.%(levelname)s %(message)s'

        coloredlogs.install(
            level=level,
            logger=self.log,
            milliseconds=True,
            datefmt='%X',
            fmt=fmt
        )

        self.log.info(f'Config {self._name}')
        self.log.info(f'日志等级 {logging.getLevelName(level)}')

        #
        self.session = requests.session()
        self.session.trust_env = False
        self.session.proxies = proxies
        self.session.params.update(UNI_PARAMS)  # type:ignore
        self.session.headers.update(UNI_HEADERS)

        self.session.get(AUTH_HOST + V2_OAUTH_AUTHORIZE, params={
            'login_type': 'custom',
            'response_type': 'code',
            'redirect_uri': 'https://www.aliyundrive.com/sign/callback',
            'client_id': CLIENT_ID,
            'state': r'{"origin":"file://"}',
            # 'state': '{"origin":"https://www.aliyundrive.com"}',
        }, stream=True).close()

        #
        SESSIONID = self.session.cookies.get('SESSIONID')
        self.log.debug(f'SESSIONID {SESSIONID}')

        #
        self.token: Optional[Token] = None
        if show is None:
            if os.name == 'nt':
                self.log.debug('Windows 操作系统')
                show = self._show_qrcode_in_window
            elif sys.platform.startswith('darwin'):
                self.log.debug('MacOS 操作系统')
                show = self._show_qrcode_in_window
            else:
                self.log.debug('类 Unix 操作系统')
                show = self._show_console
        self._show = show

        if refresh_token:
            self.log.debug('登录方式 refresh_token')
            self._refresh_token(refresh_token)
            return

        if self._name.exists():
            self.log.info(f'加载配置文件 {self._name}')
            self.token = Token(**json.load(self._name.open()))
        else:
            self.log.info('登录方式 扫描二维码')
            self._login()

        #
        self.session.headers.update({
            'Authorization': f'Bearer {self.token.access_token}'
        })

    def _save(self) -> NoReturn:
        """保存配置文件"""
        self.log.info(f'保存配置文件 {self._name}')
        json.dump(asdict(self.token), self._name.open('w'))

    def _login(self):
        """登录"""
        self.log.info('开始登录')
        response = self._login_by_qrcode()

        if response.status_code != 200:
            self.log.error('登录失败')
            self.error_log_exit(response)

        bizExt = response.json()['content']['data']['bizExt']
        bizExt = base64.b64decode(bizExt).decode('gb18030')

        # 获取解析出来的 refreshToken, 使用这个token获取下载链接是直链, 不需要带 headers
        refresh_token = json.loads(bizExt)['pds_login_result']['refreshToken']
        self._refresh_token(refresh_token, True)

    def _login_by_qrcode(self) -> requests.Response:
        """二维码登录"""
        response = self.session.get(
            PASSPORT_HOST + NEWLOGIN_QRCODE_GENERATE_DO
        )
        data = response.json()['content']['data']

        qr_link = data['codeContent']

        # 开启服务
        if self._port or self._email:
            if self._port:
                self.log.info(f'请访问 http://<YOUR_IP>:{self._port} 扫描二维码')
                _thread.start_new_thread(self._show_qrcode_in_web, (qr_link,))
            if self._email:
                self._send_email(qr_link)
        else:
            qrcode_png = self._show(qr_link)
            if qrcode_png:
                self.log.info(f'二维码图片文件 {qrcode_png}')
        self.log.info('等待扫描二维码')

        while True:
            response = self.session.post(
                PASSPORT_HOST + NEWLOGIN_QRCODE_QUERY_DO,
                data=data
            )
            login_data = response.json()['content']['data']
            qrCodeStatus = login_data['qrCodeStatus']
            if qrCodeStatus == 'NEW':
                pass
            elif qrCodeStatus == 'SCANED':
                self.log.info('已扫描 等待确认')
            elif qrCodeStatus == 'CONFIRMED':
                self.log.info(f'已确认 可关闭二维码窗口')
                if self._port:
                    # noinspection PyBroadException
                    try:
                        requests.get(f'http://localhost:{self._port}/close')
                    except:
                        pass
                return response
            else:
                self.log.warning('未知错误 可能二维码已经过期')
                self.error_log_exit(response)
            time.sleep(2)

    def _refresh_token(self, refresh_token=None, loop_call: bool = False):
        """刷新 token"""
        if refresh_token is None:
            refresh_token = self.token.refresh_token
        self.log.info('refresh token')
        response = self.session.post(
            API_HOST + V2_ACCOUNT_TOKEN,
            json={
                'refresh_token': refresh_token,
                'grant_type': 'refresh_token'
            }
        )
        if response.status_code == 200:
            self.log.info('刷新 token 成功')
            self.token = Token(**response.json())
            self._save()
        elif not loop_call:
            self.log.warning('刷新 token 失败')
            self.debug_log(response)
            self._login()

        self.session.headers.update({
            'Authorization': f'Bearer {self.token.access_token}'
        })

    def request(self, method: str, url: str,
                params: Dict = None, headers: Dict = None, data=None,
                verify: bool = None, body: Dict = None) -> requests.Response:
        """统一请求方法"""
        # 删除值为None的键
        if body is not None:
            body = {k: v for k, v in body.items() if v is not None}

        if data is not None and isinstance(data, dict):
            data = {k: v for k, v in data.items() if v is not None}

        response = None
        for i in range(1, 4):
            response = self.session.request(method=method, url=url, params=params,
                                            data=data, headers=headers, verify=verify, json=body)
            status_code = response.status_code
            self.log.info(
                f'{response.request.method} {response.url} {status_code} {response.headers.get("Content-Length", 0)}'
            )
            if status_code == 401 or (
                    # aims search 手机端apis
                    status_code == 400 and response.text.startswith('AccessToken is invalid')
            ):
                self._refresh_token()
                continue

            if status_code == 429 or status_code == 500:
                self.log.warning(f'被限制了 暂停 {5 ** i} 秒')
                time.sleep(5 ** i)
                continue

            return response

        self.log.info(f'重试 3 次仍旧失败')
        self.error_log_exit(response)

    def get(self, path: str, host: str = API_HOST, params: dict = None, headers: dict = None,
            verify: bool = None) -> requests.Response:
        """..."""
        return self.request(method='GET', url=host + path, params=params,
                            headers=headers, verify=verify)

    def post(self, path: str, host: str = API_HOST, params: dict = None, headers: dict = None, data: dict = None,
             verify: bool = None, body: dict = None) -> requests.Response:
        """..."""
        return self.request(method='POST', url=host + path, params=params, data=data,
                            headers=headers, verify=verify, body=body)

    @staticmethod
    def _show_console(qr_link: str) -> str:
        """
        在控制台上显示二维码
        :param qr_link: 二维码链接
        :return: NoReturn
        """
        qr_img = qrcode.make(qr_link)

        # try open image
        # 1.
        qr_img.show()

        # show qrcode on console
        # 2.
        qrcode_terminal.draw(qr_link)

        # save image to file
        # 3.
        qrcode_png = tempfile.mktemp('.png')
        qr_img.save(qrcode_png)
        return qrcode_png

    @staticmethod
    def _show_qrcode_in_window(qr_link: str) -> NoReturn:
        """
        通过 *.png 的关联应用程序显示 qrcode
        :param qr_link: 二维码链接
        :return: NoReturn
        """
        # show qrcode in windows & macos
        qr_img = qrcode.make(qr_link)
        qr_img.show()

    def _show_qrcode_in_web(self, qr_link: str) -> NoReturn:
        """浏览器显示二维码"""
        qr_img = qrcode.make(qr_link)
        qr_img.get_image()
        qr_img_path = tempfile.mktemp()
        qr_img.save(qr_img_path)
        self._webServer = HTTPServer(('0.0.0.0', self._port), LoginServer)
        self._webServer.qrData = open(qr_img_path, 'rb').read()
        os.remove(qr_img_path)
        # noinspection PyBroadException
        try:
            self._webServer.serve_forever()
        except:
            pass

    def _send_email(self, qr_link: str) -> NoReturn:
        """发送邮件"""
        qr_img = qrcode.make(qr_link)
        qr_img.get_image()
        qr_img_path = tempfile.mktemp('.png')
        qr_img.save(qr_img_path)
        yag = yagmail.SMTP({'aligo_notify@163.com': 'aligo notify'}, 'IYMQTISDOZYUMUFX', 'smtp.163.com', 465)
        yag.send(self._email[0], f'[阿里云盘/{self._name_name}] 扫码登录', self._email[1], attachments=[qr_img_path])
        self.log.info(f'登录二维码已发送至 {self._email[0]}')
        os.remove(qr_img_path)
